/* Copyright (C) 2003 EBI, GRL This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package org.ensembl.mart.explorer; import java.awt.BorderLayout; import java.awt.Cursor; import java.awt.Dimension; import java.awt.event.ActionListener; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; import java.io.FileReader; import java.io.IOException; import java.io.OutputStream; import java.net.URL; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Vector; import java.util.logging.Level; import java.util.logging.Logger; import javax.sql.DataSource; import javax.swing.Box; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JEditorPane; import javax.swing.JFileChooser; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JSplitPane; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import org.ensembl.mart.guiutils.PreviewPaneOutputStream; import org.ensembl.mart.guiutils.QuickFrame; import org.ensembl.mart.lib.Attribute; import org.ensembl.mart.lib.DetailedDataSource; import org.ensembl.mart.lib.Engine; import org.ensembl.mart.lib.InvalidQueryException; import org.ensembl.mart.lib.Query; import org.ensembl.mart.lib.QueryAdaptor; import org.ensembl.mart.lib.SequenceDescription; import org.ensembl.mart.lib.config.CompositeDSConfigAdaptor; import org.ensembl.mart.lib.config.ConfigurationException; import org.ensembl.mart.lib.config.DSConfigAdaptor; import org.ensembl.mart.lib.config.DatasetConfig; import org.ensembl.mart.lib.config.Option; import org.ensembl.mart.lib.config.URLDSConfigAdaptor; import org.ensembl.mart.shell.MartShellLib; import org.ensembl.mart.util.LoggingUtil; /** * Widget for creating, loading, saving and editing Queries. * * @author <a href="mailto:craig@ebi.ac.uk">Craig Melsopp</a> */ public class QueryEditor extends JPanel { /** run engine.execute(...) */ private static final int EXECUTE = 0; /** run engine.countFocus(...) */ private static final int COUNT_FOCUS = 2; private int preconfigLimit = 1000; private int maxPreconfigBytes = 100000; private AdaptorManager adaptorManager; private QueryEditorContext editorManager; private OutputStream os = null; private boolean running = false; private List listeners = new ArrayList(); private ChangeEvent changeEvent = new ChangeEvent(this); private static final Logger logger = Logger.getLogger(QueryEditor.class.getName()); /** default percentage of total width allocated to the tree constituent component. */ private double TREE_WIDTH = 0.27d; /** default percentage of total height allocated to the tree constituent component. */ private double TREE_HEIGHT = 0.7d; private Dimension MINIMUM_SIZE = new Dimension(50, 50); /** The query part of the model. */ private Query query; private Engine engine = new Engine(); private JFileChooser mqlFileChooser = new JFileChooser(); private AdaptorManager datasetPage; private String currentDatasetName; private OutputSettingsPage outputSettingsPage; private AttributePageSetWidget attributesPage; private FilterPageSetWidget filtersPage; private Option lastDatasetOption; private Feedback feedback = new Feedback(this); private JFileChooser resultsFileChooser = new JFileChooser(); private File currentDirectory; /** File for temporarily storing results in while this instance exists. */ // private File tmpFile; private JSplitPane leftAndRight; private JSplitPane middleAndBottom; private JEditorPane outputPanel; private InputPageContainer inputPanelContainer; /** * * @throws IOException if fails to create temporary results file. */ public QueryEditor( QueryEditorContext editorManager, AdaptorManager datasetConfigSettings) throws IOException { this.adaptorManager = datasetConfigSettings; this.editorManager = editorManager; this.query = new Query(); // notify any state listeners when these // aspects of the query are changed. this.query.addQueryChangeListener(new QueryAdaptor() { public void attributeAdded( Query sourceQuery, int index, Attribute attribute) { notifyAllListeners(); } public void sequenceDescriptionChanged( Query sourceQuery, SequenceDescription seq, SequenceDescription mseq){ notifyAllListeners(); } public void attributeRemoved( Query sourceQuery, int index, Attribute attribute) { notifyAllListeners(); } public void datasetChanged( Query source, String oldDataset, String newDataset) { notifyAllListeners(); } public void datasourceChanged( Query sourceQuery, DataSource oldDatasource, DataSource newDatasource) { notifyAllListeners(); } }); QueryTreeView treeConfig = new QueryTreeView(query, datasetConfigSettings.getRootAdaptor()); inputPanelContainer = new InputPageContainer(query, treeConfig, datasetConfigSettings); outputPanel = new JEditorPane(); outputPanel.setEditable(false); addWidgets( new JScrollPane(treeConfig), inputPanelContainer, new JScrollPane(outputPanel)); mqlFileChooser.addChoosableFileFilter( new org.ensembl.gui.ExtensionFileFilter("mql", "MQL Files")); // set default working directory setCurrentDirectory(new File(System.getProperty("user.home"))); } /** * */ protected void doClose() { if (editorManager != null) editorManager.remove(this); } /** * */ private void doDatasetConfigChanged() { // TODO Auto-generated method stub } /** * */ public void doLoadQuery() { if (getMqlFileChooser().showOpenDialog(this) != JFileChooser.APPROVE_OPTION) return; logger.fine("Previous query: " + query); try { File f = getMqlFileChooser().getSelectedFile().getAbsoluteFile(); logger.fine("Loading MQL from file: " + f); BufferedReader r = new BufferedReader(new FileReader(f)); StringBuffer buf = new StringBuffer(); for (String line = r.readLine(); line != null; line = r.readLine()) buf.append(line); r.close(); logger.fine("Loaded MQL: " + buf.toString()); MartShellLib msl = new MartShellLib(adaptorManager.getRootAdaptor()); setQuery(msl.MQLtoQuery(buf.toString())); logger.fine("Loaded Query:" + getQuery()); } catch (InvalidQueryException e) { feedback.warning(e.getMessage()); } catch (IOException e) { feedback.warning(e.getMessage()); } } /** * Save results to file, user must select file if no output file selected. */ public void doSaveResults() { runQuery(EXECUTE, true, false, 0); } /** * Save results to file, user must select output file. */ public void doSaveResultsAs() { runQuery(EXECUTE, true, true, 0); } /** * convenience method. * @param label * @param listener * @return button configured with the parameters */ private JButton createButton( String label, ActionListener listener, boolean enabled) { JButton b = new JButton(label); b.setEnabled(enabled); b.addActionListener(listener); return b; } /** * Repositions the dividers after the component has been resized to maintain * the relative size of the panes. */ private void resizeSplits() { // must set divider by explicit values rather than // proportions because the proportion approach fails // on winxp jre 1.4 when the component is FIRST added. // (It does work when the component is resized). Dimension size = getParent().getSize(); int treeWidth = (int) (TREE_WIDTH * size.width); int treeHeight = (int) ((1 - TREE_WIDTH) * size.height); leftAndRight.setDividerLocation(treeWidth); middleAndBottom.setDividerLocation(treeHeight); // need to do this so the component is redrawn on win xp jre 1.4 validate(); } /** * Sets the relative positions of the constituent components with splitters * where needed. Layout is: * <pre> * ----------------- * left | right * ----------------- * bottom * </pre> */ private void addWidgets( JComponent left, JComponent right, JComponent bottom) { left.setMinimumSize(MINIMUM_SIZE); right.setMinimumSize(MINIMUM_SIZE); bottom.setMinimumSize(MINIMUM_SIZE); leftAndRight = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, left, right); leftAndRight.setOneTouchExpandable(true); middleAndBottom = new JSplitPane(JSplitPane.VERTICAL_SPLIT, leftAndRight, bottom); middleAndBottom.setOneTouchExpandable(true); // don't use default FlowLayout manager because it won't resize components if // QueryEditor is resized. setLayout(new BorderLayout()); addComponentListener(new ComponentAdapter() { public void componentResized(ComponentEvent e) { resizeSplits(); } }); add(middleAndBottom, BorderLayout.CENTER); } /** * Loads dataset configs from files in classpath for test * purposes. * @return preloaded dataset configs * @throws ConfigurationException */ static DSConfigAdaptor testDSConfigAdaptor(DSConfigAdaptor adaptor) throws ConfigurationException { //TODO: change to defaultMartRegistry.xml //CompositeDSConfigAdaptor adaptor = new CompositeDSConfigAdaptor(); String[] urls = new String[] { //"data/XML/hsapiens_gene_est.xml" "data/XML/hsapiens_gene_ensembl.xml" //,"data/XML/hsapiens_gene_vega.xml" }; for (int i = 0; i < urls.length; i++) { URL dvURL = QueryEditor.class.getClassLoader().getResource(urls[i]); //dont ignore cache, dont include hidden members (these are only for MartEditor) ((CompositeDSConfigAdaptor) adaptor).add( new URLDSConfigAdaptor(dvURL, false, false)); } return adaptor; } /** * @return list of datasources */ /** static List testDatasources() throws ConfigurationException { Vector dss = new Vector(); dss.add( new DetailedDataSource( "mysql", "ensembldb.ensembl.org", "3306", "ensembl_mart_17_1", "ensembl_mart_17_1", "anonymous", null, 10, "com.mysql.jdbc.Driver")); dss.add( new DetailedDataSource( "mysql", "ensembldb.ensembl.org", "3306", "ensembl_mart_18_1", "anonymous", null, 10, "com.mysql.jdbc.Driver")); return dss; } */ public static void main(String[] args) throws Exception { // enable logging messages LoggingUtil.setAllRootHandlerLevelsToFinest(); logger.setLevel(Level.FINEST); Logger.getLogger(Query.class.getName()).setLevel(Level.FINEST); AdaptorManager dvs = testDatasetConfigSettings(); final QueryEditor editor = new QueryEditor(null, dvs); editor.setName("test_query"); editor.setPreferredSize(new Dimension(1024, 768)); Box p = Box.createVerticalBox(); p.add(editor); new QuickFrame("Query Editor (Test Frame)", p); // set 1st dsv to save having to do it while testing. editor.getQuery().setDatasetConfig( (DatasetConfig) dvs.getRootAdaptor().getDatasetConfigs().next()); } /** * @return query associated with this editor. */ public Query getQuery() { return query; } /** * Executes query and writes results to preconfig panel, limits number * of result rows printed to preconfigLimit. * */ public void doPreview() { runQuery(EXECUTE, false, false, preconfigLimit); } /** * Executes query and writes results to temporary file. * */ public void doExecute() { runQuery(EXECUTE, false, false, 0); } /** * Counts the focus objects that would be returned if the * query were executed and prints the value in the * preview window. * */ public void doCountFocus() { runQuery(COUNT_FOCUS, false, false, 0); } /** * Stops running query. Does nothing if query not running. * */ public void doStop() { // Stop the query by closing the output stream. This is a little hacky // but is the only way to // stop the execution (other than Thread.stop() ) becuase the engine does not // have any "stop" hooks. if (os != null) { try { os.close(); } catch (IOException e) { e.printStackTrace(); } os = null; } outputPanel.setText(""); } /** * Executes the query on a separate thread and * stores the results in a file if necesary. * * @param method method with which to run the query, EXECUTE, * COUNT_ROWS or COUNT_FOCUS * @param save whether results file should be saved * @param changeResultsFile if true the results file chooser is displayed * @param limit max number of rows in result set * @see #EXECUTE * @see #COUNT_ROWS * @see #COUNT_FOCUS */ private void runQuery( final int method, final boolean save, final boolean changeResultsFile, final int limit) { if (os != null) { feedback.info("Query is already running."); return; } // user select result file if necessary if (save && resultsFileChooser.getSelectedFile() == null || changeResultsFile) { if (resultsFileChooser.getSelectedFile() == null) resultsFileChooser.setSelectedFile(new File(getName() + ".mart")); int option = resultsFileChooser.showSaveDialog(this); if (option != JFileChooser.APPROVE_OPTION) return; } // clear last results set before executing query outputPanel.setText(""); new Thread() { public void run() { execute(method, save, limit); } } .start(); } /** * Runs the query using the specified method, writes * results to the preview panel and saves the results * to file if required. * * @param method method with which to run the query, EXECUTE, * COUNT_ROWS or COUNT_FOCUS * @param save whether results file should be saved * @param limit max number of rows in result set * @see #EXECUTE * @see #COUNT_ROWS * @see #COUNT_FOCUS */ private synchronized void execute( final int method, final boolean save, final int limit) { if (query.getDataSource() == null) { feedback.warning("Data base must be set before executing query."); return; } else if ( query.getAttributes().length == 0 && query.getSequenceDescription() == null) { feedback.warning("Attributes must be set before executing query."); return; } try { File outFile = null; if (save) { outFile = resultsFileChooser.getSelectedFile(); os = new FileOutputStream(outFile); } else os = new PreviewPaneOutputStream(null, outputPanel, maxPreconfigBytes); int oldLimit = query.getLimit(); if (limit > 0) query.setLimit(limit); setRunning(true); switch (method) { case EXECUTE : engine.execute( query, inputPanelContainer.getOutputSettingsPage().getFormat(), os); break; case COUNT_FOCUS : engine.countFocus(os, query); break; default : throw new RuntimeException("Unsupported method: " + method); } os.close(); os = null; if (save && outFile != null && outFile.toURL().openConnection().getContentLength() < 1) feedback.warning("Empty result set."); if (limit > 0) query.setLimit(oldLimit); } catch (Exception e) { // if the os is null then it must have been set by doCancel() e.printStackTrace(); if (os != null) { try { os.close(); } catch (IOException e1) { //ignore, it gets nulled out next } os = null; feedback.warning(e); } } finally { setRunning(false); } } /** * Set the name for this widget and the query it contains. */ public void setName(String name) { super.setName(name); query.setQueryName(name); } /* * @return the name of this widget, this is derived from it's query.name. */ public String getName() { return query.getQueryName(); } /** * @return mql representation of the current query, * or null if datasetConfig unset. */ public String getQueryAsMQL() throws InvalidQueryException { String mql = null; DatasetConfig datasetConfig = query.getDatasetConfig(); if (datasetConfig == null) throw new InvalidQueryException("DatasetConfig must be selected before query can be converted to MQL."); MartShellLib msl = new MartShellLib(null); mql = msl.QueryToMQL(query, datasetConfig); return mql; } /** * Exports mql to a file chosen by user. */ public void doSaveQuery() { try { String mql = getQueryAsMQL(); if (getMqlFileChooser().showSaveDialog(this) != JFileChooser.APPROVE_OPTION) return; File f = getMqlFileChooser().getSelectedFile().getAbsoluteFile(); FileOutputStream os; os = new FileOutputStream(f); os.write(mql.getBytes()); os.write('\n'); os.close(); } catch (InvalidQueryException e) { feedback.warning(e.getMessage()); } catch (IOException e) { feedback.warning(e.getMessage()); } } /** * Initialises "current dir" of chooser if no file currently set. */ private JFileChooser getMqlFileChooser() { // Do this here rather than (more simply) in constructor because // that cause an excption to be thrown on linux. Possibly a bug in JVM/library. if (mqlFileChooser.getSelectedFile() == null) mqlFileChooser.setCurrentDirectory(currentDirectory); return mqlFileChooser; } /** * Initialise this editor with the contents of the specified query. * @param query query settings to be used by editor. */ public void setQuery(Query query) { this.query.initialise(query); } /** * @return current directory. */ public File getCurrentDirectory() { return currentDirectory; } /** * @param directory * @throws IllegalArgumentException if directory not exist or is not a real direcory */ public void setCurrentDirectory(File directory) { if (!directory.exists()) throw new IllegalArgumentException("Directory not exist: " + directory); if (!directory.isDirectory()) throw new IllegalArgumentException( "File is not a directory: " + directory); currentDirectory = directory; } /** * */ public static AdaptorManager testDatasetConfigSettings() { AdaptorManager dvs = new AdaptorManager(false); dvs.setAdvancedOptionsEnabled(true); try { testDSConfigAdaptor(dvs.getRootAdaptor()); } catch (ConfigurationException e) { e.printStackTrace(); } return dvs; } /** * When doPreconfig() is called a limit is set on the query with this value. * @return max number of rows in preconfig results. */ public int getPreconfigLimit() { return preconfigLimit; } /** * Set the maximum number of rows to be displayed during a doPreconfig() cal. * @return max number of rows to include in preconfig results pane. */ public void setPreconfigLimit(int i) { preconfigLimit = i; } /** * */ public void openDatasetConfigMenu() { inputPanelContainer.openDatasetConfigMenu(); } /** * Set the cursor to busy for this component and notify * listeners that the QueryEdotor is running a query or has * finished running one. * @param running whether the query is running or not */ private void setRunning(boolean running) { this.running = running; if (running) setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); else setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); notifyAllListeners(); } public boolean isRunning() { return running; } public void addChangeListener(ChangeListener listener) { listeners.add(listener); } public void removeChangeListener(ChangeListener listener) { listeners.remove(listener); } /** * Notify all listeners of a state change. */ private void notifyAllListeners() { for (Iterator iter = listeners.iterator(); iter.hasNext();) ((ChangeListener) iter.next()).stateChanged(changeEvent); } }