ca.sqlpower.wabit.swingui.QueryPanel.java Source code

Java tutorial

Introduction

Here is the source code for ca.sqlpower.wabit.swingui.QueryPanel.java

Source

/*
 * Copyright (c) 2008, SQL Power Group Inc.
 *
 * This file is part of Wabit.
 *
 * Wabit is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * Wabit is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>. 
 */

package ca.sqlpower.wabit.swingui;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.datatransfer.Transferable;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DragGestureEvent;
import java.awt.dnd.DragGestureListener;
import java.awt.dnd.DragSource;
import java.awt.dnd.DragSourceAdapter;
import java.awt.event.ActionEvent;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.HierarchyEvent;
import java.awt.event.HierarchyListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.event.MouseMotionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.prefs.Preferences;

import javax.swing.AbstractAction;
import javax.swing.AbstractListModel;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTabbedPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.JToolBar;
import javax.swing.JTree;
import javax.swing.KeyStroke;
import javax.swing.ListModel;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;
import javax.swing.text.BadLocationException;
import javax.swing.tree.TreePath;

import net.miginfocom.swing.MigLayout;

import org.apache.log4j.Logger;
import org.fife.ui.rtextarea.RTextScrollPane;

import ca.sqlpower.architect.swingui.dbtree.DBTreeCellRenderer;
import ca.sqlpower.architect.swingui.dbtree.DBTreeModel;
import ca.sqlpower.object.ObjectDependentException;
import ca.sqlpower.object.SPVariableHelper;
import ca.sqlpower.query.Item;
import ca.sqlpower.query.SQLGroupFunction;
import ca.sqlpower.query.QueryImpl.OrderByArgument;
import ca.sqlpower.sql.JDBCDataSource;
import ca.sqlpower.sql.SPDataSource;
import ca.sqlpower.sql.SpecificDataSourceCollection;
import ca.sqlpower.sqlobject.SQLObject;
import ca.sqlpower.sqlobject.SQLObjectException;
import ca.sqlpower.sqlobject.SQLObjectRoot;
import ca.sqlpower.swingui.SPSwingWorker;
import ca.sqlpower.swingui.dbtree.SQLObjectSelection;
import ca.sqlpower.swingui.object.InsertVariableAction;
import ca.sqlpower.swingui.object.VariableInserter;
import ca.sqlpower.swingui.query.SQLQueryUIComponents;
import ca.sqlpower.swingui.query.TableChangeEvent;
import ca.sqlpower.swingui.query.TableChangeListener;
import ca.sqlpower.swingui.querypen.QueryPen;
import ca.sqlpower.swingui.table.FancyExportableJTable;
import ca.sqlpower.swingui.table.TableModelSortDecorator;
import ca.sqlpower.util.RunnableDispatcher;
import ca.sqlpower.util.WorkspaceContainer;
import ca.sqlpower.validation.swingui.StatusComponent;
import ca.sqlpower.wabit.WabitSessionContext;
import ca.sqlpower.wabit.rs.ResultSetEvent;
import ca.sqlpower.wabit.rs.ResultSetListener;
import ca.sqlpower.wabit.rs.ResultSetProducerEvent;
import ca.sqlpower.wabit.rs.ResultSetProducerListener;
import ca.sqlpower.wabit.rs.query.QueryCache;
import ca.sqlpower.wabit.swingui.action.CreateLayoutFromQueryAction;
import ca.sqlpower.wabit.swingui.action.ExportSQLScriptAction;
import ca.sqlpower.wabit.swingui.action.ExportWabitObjectAction;
import ca.sqlpower.wabit.swingui.action.NewChartAction;
import ca.sqlpower.wabit.swingui.action.ShowQueryPropertiesAction;

import com.jgoodies.forms.builder.ButtonBarBuilder;
import com.jgoodies.forms.builder.DefaultFormBuilder;
import com.jgoodies.forms.layout.FormLayout;

public class QueryPanel implements WabitPanel {

    /**
     * This icon is added to actions that export the query.
     */
    private static final ImageIcon EXPORT_ICON = new ImageIcon(
            QueryPanel.class.getClassLoader().getResource("icons/32x32/export.png"));

    /**
     * The icon added to actions that resets the query pen.
     */
    private static final ImageIcon RESET_ICON = new ImageIcon(
            QueryPanel.class.getClassLoader().getResource("icons/32x32/cancel.png"));

    /**
     * The icon added to actions that allow users to create a join between two
     * columns in two different tables.
     */
    private static final ImageIcon CREATE_JOIN_ICON = new ImageIcon(
            QueryPanel.class.getClassLoader().getResource("icons/32x32/join.png"));

    /**
     * Icon added to actions that create a chart based on the current query.
     */
    private static final ImageIcon CREATE_CHART_ICON = new ImageIcon(
            QueryPanel.class.getClassLoader().getResource("icons/32x32/chart.png"));

    /**
     * Icon added to actions that execute the current query.
     */
    private static final ImageIcon EXECUTE_ICON = new ImageIcon(
            QueryPanel.class.getClassLoader().getResource("icons/32x32/run.png"));

    /**
     * Icon on actions that reverses the last change to the editor.
     */
    private static final ImageIcon UNDO_ICON = new ImageIcon(
            QueryPanel.class.getClassLoader().getResource("icons/32x32/undo.png"));

    /**
     * Icon on actions that repeats the last action that was undone.
     */
    private static final ImageIcon REDO_ICON = new ImageIcon(
            QueryPanel.class.getClassLoader().getResource("icons/32x32/redo.png"));

    /**
     * Icon for the action to stop the query from executing.
     */
    private static final ImageIcon STOP_ICON = new ImageIcon(
            QueryPanel.class.getClassLoader().getResource("icons/32x32/stop.png"));

    //    /**
    //     * Icon added to actions that will change the editor to display the 
    //     * query executed just before the current query.
    //     */
    //    private static final ImageIcon PREV_QUERY_ICON = 
    //        new ImageIcon(QueryPanel.class.getClassLoader().getResource(
    //                "icons/32x32/previous.png"));

    private static final Logger logger = Logger.getLogger(QueryPanel.class);

    private static final String SQL_TEXT_TAB_HEADING = "SQL";

    private static final ImageIcon THROBBER = new ImageIcon(
            QueryPanel.class.getClassLoader().getResource("icons/throbber16-01.png"));

    private static final ImageIcon ICON = new ImageIcon(
            StatusComponent.class.getClassLoader().getResource("ca/sqlpower/swingui/query/search.png"));

    /**
     * This is the property name for changes to the width on a {@link TableColumn}.
     * The constant COLUMN_WIDTH_PROPERTY is not the property that will be fired
     * on a column width change.
     */
    private static final String TABLE_COLUMN_WIDTH = "preferredWidth";

    /**
     * The background colour given to the JTables when they are being updated. This will
     * give the users a more noticeable change when there is an update occurring.
     */
    private static final Color REFRESH_GREY = new Color(0xeeeeee);

    private static final Preferences prefs = Preferences.userNodeForPackage(QueryPanel.class);

    /**
     * Prefs key for the horizontal split pane's divider location.
     * <p>
     * The value stored under this key is an <code>int</code>.
     */
    private static final String RESULTS_DIVIDER_LOCATON_KEY = "QueryPanel.RESULTS_DIVIDER_LOCATON";

    /**
     * This is a listModel that just returns the row Number for the rowHeaderRender
     */
    private class RowListModel extends AbstractListModel {
        int tableRowSize;

        public RowListModel(JTable table) {
            tableRowSize = table.getRowCount();
        }

        public Object getElementAt(int index) {
            return index + 1;
        }

        public int getSize() {
            return tableRowSize;
        }

    }

    /**
     * This class will display a modal dialog when it is created that will
     * prompt the user if they want to continue executing a query that contains
     * cross joins. Their response and their choice to keep seeing the prompt
     * are retrievable from methods in this class.
     */
    private static class CrossJoinDialog {

        private boolean continuingExecution;

        private boolean dontAskAgain = false;

        public CrossJoinDialog(JFrame parent) {
            final JDialog crossJoinDialog = new JDialog(parent, "Query contains cross joins", true);
            JPanel crossJoinPanel = new JPanel(new MigLayout());
            final JLabel textLabel = new JLabel(
                    "<html>The query you are about to execute contains cross joins.<br> "
                            + "This query could take more time than expected to execute.<br> "
                            + "Do you wish to continue?</html>");
            textLabel.setHorizontalAlignment(SwingConstants.CENTER);
            crossJoinPanel.add(textLabel, "align 50%, span, wrap");
            final JCheckBox askAgainCheckBox = new JCheckBox("Do not ask me again.", dontAskAgain);
            crossJoinPanel.add(askAgainCheckBox, "align 50%, span, wrap");

            ButtonBarBuilder builder = new ButtonBarBuilder();
            builder.addGridded(new JButton(new AbstractAction("Continue") {

                public void actionPerformed(ActionEvent e) {
                    continuingExecution = true;
                    dontAskAgain = askAgainCheckBox.isSelected();
                    crossJoinDialog.dispose();
                }
            }));

            builder.addGridded(new JButton(new AbstractAction("Stop") {

                public void actionPerformed(ActionEvent e) {
                    continuingExecution = false;
                    dontAskAgain = askAgainCheckBox.isSelected();
                    crossJoinDialog.dispose();
                }
            }));
            crossJoinPanel.add(builder.getPanel(), "align right");

            crossJoinDialog.add(crossJoinPanel);
            crossJoinDialog.pack();
            crossJoinDialog.setLocationRelativeTo(parent);
            crossJoinDialog.setVisible(true);
        }

        public boolean isContinuingExecution() {
            return continuingExecution;
        }

        public boolean getDontAskAgain() {
            return dontAskAgain;
        }

    }

    private SQLQueryUIComponents queryUIComponents;
    private JCheckBox groupingCheckBox;
    private final JLabel groupingLabel = new JLabel("Group Function");
    private final JLabel havingLabel = new JLabel("Having");
    private final JLabel columnNameLabel = new JLabel();
    private QueryPen queryPen;

    /**
     * This is the panel in the top left of the results table. It will
     * give row headers for the group by and having fields.
     */
    private final JPanel cornerPanel;

    /**
     * Stores the parts of the query.
     */
    final private QueryCache queryCache;

    /**
     * This is the tabbed pane that contains the query pen and text editor.
     * All the query editing UI should be in this tabbed pane.
     */
    private JTabbedPane queryPenAndTextTabPane;

    private final QueryController queryController;

    private final WabitSwingSession session;

    private final WabitSwingSessionContext context;

    /**
     * The tree on the right-hand side that you drag tables into the query pen from.
     */
    private JTree dragTree;

    /**
     * Wraps {@link #dragTree}. This is the component returned by
     * {@link #getSourceComponent()}.
     */
    private JScrollPane dragTreeScrollPane;

    /**
     * NOTE: This is the combo box for database connections. Not sure why it's called
     * a report combo box.
     */
    private JComboBox reportComboBox;

    /**
     * This is the main JComponent for this query. All other components
     * are placed in this.
     */
    private final JSplitPane mainSplitPane;

    /**
     * This is the root of the JTree on the right of the query builder. This
     * will let the user drag and drop components into the query.
     */
    private SQLObjectRoot rootNode;

    /**
     * This is the panel that holds the QueryPen and the GUI SQL select in the tabbed pane.
     */
    private JPanel queryPenPanel;

    /**
     * This is the panel that holds the text editor for the query.
     */
    private JComponent queryToolPanel;

    /**
     * The field that will search for a given string across all result sets simultaneously.
     */
    private JTextField searchField;

    /**
     * This is the current column model of the JTable being displayed in the results.
     * The column model will tell the query cache the size changes of each column
     * to keep them the same size.
     */
    TableColumnModel tableColumnModel;

    /**
     * The listener that will update the column sizes in the model. This will
     * allow changing the query while keeping the sizes of the remaining columns
     * the same.
     */
    private final PropertyChangeListener resizingColumnChangeListener = new PropertyChangeListener() {
        public void propertyChange(PropertyChangeEvent evt) {
            if (evt.getPropertyName().equals(TABLE_COLUMN_WIDTH)
                    && !((Integer) evt.getNewValue()).equals(evt.getOldValue())) {
                Enumeration<TableColumn> columns = tableColumnModel.getColumns();
                int i = 0;
                while (columns.hasMoreElements()) {
                    if (columns.nextElement() == evt.getSource()) {
                        break;
                    }
                    i++;
                }
                logger.debug("Received column width change on column " + i + " the new width is "
                        + (Integer) evt.getNewValue());
                Item resizedItem = queryCache.getSelectedColumns().get(i);
                resizedItem.setColumnWidth((Integer) evt.getNewValue());

            }
        }
    };

    /**
     * This listens to mouse dragging of a column in a table. This handles 
     * auto-scrolling during the drag and drop operation, in the case that the
     * user wants to drag the column past the visible region on the screen.
     */
    private final MouseMotionListener reorderSelectionByHeaderAutoScrollTable = new MouseMotionAdapter() {
        public void mouseDragged(MouseEvent e) {
            Rectangle rect = new Rectangle(e.getX(), e.getY(), 1, 1);
            ((JTableHeader) e.getSource()).getTable().scrollRectToVisible(rect);
        }
    };

    private final ExportWabitObjectAction<QueryCache> exportQueryAction;

    /**
     * This action does the exporting of the query to a file or straight text.
     */
    private Action exportAction = new AbstractAction("Export", EXPORT_ICON) {
        public void actionPerformed(ActionEvent e) {
            if (e.getSource() instanceof JButton) {
                JButton source = (JButton) e.getSource();
                JPopupMenu popupMenu = new JPopupMenu();
                JMenuItem menuItem = new JMenuItem(exportQueryAction);
                menuItem.setText("Export Query to Workspace file");
                popupMenu.add(menuItem);
                menuItem = new JMenuItem(new ExportSQLScriptAction(session, queryCache));
                menuItem.setText("Export Query to SQL Script");
                popupMenu.add(menuItem);
                popupMenu.show(source, 0, source.getHeight());
            }
        }
    };

    /**
     * This toolbar builder is a permanent fixture of the query panel, but it
     * does get cleared out and refilled with different actions whenever we
     * switch views between the GUI (querypen) and the textual SQL editor.
     * 
     * @see #changeToTextToolBar()
     * @see #changeToGUIToolBar()
     */
    private final WabitToolBarBuilder toolBarBuilder = new WabitToolBarBuilder();

    private ResultSetListener resultSetListener = new ResultSetListener() {
        public void newData(ResultSetEvent evt) {
            // don't care
        }

        public void executionStarted(ResultSetEvent evt) {
            columnNameLabel.setIcon(THROBBER);
            queryUIComponents.getStopButton().setEnabled(true);
        }

        public void executionComplete(ResultSetEvent evt) {
            columnNameLabel.setIcon(null);
            queryUIComponents.getStopButton().setEnabled(false);
            if (evt.getSourceHandle().getException() != null) {
                String errorMessage = SQLQueryUIComponents
                        .createErrorStringMessage(evt.getSourceHandle().getException());
                queryUIComponents.getLogTextArea().append(errorMessage + "\n");
                queryUIComponents.getLogTextArea()
                        .setCaretPosition(queryUIComponents.getLogTextArea().getDocument().getLength());
                SwingUtilities.invokeLater(new Runnable() {
                    public void run() {
                        queryUIComponents.getResultTabPane().setSelectedIndex(0);
                    }
                });
            }
        }
    };

    private ResultSetProducerListener rsProducerListener = new ResultSetProducerListener() {
        public void structureChanged(ResultSetProducerEvent evt) {
            boolean disableAutoExecute = context.getPrefs()
                    .getBoolean(WabitSessionContext.DISABLE_QUERY_AUTO_EXECUTE, false);
            if (queryCache.isAutomaticallyExecuting() && !disableAutoExecute) {
                execute();
            }
        }

        public void executionStopped(ResultSetProducerEvent evt) {
            // don't care
        }

        public void executionStarted(ResultSetProducerEvent evt) {
            // don't care
        }
    };

    private class CustomSQLObjectRoot extends SQLObjectRoot {
        @Override
        public WorkspaceContainer getWorkspaceContainer() {
            return session;
        }

        @Override
        public RunnableDispatcher getRunnableDispatcher() {
            return session;
        }
    }

    public QueryPanel(WabitSwingSession session, QueryCache cache) {
        logger.debug("Constructing new QueryPanel@" + System.identityHashCode(this));
        this.session = session;
        context = (WabitSwingSessionContext) session.getContext();
        mainSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
        queryCache = cache;

        queryCache.setResultSetListener(resultSetListener);
        queryCache.addResultSetProducerListener(rsProducerListener);

        final Action queryPenExecuteButtonAction = new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                execute();
            }
        };
        queryPen = new QueryPen(queryPenExecuteButtonAction, queryCache);
        queryPen.setExecuteIcon((ImageIcon) WabitIcons.RUN_ICON_32);
        queryPen.getGlobalWhereText().setText(cache.getGlobalWhereClause());

        exportQueryAction = new ExportWabitObjectAction<QueryCache>(session, queryCache,
                WabitIcons.WABIT_FILE_ICON_16, "Export Query to Wabit file");

        queryUIComponents = new SQLQueryUIComponents(session,
                new SpecificDataSourceCollection<JDBCDataSource>(session.getWorkspace(), JDBCDataSource.class),
                context, mainSplitPane, queryCache);
        queryUIComponents.setRowLimitSpinner(context.getRowLimitSpinner());
        queryUIComponents.setShowSearchOnResults(false);
        queryController = new QueryController(queryCache, queryPen, queryUIComponents.getDatabaseComboBox(),
                queryUIComponents.getQueryArea(), queryPen.getZoomSlider());
        queryPen.setZoom(queryCache.getZoomLevel());
        reportComboBox = queryUIComponents.getDatabaseComboBox();

        cornerPanel = new JPanel();
        DefaultFormBuilder builder = new DefaultFormBuilder(new FormLayout("pref", "pref, pref, pref"),
                cornerPanel);
        groupingLabel.setFont(new JTableHeader().getFont());

        //Resize grouping and having labels to the height of a combo box to be spaced properly
        //beside the headers in the results table. This is done by a listener as the components
        //aren't realized until they are displayed.
        reportComboBox.addComponentListener(new ComponentAdapter() {
            public void componentResized(ComponentEvent e) {
                groupingLabel.setPreferredSize(new Dimension((int) groupingLabel.getPreferredSize().getWidth(),
                        reportComboBox.getHeight()));
                havingLabel.setPreferredSize(
                        new Dimension((int) havingLabel.getPreferredSize().getWidth(), reportComboBox.getHeight()));
            }
        });
        havingLabel.setFont(new JTableHeader().getFont());
        builder.append(groupingLabel);
        builder.append(havingLabel);
        builder.append(columnNameLabel);

        dragTree = new JTree() {
            public void expandPath(TreePath tp) {
                try {
                    if (tp.getLastPathComponent() instanceof SQLObject) {
                        ((SQLObject) tp.getLastPathComponent()).populate();
                    }
                    super.expandPath(tp);
                } catch (Exception ex) {
                    logger.warn("Unexpected exception while expanding path " + tp, ex); //$NON-NLS-1$
                }
            }

            @Override
            public void expandRow(int row) {
                if (getPathForRow(row).getLastPathComponent() instanceof SQLObject) {
                    try {
                        ((SQLObject) getPathForRow(row).getLastPathComponent()).populate();
                    } catch (SQLObjectException e) {
                        throw new RuntimeException(e);
                    }
                }
                super.expandRow(row);
            }
        };
        dragTree.setRootVisible(false);
        rootNode = new CustomSQLObjectRoot();
        reportComboBox.addActionListener(new AbstractAction() {
            public void actionPerformed(ActionEvent event) {
                try {
                    for (int i = rootNode.getChildren().size() - 1; i >= 0; i--) {
                        rootNode.removeChild(rootNode.getChildren().get(i));
                    }
                    if (reportComboBox.getSelectedItem() != null) {
                        // FIXME the session (or session context) should be maintaining a map of data
                        // sources to SQLDatabase instances. Each SQLDatabase instance has its own connection pool! 
                        rootNode.addChild(context.getDatabase((JDBCDataSource) reportComboBox.getSelectedItem()));
                        for (SQLObject child : rootNode.getChildren()) {
                            child.populate();
                        }
                        DBTreeModel tempTreeModel = new DBTreeModel(rootNode, dragTree);
                        dragTree.setModel(tempTreeModel);
                        dragTree.expandRow(0);
                        dragTree.setVisible(true);
                    }
                } catch (SQLObjectException e) {
                    throw new RuntimeException("Could not add DataSource to rootNode", e);
                } catch (ObjectDependentException e) {
                    throw new RuntimeException(e);
                }

            }
        });
        if (session.getWorkspace().getDataSources().size() != 0) {
            if (queryCache.getDatabase() == null) {
                dragTree.setVisible(false);
                List<SPDataSource> dataSources = session.getWorkspace().getConnections();
                List<JDBCDataSource> availableDS = new ArrayList<JDBCDataSource>();
                for (SPDataSource ds : dataSources) {
                    if (ds instanceof JDBCDataSource) {
                        availableDS.add((JDBCDataSource) ds);
                    }
                }
                final JDBCDataSource startingDataSource;
                if (availableDS.size() > 0) {
                    startingDataSource = (JDBCDataSource) availableDS.get(0);
                } else {
                    startingDataSource = null;
                }

                SPSwingWorker databaseLazyLoad = new SPSwingWorker(session) {
                    public void doStuff() throws Exception {
                        if (startingDataSource != null) {
                            //populate the database
                            context.getDatabase(startingDataSource);
                        }
                    }

                    public void cleanup() throws Exception {
                        if (reportComboBox.getSelectedItem() == null) {
                            reportComboBox.setSelectedItem(startingDataSource);
                            dragTree.setVisible(true);
                        }
                    }
                };
                //populates some data in a separate thread to create an easier workflow
                //when a user creates a new query (bug 2054)
                if (startingDataSource != null) {
                    databaseLazyLoad.run();
                }
            } else {
                reportComboBox.setSelectedItem((SPDataSource) queryCache.getDatabase().getDataSource());
                dragTree.setVisible(true);
            }
        } else {
            dragTree.setVisible(false);
        }

        dragTree.setCellRenderer(new DBTreeCellRenderer());
        DragSource ds = new DragSource();
        ds.createDefaultDragGestureRecognizer(dragTree, DnDConstants.ACTION_COPY, new DragGestureListener() {

            public void dragGestureRecognized(DragGestureEvent dge) {

                if (dragTree.getSelectionPaths() == null) {
                    return;
                }
                ArrayList<SQLObject> list = new ArrayList<SQLObject>();
                for (TreePath path : dragTree.getSelectionPaths()) {
                    Object selectedNode = path.getLastPathComponent();
                    if (!(selectedNode instanceof SQLObject)) {
                        throw new IllegalStateException(
                                "DBTrees are not allowed to contain non SQLObjects. This tree contains a "
                                        + selectedNode.getClass());
                    }
                    list.add((SQLObject) selectedNode);
                }

                Transferable dndTransferable = new SQLObjectSelection(list);
                dge.getDragSource().startDrag(dge, null, dndTransferable, new DragSourceAdapter() {
                    // no op 
                });

            }
        });

        queryUIComponents.addTableChangeListener(new TableChangeListener() {
            public void tableRemoved(TableChangeEvent e) {
                if (tableColumnModel != null) {
                    Enumeration<TableColumn> enumeration = tableColumnModel.getColumns();
                    while (enumeration.hasMoreElements()) {
                        enumeration.nextElement().removePropertyChangeListener(resizingColumnChangeListener);
                    }
                    e.getChangedTable().getTableHeader()
                            .removeMouseMotionListener(reorderSelectionByHeaderAutoScrollTable);
                }
            }

            public void tableAdded(final TableChangeEvent e) {

                logger.debug("Table added.");
                queryController.unlistenToCellRenderer();
                TableModelSortDecorator sortDecorator = null;
                final JTable table = e.getChangedTable();
                if (table instanceof FancyExportableJTable) {
                    FancyExportableJTable fancyTable = (FancyExportableJTable) table;
                    sortDecorator = fancyTable.getTableModelSortDecorator();
                }
                ComponentCellRenderer renderer = new ComponentCellRenderer(table, sortDecorator);
                table.getTableHeader().setDefaultRenderer(renderer);

                ListModel lm = new RowListModel(table);
                final JList rowHeader = new JList(lm);
                rowHeader.setFixedCellWidth(groupingLabel.getPreferredSize().width + 2);

                rowHeader.setCellRenderer(new RowHeaderRenderer(table));

                table.addPropertyChangeListener("rowHeight", new PropertyChangeListener() {
                    public void propertyChange(PropertyChangeEvent evt) {
                        rowHeader.setFixedCellHeight(table.getRowHeight());
                    }
                });

                rowHeader.setFixedCellHeight(table.getRowHeight());

                ((JScrollPane) table.getParent().getParent()).setRowHeaderView(rowHeader);

                ((JScrollPane) table.getParent().getParent()).setCorner(JScrollPane.UPPER_LEFT_CORNER, cornerPanel);
                addGroupingTableHeaders();

                tableColumnModel = e.getChangedTable().getColumnModel();
                Enumeration<TableColumn> enumeration = tableColumnModel.getColumns();
                while (enumeration.hasMoreElements()) {
                    enumeration.nextElement().addPropertyChangeListener(resizingColumnChangeListener);
                }
                table.getTableHeader().addMouseMotionListener(reorderSelectionByHeaderAutoScrollTable);

                //TODO: Add the new renderer to result sets on both tabs when a parser exists to go between them easier.
                if (queryPenAndTextTabPane.getSelectedComponent() != queryToolPanel) {
                    queryController.listenToCellRenderer(renderer);
                }

                columnNameLabel.setIcon(null);

                searchField.setDocument(queryUIComponents.getSearchDocument());

            }
        });

        buildUI();

        /*
         * Default split pane size is 3/4-1/4 of the screen height or else
         * the results won't be visible and the user won't see them update
         */
        mainSplitPane.setDividerLocation(prefs.getInt(RESULTS_DIVIDER_LOCATON_KEY,
                (int) (session.getContext().getFrame().getHeight() * 3 / 4)));

    }

    private void buildUI() {
        JTabbedPane resultPane = queryUIComponents.getResultTabPane();

        queryUIComponents.getQueryArea().setLineWrap(true);
        queryToolPanel = new RTextScrollPane(queryUIComponents.getQueryArea(), true);

        queryPenAndTextTabPane = new JTabbedPane();
        JPanel playPen = queryPen.createQueryPen();
        DefaultFormBuilder queryExecuteBuilder = new DefaultFormBuilder(new FormLayout("pref:grow, 10dlu, pref"));

        queryPenPanel = new JPanel(new BorderLayout());
        queryPenPanel.add(playPen, BorderLayout.CENTER);
        queryPenPanel.add(queryExecuteBuilder.getPanel(), BorderLayout.SOUTH);
        queryPenAndTextTabPane.add(queryPenPanel, "QueryPen");
        queryPenAndTextTabPane.add(queryToolPanel, SQL_TEXT_TAB_HEADING);
        if (queryCache.isScriptModified()) {
            queryPenAndTextTabPane.setSelectedComponent(queryToolPanel);
            queryUIComponents.getQueryArea().setText(queryCache.generateQuery());
        }

        // We need to map the insert var action here.
        final SPVariableHelper helper = new SPVariableHelper(queryCache);
        Action insertVariableAction = new InsertVariableAction("Variables", helper, null, new VariableInserter() {
            public void insert(String variable) {
                try {
                    queryUIComponents.getQueryArea().getDocument()
                            .insertString(queryUIComponents.getQueryArea().getCaretPosition(), variable, null);
                } catch (BadLocationException e) {
                    throw new AssertionError(e);
                }
            }
        }, queryUIComponents.getQueryArea());

        // Maps CTRL+SPACE to insert variable
        queryUIComponents.getQueryArea().getInputMap()
                .put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, InputEvent.CTRL_MASK), "insertVariable");
        queryUIComponents.getQueryArea().getActionMap().put("insertVariable", insertVariableAction);

        final JLabel whereText = new JLabel("Where:");

        final JPanel topPanel = new JPanel(new BorderLayout());
        topPanel.add(queryPenAndTextTabPane, BorderLayout.CENTER);

        DefaultFormBuilder builder = new DefaultFormBuilder(new FormLayout("pref, 5dlu, pref"));
        builder.setBorder(BorderFactory.createEmptyBorder(5, 5, 0, 5));
        builder.append("Database Connection", reportComboBox);
        topPanel.add(builder.getPanel(), BorderLayout.NORTH);

        if (queryPenPanel == queryPenAndTextTabPane.getSelectedComponent()) {
            changeToGUIToolBar();
        } else if (queryToolPanel == queryPenAndTextTabPane.getSelectedComponent()) {
            changeToTextToolBar();
        }

        queryPenAndTextTabPane.addChangeListener(new ChangeListener() {
            public void stateChanged(ChangeEvent e) {
                boolean execute = false;
                if (queryPenPanel == queryPenAndTextTabPane.getSelectedComponent()) {
                    //This is temporary until we can parse the user string and set the query cache to look like 
                    //the query the user modified.
                    int retval = JOptionPane.OK_OPTION;
                    if (queryCache.isScriptModified()) {
                        retval = JOptionPane.showConfirmDialog(getPanel(),
                                "Changes will be lost to the SQL script if you go back to the PlayPen. \nDo you wish to continue?",
                                "Changing", JOptionPane.YES_NO_OPTION);
                    }

                    if (retval != JOptionPane.OK_OPTION) {
                        queryPenAndTextTabPane.setSelectedComponent(queryToolPanel);
                    } else {

                        //The RTextArea needs to have its text set to the empty string
                        //or else it will set its text to the empty string before changing
                        //its text when we come back to the query side and it will get
                        //the query in a state where it thinks the user changed the query twice.
                        queryUIComponents.getQueryArea().setText(null);

                        queryCache.removeUserModifications();

                        boolean disableAutoExecute = context.getPrefs()
                                .getBoolean(WabitSessionContext.DISABLE_QUERY_AUTO_EXECUTE, false);
                        if (queryCache.isAutomaticallyExecuting() && !disableAutoExecute) {
                            execute = true;
                        }

                        queryPen.getGlobalWhereText().setVisible(true);
                        groupingCheckBox.setVisible(true);
                        whereText.setVisible(true);
                        changeToGUIToolBar();

                    }

                } else if (queryToolPanel == queryPenAndTextTabPane.getSelectedComponent()) {
                    if (!queryCache.isScriptModified()) {
                        queryUIComponents.getQueryArea().setText(queryCache.generateQuery());
                        // Since the query controller sets the userModifiedQuery property
                        // on any insert to the query area, we need to revert the value
                        // back to null. This is to ensure that if the user has not made
                        // any changes, changing back to the query pen will not prompt
                        // the user that "changes will be lost".
                        queryCache.removeUserModifications();

                        boolean disableAutoExecute = context.getPrefs()
                                .getBoolean(WabitSessionContext.DISABLE_QUERY_AUTO_EXECUTE, false);
                        if (queryCache.isAutomaticallyExecuting() && !disableAutoExecute) {
                            execute = true;
                        }
                    }
                    queryPen.getGlobalWhereText().setVisible(false);
                    groupingCheckBox.setVisible(false);
                    whereText.setVisible(false);
                    changeToTextToolBar();
                }

                if (execute) {
                    execute();
                }
            }
        });

        groupingCheckBox = new JCheckBox("Grouping");
        groupingCheckBox.setSelected(queryCache.isGroupingEnabled());
        groupingCheckBox.addActionListener(new AbstractAction() {

            public void actionPerformed(ActionEvent e) {
                queryCache.setGroupingEnabled(groupingCheckBox.isSelected());

                boolean disableAutoExecute = context.getPrefs()
                        .getBoolean(WabitSessionContext.DISABLE_QUERY_AUTO_EXECUTE, false);
                if (queryCache.isAutomaticallyExecuting() && !disableAutoExecute) {
                    execute();
                }
            }
        });
        FormLayout layout = new FormLayout("pref, 5dlu, pref, 3dlu, pref:grow, 5dlu, max(pref;50dlu)",
                "pref, fill:0dlu:grow");
        DefaultFormBuilder southPanelBuilder = new DefaultFormBuilder(layout);
        southPanelBuilder.append(groupingCheckBox);
        southPanelBuilder.append(whereText, queryPen.getGlobalWhereText());
        JPanel searchPanel = new JPanel(new BorderLayout());
        searchPanel.add(new JLabel(ICON), BorderLayout.WEST);
        searchField = new JTextField(queryUIComponents.getSearchDocument(), null, 0);
        searchPanel.add(searchField, BorderLayout.CENTER);
        southPanelBuilder.append(searchPanel);
        southPanelBuilder.nextLine();
        resultPane.setPreferredSize(new Dimension((int) resultPane.getPreferredSize().getWidth(), 0));
        southPanelBuilder.append(resultPane, 7);

        dragTreeScrollPane = new JScrollPane(dragTree);
        dragTreeScrollPane.setMinimumSize(new Dimension(DBTreeCellRenderer.DB_ICON.getIconWidth() * 3, 0));

        mainSplitPane.setOneTouchExpandable(true);
        mainSplitPane.setResizeWeight(1);
        mainSplitPane.add(topPanel, JSplitPane.TOP);
        JPanel bottomPanel = southPanelBuilder.getPanel();
        mainSplitPane.add(bottomPanel, JSplitPane.BOTTOM);
        bottomPanel.setMinimumSize(new Dimension(0, ICON.getIconHeight() * 5));

        mainSplitPane.addHierarchyListener(new HierarchyListener() {
            public void hierarchyChanged(HierarchyEvent e) {
                boolean disableAutoExecute = context.getPrefs()
                        .getBoolean(WabitSessionContext.DISABLE_QUERY_AUTO_EXECUTE, false);
                if ((e.getChangeFlags() & HierarchyEvent.PARENT_CHANGED) > 0 && mainSplitPane.getParent() != null
                        && !queryCache.isScriptModified() && queryCache.isAutomaticallyExecuting()
                        && !disableAutoExecute) {
                    execute();
                    mainSplitPane.removeHierarchyListener(this);
                }
            }
        });
    }

    /**
     * This will modify the given tool bar to contain the buttons necessary to
     * use the text editor of the query panel.
     */
    private void changeToTextToolBar() {

        toolBarBuilder.clear();

        JButton executeButton = queryUIComponents.getExecuteButton();
        toolBarBuilder.add(executeButton, "Execute", EXECUTE_ICON);

        queryUIComponents.getStopButton().setAction(new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                if (queryCache.getInternalHandle() != null) {
                    queryCache.getInternalHandle().cancel();
                }
            }
        });
        toolBarBuilder.add(queryUIComponents.getStopButton(), "Stop", STOP_ICON);

        toolBarBuilder.addSeparator();

        JButton clearButton = queryUIComponents.getClearButton();
        toolBarBuilder.add(clearButton, "Clear", RESET_ICON);
        JButton undoButton = queryUIComponents.getUndoButton();
        toolBarBuilder.add(undoButton, "Undo", UNDO_ICON);
        JButton redoButton = queryUIComponents.getRedoButton();
        toolBarBuilder.add(redoButton, "Redo", REDO_ICON);
        toolBarBuilder.addSeparator();

        toolBarBuilder.add(exportAction, "Export", EXPORT_ICON);
        toolBarBuilder.addSeparator();

        toolBarBuilder.add(
                new CreateLayoutFromQueryAction(session.getWorkspace(), queryCache, queryCache.getName()),
                "Create Report");
        toolBarBuilder.add(new NewChartAction(session, queryCache), "Create Chart", CREATE_CHART_ICON);
        toolBarBuilder.add(new ShowQueryPropertiesAction(queryCache, context.getFrame()), "Properties");
    }

    /**
     * This will add all of the necessary actions to the given tool bar. Before
     * any actions are added the tool bar will have all of its current buttons
     * and separators removed.
     * 
     * @param toolbarBuilder
     *            The tool bar to modify. The tool bar will contain all of the
     *            buttons necessary for editing the GUI query editor.
     */
    private void changeToGUIToolBar() {

        toolBarBuilder.clear();

        toolBarBuilder.add(queryPen.getExecuteQueryAction(), "Execute", WabitIcons.RUN_ICON_32);
        toolBarBuilder.addSeparator();

        toolBarBuilder.add(exportAction, "Export");
        toolBarBuilder.addSeparator();

        final Action resetAction = new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                queryCache.reset();
                queryCache.cancel();
            }
        };
        toolBarBuilder.add(resetAction, "Reset", RESET_ICON);
        Action createJoinAction = queryPen.getJoinAction();
        toolBarBuilder.add(createJoinAction, "Join", CREATE_JOIN_ICON);
        toolBarBuilder.addSeparator();
        JPanel zoomSliderContainer = queryPen.getZoomSliderContainer();
        toolBarBuilder.add(zoomSliderContainer);
        toolBarBuilder.addSeparator();

        Action createLayoutAction = new CreateLayoutFromQueryAction(session.getWorkspace(), queryCache,
                queryCache.getName());
        toolBarBuilder.add(createLayoutAction, "Create Report");
        toolBarBuilder.add(new NewChartAction(session, queryCache), "Create Chart", CREATE_CHART_ICON);
        Action showPropertiesAction = new ShowQueryPropertiesAction(queryCache, context.getFrame());
        toolBarBuilder.add(showPropertiesAction, "Properties");
    }

    /**
     * This will add a {@link ComponentCellRenderer} to the table headers
     * to allow grouping when the grouping checkbox is checked. This will
     * need to be called each time the tables are recreated.
     * 
     * @param initialDisplay If true this header will be displayed with default values for the
     * headers. If false it will display the header with only what is defined in the QueryCache.
     */
    private void addGroupingTableHeaders() {
        //XXX The group by and having clauses should be allowed
        // to be shown on both the query pen and text editor tabs
        // however we currently can't update the query cache from 
        // the text side so we won't be able to use these components
        // from the text side and they will cause errors as they won't
        // be able to synchronize with the new queries being run.
        if (queryPenAndTextTabPane.getSelectedIndex() == 1) {
            groupingLabel.setVisible(false);
            havingLabel.setVisible(false);
        }
        if (queryPenAndTextTabPane.getSelectedIndex() == 0) {
            ArrayList<JTable> tables = queryUIComponents.getResultTables();
            for (JTable t : tables) {
                ComponentCellRenderer renderPanel = (ComponentCellRenderer) t.getTableHeader().getDefaultRenderer();
                if (groupingCheckBox.isSelected()) {
                    renderPanel.setGroupingEnabled(true);
                    logger.debug("Grouping Enabled");
                    groupingLabel.setVisible(true);
                    havingLabel.setVisible(true);
                } else {
                    renderPanel.setGroupingEnabled(false);
                    groupingLabel.setVisible(false);
                    havingLabel.setVisible(false);
                }
                for (int i = 0; i < renderPanel.getComboBoxes().size(); i++) {
                    final SQLGroupFunction groupBy = this.queryCache.getSelectedColumns().get(i).getGroupBy();
                    if (!groupBy.equals(SQLGroupFunction.GROUP_BY)) {
                        String groupByAggregate = groupBy.getGroupingName();
                        renderPanel.getComboBoxes().get(i).setSelectedItem(groupByAggregate);
                    }
                }

                for (int i = 0; i < renderPanel.getTextFields().size(); i++) {
                    String havingText = this.queryCache.getSelectedColumns().get(i).getHaving();
                    if (havingText != null) {
                        renderPanel.getTextFields().get(i).setText(havingText);
                    }
                }

                LinkedHashMap<Integer, Integer> columnSortMap = new LinkedHashMap<Integer, Integer>();
                for (Item item : this.queryCache.getOrderByList()) {
                    int columnIndex = this.queryCache.indexOfSelectedItem(item);
                    if (columnIndex < 0) {
                        throw new IllegalStateException(
                                "Cannot find " + item.getName() + " in " + this.queryCache.getName());
                    }
                    OrderByArgument arg = item.getOrderBy();
                    if (arg != null && arg != OrderByArgument.NONE) {
                        if (arg == OrderByArgument.ASC) {
                            columnSortMap.put(columnIndex, TableModelSortDecorator.ASCENDING);
                        } else if (arg == OrderByArgument.DESC) {
                            columnSortMap.put(columnIndex, TableModelSortDecorator.DESCENDING);
                        } else {
                            logger.debug("Order by argument for column " + columnIndex + " is " + arg.toString()
                                    + " but was not set for an unknown reason.");
                        }
                    }
                }
                renderPanel.setSortingStatus(columnSortMap);

                for (int i = 0; i < t.getColumnCount(); i++) {
                    Integer width = this.queryCache.getSelectedColumns().get(i).getColumnWidth();
                    logger.debug("Width in cache for column " + i + " is " + width);
                    if (width != null) {
                        t.getColumnModel().getColumn(i).setPreferredWidth(width);
                    }
                }
            }
        }
    }

    public void execute() {
        if (queryCache.getPromptForCrossJoins() && queryCache.containsCrossJoins()) {
            CrossJoinDialog dialog = new CrossJoinDialog(context.getFrame());
            queryCache.setPromptForCrossJoins(!dialog.getDontAskAgain());
            queryCache.setExecuteQueriesWithCrossJoins(dialog.isContinuingExecution());
            if (!dialog.isContinuingExecution())
                return;
        }
        queryUIComponents.executeQuery(this.queryCache);
    }

    public JComponent getPanel() {
        return mainSplitPane;
    }

    public JSplitPane getFullSplitPane() {
        return mainSplitPane;
    }

    public SQLObjectRoot getRootNode() {
        return rootNode;
    }

    public boolean applyChanges() {
        //Changes are currently always done immediately. If we add a save button this will change.
        cleanup();
        return true;
    }

    public void discardChanges() {
        //Changes are currently always done immediately. If we add a save button this will change.
        cleanup();
    }

    public boolean hasUnsavedChanges() {
        //Changes are currently always done immediately. If we add a save button this will change.
        return false;
    }

    /**
     * Disconnects all listeners in the query cache. Also closes any open database
     * connections and updates prefs.
     */
    private void cleanup() {
        logger.debug("QueryPanel@" + System.identityHashCode(this) + " is cleaning up");
        prefs.putInt(RESULTS_DIVIDER_LOCATON_KEY, mainSplitPane.getDividerLocation());
        queryController.disconnect();
        queryCache.removeResultSetProducerListener(rsProducerListener);
        logger.debug("Removed the query panel change listener on the query cache");
        queryUIComponents.closeConMap();
        queryUIComponents.disconnectListeners();
        try {
            for (int i = rootNode.getChildren().size() - 1; i >= 0; i--) {
                rootNode.removeChild(rootNode.getChildren().get(i));
            }
        } catch (ObjectDependentException e) {
            throw new RuntimeException(e);
        }
        queryPen.cleanup();
        logger.debug("QueryPanel@" + System.identityHashCode(this) + " cleanup done");
    }

    /**
     * Returns the queryUIComponents used in this query panel for testing purposes.
     */
    SQLQueryUIComponents getQueryUIComponents() {
        return queryUIComponents;
    }

    public String getTitle() {
        return "Query Editor - " + queryCache.getName();
    }

    public JComponent getSourceComponent() {
        return dragTreeScrollPane;
    }

    public JToolBar getToolbar() {
        return toolBarBuilder.getToolbar();
    }

    boolean isInTextEditMode() {
        if (this.queryPenPanel == this.queryPenAndTextTabPane.getSelectedComponent()) {
            return false;
        } else if (queryToolPanel == queryPenAndTextTabPane.getSelectedComponent()) {
            return true;
        } else {
            throw new AssertionError();
        }
    }
}