/* 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 */ //TODO: Either needs to have a DatasetConfig associated with it, or a dataset and internalName, before being sent to QueryToMQL package org.ensembl.mart.lib; import java.util.ArrayList; import java.util.List; import java.util.Vector; import java.util.logging.Level; import java.util.logging.Logger; import org.ensembl.mart.lib.config.DatasetConfig; import org.ensembl.util.StringUtil; /** * A mart query object. Instances of this class specify all of the parameters * necessary for execution against a mart instance. Also any * QueryChangeListeners, added with <code>addQueryChangeListener(listener)</code>, * are notified of changes in the query's state such as the addition of an attribute or remval of * a filter. * * <p>If the log level is set to >= FINE then the query is written to the log * after it's state is changed but before calling the listeners.</p> * * * TODO addXXX(int, o) -> add(o,int) * * @author <a href="mailto:craig@ebi.ac.uk">Craig Melsopp</a> * @author <a href="mailto:dlondon@ebi.ac.uk">Darin London</a> * @see Attribute * @see SequenceDescription * @see Filter * @see QueryListener * */ public class Query { private final static Logger logger = Logger.getLogger(Query.class.getName()); private List listeners = new ArrayList(); /** * enums over query types * clients can set type using the constant * and test / get results as well */ public final static int ATTRIBUTE = 1; public final static int SEQUENCE = 2; public Query() { } public Query(Query oq) { initialise(oq); } /** * Inititise this query by removing all current properties * and copying all the properties from oq. * * @param oq * @throws InvalidQueryException */ public synchronized void initialise(Query oq) { setDataset(oq.getDataset()); setDataSource(oq.getDataSource()); try { removeAllAttributes(); } catch (InvalidQueryException e3) { // ignore, oq would have thrown if there was a problem } if (oq.getAttributes().length > 0) { Attribute[] oatts = oq.getAttributes(); // TODO copy attribute and filter by value rather than reference for (int i = 0; i < oatts.length; ++i) addAttribute(oatts[i]); } removeAllFilters(); if (oq.getFilters().length > 0) { Filter[] ofilts = oq.getFilters(); // TODO copy attribute and filter by value rather than reference for (int i = 0; i < ofilts.length; ++i) addFilter(ofilts[i]); } if (oq.getMainTables().length > 0) { String[] oStars = oq.getMainTables(); String[] nStars = new String[oStars.length]; System.arraycopy(oStars, 0, nStars, 0, oStars.length); setMainTables(nStars); } else { setMainTables(null); } if (oq.getPrimaryKeys().length > 0) { String[] oPkeys = oq.getPrimaryKeys(); String[] nPkeys = new String[oPkeys.length]; System.arraycopy(oPkeys, 0, nPkeys, 0, oPkeys.length); setPrimaryKeys(nPkeys); } else { setPrimaryKeys(null); } limit = oq.getLimit(); if (oq.hasSort()) { Attribute[] sortAtts = oq.getSortByAttributes(); for (int i = 0, n = sortAtts.length; i < n; i++) { addSortByAttribute(sortAtts[i]); } } // TODO copy other querytypes? if (oq.querytype == Query.SEQUENCE) setSequenceDescription( new SequenceDescription(oq.getSequenceDescription())); } /** * returns the query type (one of ATTRIBUTE or SEQUENCE) * @return int querytype */ public int getType() { return querytype; } /** * Determines if the specified attribute object is * contained within the attribute array of the Query. * * @param attribute attribute to look for * @return boolean true if attribute is present, otherwise false. */ public boolean hasAttribute(Attribute attribute) { return attributes.contains(attribute); } /** * Determines if the specified filter object is * contained within the filter array of the Query. * * @param filter filter to look for * @return boolean true if filter is present, otherwise false. */ public boolean hasFilter(Attribute filter) { return filters.contains(filter); } /** * Adds attribute to the end of the attributes array. * @param attribute item to be added. * @throws InvalidQueryException * @throws IllegalArgumentException if attribute is * null or already added. */ public synchronized void addAttribute(Attribute attribute) { addAttribute(attributes.size(), attribute); } /** * remove a Filter object from the list of Attributes * * @param Filter filter * @throws InvalidQueryException */ public synchronized void removeFilter(Filter filter) { int index = filters.indexOf(filter); if (index > -1) { filters.remove(index); log(); for (int i = 0; i < listeners.size(); ++i) ((QueryListener) listeners.get(i)).filterRemoved(this, index, filter); } } /** * get all Attributes as an Attribute :ist * * @return Attribute[] attributes */ public Attribute[] getAttributes() { Attribute[] a = new Attribute[attributes.size()]; attributes.toArray(a); return a; } /** * get all Filter objects as a Filter[] Array * * @return Filters[] filters */ public Filter[] getFilters() { Filter[] f = new Filter[filters.size()]; filters.toArray(f); return f; } /** * Allows the retrieval of a specific Filter object with a specified field name. * * @param name - name of the fieldname for this Filter. * @return Filter object named by given field name. */ public Filter getFilterByName(String name) { for (int i = 0, n = filters.size(); i < n; i++) { Filter element = (Filter) filters.get(i); if (element.getField().equals(name)) return element; } return null; } /** * Add filter to end of filter list. * * @param Filter filter to be added. * @throws InvalidQueryException * @throws IllegalArgumentException if filter is * null or already added. */ public synchronized void addFilter(Filter filter) { addFilter(filters.size(), filter); } /** * Add filter to the filters array at the specified index. * * @param index position where to insert the filter in the filters array. * @param filter filter to be added * @throws IllegalArgumentException if attribute is * null or already added. */ public synchronized void addFilter(int index, Filter filter) { if (filter == null) throw new IllegalArgumentException("Can not add a null filter"); if (filters.contains(filter)) throw new IllegalArgumentException("Filter already present: " + filter); filters.add(index, filter); log(); for (int i = 0; i < listeners.size(); ++i) ((QueryListener) listeners.get(i)).filterAdded(this, index, filter); } /** * Remove Attribute if present, otherwise do nothing. * * @param Attribute attribute to be removed. * @throws InvalidQueryException */ public synchronized void removeAttribute(Attribute attribute) { int index = attributes.indexOf(attribute); if (index > -1) { attributes.remove(index); log(); for (int i = 0; i < listeners.size(); ++i) ((QueryListener) listeners.get(i)).attributeRemoved( this, index, attribute); } } /** * Sets a SequenceDescription to the Query, and sets querytype = SEQUENCE. * @param s A SequenceDescription object. * @throws InvalidQueryException */ public synchronized void setSequenceDescription(SequenceDescription s) { SequenceDescription oldSequenceDescription = this.sequenceDescription; this.sequenceDescription = s; if (s==null) this.querytype = Query.ATTRIBUTE; else this.querytype = Query.SEQUENCE; log(); for (int i = 0; i < listeners.size(); ++i) ((QueryListener) listeners.get(i)).sequenceDescriptionChanged( this, oldSequenceDescription, this.sequenceDescription); } private void migrateAttributes() throws InvalidQueryException { Attribute[] oAttributes = getAttributes(); removeAllAttributes(); for (int i = 0, n = oAttributes.length; i < n; i++) { if (attributes.size() == 0) { //must get the attribute, so that the sequenceDescription initializes properly, //but must then add the exportables before this (first) attribute Attribute sa = sequenceDescription.getAttribute(oAttributes[i]); Attribute[] eAtts = sequenceDescription.getFinalLink(); if (eAtts == null) throw new InvalidQueryException("Sequence type " + sequenceDescription.getSeqType() + " is not supported\n"); for (int j = 0, m = eAtts.length; j < m; j++) { addAttribute(eAtts[j]); } addAttribute(sa); } else addAttribute( sequenceDescription.getAttribute(oAttributes[i]) ); } } private void migrateFilters() throws InvalidQueryException { Filter[] oFilters = getFilters(); // if there are no filters added, this will just be a filterless subquery if (oFilters.length > 0) { removeAllFilters(); IDListFilter lastFilter = null; for (int i = 0, n = oFilters.length; i < n; i++) { lastFilter = sequenceDescription.getFilter(oFilters[i]); //add it back if this is a non subquery based sequence query if (lastFilter == null) addFilter(oFilters[i]); } if (lastFilter != null) addFilter(lastFilter); } } public void initializeForSequence() throws InvalidQueryException { sequenceDescription.setSubQuery(this); //a valid sequence query must have one attribute //in order to function properly. Throw if not. if (attributes.size() < 1) throw new InvalidQueryException("Sequence Queries must contain at least one header attribute\n"); migrateAttributes(); migrateFilters(); if (sequenceDescription.getFinalDatasetName() != null) { setDataset(sequenceDescription.getFinalDatasetName()); setDatasetConfig(sequenceDescription.getFinalDataset()); setDataSource(sequenceDescription.getFinalDataSource()); setMainTables(sequenceDescription.getStructureMainTables()); setPrimaryKeys(sequenceDescription.getStructurePrimaryKeys()); //this query is now a structure query with a core subQuery, not the original core query } } /** * returns the SequenceDescription for this Query. * @return SequenceDescription */ public SequenceDescription getSequenceDescription() { return sequenceDescription; } /** * get the primaryKeys of the Query * @return String primaryKeys */ public String[] getPrimaryKeys() { return primaryKeys; } /** * set the primaryKeys for the Query * @param String primaryKeys */ public synchronized void setPrimaryKeys(String[] primaryKeys) { String[] old = this.primaryKeys; this.primaryKeys = primaryKeys; log(); for (int i = 0; i < listeners.size(); ++i) ((QueryListener) listeners.get(i)).primaryKeysChanged( this, old, this.primaryKeys); } /** * get the starBases for the Query * @return String starBases */ public String[] getMainTables() { return starBases; } /** * set the starBases for the Query * @param String starBases */ public synchronized void setMainTables(String[] starBases) { String[] old = this.starBases; this.starBases = starBases; log(); for (int i = 0; i < listeners.size(); ++i) ((QueryListener) listeners.get(i)).starBasesChanged( this, old, this.starBases); } /** * Set a limit for the Query. * @param inlimit - int limit to add to the Query, should be >=0 where * 0 mean no limit */ public synchronized void setLimit(int inlimit) { //assert inlimit > -1 : "invalid limit should be >=0 but is " + inlimit; int old = this.limit; this.limit = inlimit; log(); for (int i = 0; i < listeners.size(); ++i) ((QueryListener) listeners.get(i)).limitChanged(this, old, this.limit); } /** * Determine if the Query has a limit > 0. * @return true if limit > 0, false if not */ public boolean hasLimit() { return (limit > 0); } /** * Returns the limit for the Query. limit == 0 means no limit * @return limit */ public int getLimit() { return limit; } /** * Returns the total number of filters of all types added to the Query * @return int count of all filters added */ public int getTotalFilterCount() { return filters.size(); } /** * returns a description of the Query for logging purposes * * @return String description (primaryKeys=primaryKeys\nstarBases=starBases\nattributes=attributes\nfilters=filters) */ public String toString() { StringBuffer buf = new StringBuffer(); buf.append("["); buf.append(" datasetConfig=").append( datasetConfig==null ? "unset" : datasetConfig.getInternalName() ); buf.append(" dataset=").append(dataset); buf.append(", dataSource=").append(dataSource); buf.append(", starBases=[").append(StringUtil.toString(starBases)); buf.append("], primaryKeys=[").append(StringUtil.toString(primaryKeys)); buf.append("], querytype=").append(querytype); buf.append(", attributes=").append(attributes); buf.append(", filters=").append(filters); if (sequenceDescription != null) buf.append(", sequencedescription=").append(sequenceDescription); if (hasSort) { buf.append(", sortBy=").append(sortAttributes); } buf.append(", limit=").append(limit); buf.append("]"); return buf.toString(); } /** * Allows Equality Comparisons manipulation of Query objects * Mainly for testing of copy constructor */ public boolean equals(Object o) { return o instanceof Query && hashCode() == ((Query) o).hashCode(); } public int hashCode() { int tmp = 0; if (querytype == Query.SEQUENCE) { tmp = (31 * tmp) + sequenceDescription.hashCode(); tmp = (31 * tmp) + querytype; } for (int i = 0, n = starBases.length; i < n; i++) tmp = (31 * tmp) + starBases[i].hashCode(); for (int i = 0, n = primaryKeys.length; i < n; i++) tmp = (31 * tmp) + primaryKeys[i].hashCode(); for (int i = 0, n = attributes.size(); i < n; i++) { FieldAttribute element = (FieldAttribute) attributes.get(i); tmp = (31 * tmp) + element.hashCode(); } for (int i = 0, n = filters.size(); i < n; i++) tmp = (31 * tmp) + filters.get(i).hashCode(); tmp *= 31; if (datasetConfig != null) tmp += datasetConfig.hashCode(); tmp *= 31; if (queryName != null) tmp += queryName.hashCode(); return tmp; } private List attributes = new Vector(); private List filters = new Vector(); private String queryName; private int querytype = Query.ATTRIBUTE; // default to ATTRIBUTE, over ride for SEQUENCE private SequenceDescription sequenceDescription; private String[] primaryKeys; private String[] starBases; private DatasetConfig datasetConfig; private int limit = 0; // add a limit clause to the SQL with an int > 0 /** * Datasource this query applies to. */ private DetailedDataSource dataSource; /** * Name of dataset this query applies to. */ private String dataset; public QueryListener[] getQueryChangeListeners() { return (QueryListener[]) listeners.toArray( new QueryListener[listeners.size()]); } public synchronized void removeQueryChangeListener(QueryListener listener) { listeners.remove(listener); } /** * Convenience method that removes all attributes from the query. Notifies listeners. * @throws InvalidQueryException */ public synchronized void removeAllAttributes() throws InvalidQueryException { Attribute[] attributes = getAttributes(); for (int i = 0, n = attributes.length; i < n; i++) { removeAttribute(attributes[i]); } } /** * Removes all Filters from the query. Each removed Filter will * generate a separate property change event. * @throws InvalidQueryException */ public synchronized void removeAllFilters() { Filter[] filters = getFilters(); for (int i = 0, n = filters.length; i < n; i++) { removeFilter(filters[i]); } } /** * Removes all QueryChangeListeners. Note: does not notify * listeners that they have been removed. */ public synchronized void removeAllQueryChangeListeners() { listeners.clear(); } /** * Replace the oldFilter with the new one. * @param oldFilter * @param newFilter * @throws RuntimeException if oldFilter is not currently in the query. * TODO remove replaceFilter() + listener method. */ public synchronized void replaceFilter(Filter oldFilter, Filter newFilter) { int index = filters.indexOf(oldFilter); if (index == -1) throw new IllegalArgumentException( "Old filter can not be removed because not in query: " + oldFilter); filters.remove(index); filters.add(index, newFilter); log(); for (int i = 0; i < listeners.size(); ++i) ((QueryListener) listeners.get(i)).filterChanged( this, index, oldFilter, newFilter); } public DetailedDataSource getDataSource() { return dataSource; } /** * Sets the value and notifies listeners. * @param dataSource new dataSource. */ public synchronized void setDataSource(DetailedDataSource dataSource) { DetailedDataSource oldDatasource = this.dataSource; this.dataSource = dataSource; log(); for (int i = 0; i < listeners.size(); ++i) ((QueryListener) listeners.get(i)).datasourceChanged( this, oldDatasource, this.dataSource); } public String getDataset() { return dataset; } /** * Sets the value and propagates a PropertyChange event to listeners. The * property name is in the event is "dataset". No event is propagated * if the parameter is equal to the current dataset. * @param dataset new dataset. */ public synchronized void setDataset(String datasetName) { if (this.dataset == datasetName || datasetName != null && datasetName.equals(this.dataset)) return; String oldDatasetName = this.dataset; this.dataset = datasetName; log(); for (int i = 0; i < listeners.size(); ++i) ((QueryListener) listeners.get(i)).datasetChanged( this, oldDatasetName, this.dataset); } /** * Returns the name that has been set for this Query, null if not set * @return String Query name */ public String getQueryName() { return queryName; } /** * @param string -- String name to apply to this Query. */ public synchronized void setQueryName(String queryName) { if (this.queryName != null && this.queryName.equals(queryName)) return; String old = this.queryName; this.queryName = queryName; log(); for (int i = 0; i < listeners.size(); ++i) ((QueryListener) listeners.get(i)).queryNameChanged( this, old, this.queryName); } /** * @param listener */ public synchronized void addQueryChangeListener(QueryListener listener) { listeners.add(listener); } /** * Adds attribute at the specified index. * @param index position in array to add attribute. * @param attribute item to be added to attributes array. * @throws IllegalArgumentException if attribute is * null or already added. */ public synchronized void addAttribute(int index, Attribute attribute) { if (attribute == null) throw new IllegalArgumentException("Can not add a null attribute"); // if (attributes.contains(attribute)) // throw new IllegalArgumentException( // "attribute already present: " + attribute); attributes.add(index, attribute); log(); for (int i = 0; i < listeners.size(); ++i) ((QueryListener) listeners.get(i)).attributeAdded(this, index, attribute); } /** * Cause the result of toString() to be printed to the log if * logging level for this class is >= FINE. Useful for debugging * and test purposes. Called automatically by all the state changing * methods such as addFilter(...) and setDataset(...). */ public void log() { if (logger.isLoggable(Level.FINE)) logger.fine(this.toString()); } /** * Unsets all property values. */ public synchronized void clear() { setDataSource(null); setDataset(null); setDatasetConfig(null); try { removeAllAttributes(); removeAllFilters(); } catch (InvalidQueryException e) { // ignore } setLimit(0); setPrimaryKeys(null); setMainTables(null); } public DatasetConfig getDatasetConfig() { return datasetConfig; } public synchronized void setDatasetConfig(DatasetConfig datasetConfig) { DatasetConfig old = this.datasetConfig; this.datasetConfig = datasetConfig; log(); for (int i = 0; i < listeners.size(); ++i) ((QueryListener) listeners.get(i)).datasetConfigChanged( this, old, datasetConfig); } //special sortby feature, only an advanced MartShell feature, so no need to link with the QueryListener private boolean hasSort = false; private List sortAttributes = null; public synchronized void addSortByAttribute(Attribute sortAtt) { if (!hasSort) { hasSort = true; sortAttributes = new ArrayList(); } sortAttributes.add(sortAtt); } public synchronized void removeSortBy() { hasSort = false; sortAttributes = null; } public boolean hasSort() { return hasSort; } public Attribute[] getSortByAttributes() { if (!hasSort) return null; Attribute[] ret = new Attribute[sortAttributes.size()]; sortAttributes.toArray(ret); return ret; } }