/* * Geotoolkit - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2009-2010, Geomatys * * 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; * version 2.1 of the License. * * 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. */ package org.geotoolkit.data.query; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import org.geotoolkit.data.DefaultJoinFeatureCollection; import org.geotoolkit.data.DefaultSelectorFeatureCollection; import org.geotoolkit.data.DefaultTextStmtFeatureCollection; import org.geotoolkit.data.FeatureCollection; import org.geotoolkit.data.DefaultFeatureStoreJoinFeatureCollection; import org.geotoolkit.data.session.Session; import org.geotoolkit.factory.FactoryFinder; import org.apache.sis.util.NullArgumentException; import org.apache.sis.internal.util.UnmodifiableArrayList; import org.apache.sis.storage.DataStoreException; import org.opengis.util.GenericName; import org.opengis.filter.Filter; import org.opengis.filter.FilterFactory; import org.opengis.filter.sort.SortBy; /** * * @author Johann Sorel (Geomatys) * @module */ public class QueryUtilities { private static final FilterFactory FF = FactoryFinder.getFilterFactory(null); private QueryUtilities(){} /** * A source is considered absolute when all selector in the source have * a session defined. That implies we can use a query with this source * directly on a EvaluatedFeatureCollection. * * @param source * @return true if the source is absolute */ public static boolean isAbsolute(final Source source){ if(source instanceof Join){ final Join j = (Join) source; return isAbsolute(j.getLeft()) && isAbsolute(j.getRight()); }else if (source instanceof Selector){ return ((Selector)source).getSession() != null; }else if (source instanceof TextStatement){ return ((TextStatement)source).getSession() != null; }else{ throw new IllegalStateException("Source type is unknowned : " + source + "\n valid types ares Join and Selector"); } } /** * When a source is not yet absolute, you can reconfigure it to be so. * every Selector that doesn't have a session configure will be replaced by * the given one. * * @param source * @param session * @return an absolute source */ public static Source makeAbsolute(final Source source, final Session session){ final Source absolute; if(source instanceof Join){ final Join j = (Join) source; if(isAbsolute(j)){ absolute = j; }else{ final Source left = makeAbsolute(j.getLeft(), session); final Source right = makeAbsolute(j.getLeft(), session); absolute = new DefaultJoin(left, right, j.getJoinType(), j.getJoinCondition()); } }else if (source instanceof Selector){ final Selector select = (Selector) source; if (select.getSession() == null){ if(session == null){ throw new NullPointerException("Session can not be null."); } absolute = new DefaultSelector(session, select.getFeatureTypeName(), select.getSelectorName()); }else{ absolute = source; } }else if (source instanceof TextStatement){ final TextStatement select = (TextStatement) source; if (select.getSession() == null){ if(session == null){ throw new NullPointerException("Session can not be null."); } absolute = new DefaultTextStatement(select.getStatement(), session, select.getName()); }else{ absolute = source; } }else{ throw new IllegalStateException("Source type is unknowned : " + source + "\n valid types ares Join and Selector"); } return absolute; } public static Query makeAbsolute(final Query query, final Session session){ Source source = query.getSource(); if(isAbsolute(source)){ //nothing to change, query is absolute already return query; } source = makeAbsolute(source, session); QueryBuilder qb = new QueryBuilder(query); qb.setSource(source); return qb.buildQuery(); } public static FeatureCollection evaluate(final String id, final Query query){ return evaluate(id,query, null); } /** * Create a feature collection for the given query. * This method will try to use the feature store query capabilities if several * selector/join source use the same Session. * Otherwise a generic (slower) implementation will be returned. * * @param id : feature collection id. * @param query : query * @param session : use this session if the query is not absolute * @return feature collection */ public static FeatureCollection evaluate(final String id, Query query, final Session session){ query = QueryUtilities.makeAbsolute(query, session); final String language = query.getLanguage(); if(Query.GEOTK_QOM.equalsIgnoreCase(language)){ final Source s = query.getSource(); if(s instanceof Selector){ return new DefaultSelectorFeatureCollection(id, query); }else if(s instanceof Join){ final Collection<Session> sessions = getSessions(s, null); if(sessions.size() == 1 && sessions.iterator().next().getFeatureStore().getQueryCapabilities().handleCrossQuery()){ //the feature store can handle our join query, it will be much more efficient then a generic implementation return new DefaultFeatureStoreJoinFeatureCollection(id, query); }else{ //can't optimize it, use the generic implementation return new DefaultJoinFeatureCollection(id, query); } }else{ throw new IllegalArgumentException("Query source is an unknowned type : " + s); } }else{ //custom language query, let the feature store handle it return new DefaultTextStmtFeatureCollection(id, query); } } /** * Explore all source and check that the type is writable. * * @param source * @return true if all source are writable */ public static boolean isWritable(final Source source) throws DataStoreException{ if(source instanceof Join){ final Join j = (Join) source; return isWritable(j.getLeft()) && isWritable(j.getRight()); }else if(source instanceof Selector){ final Selector select = (Selector) source; final Session session = select.getSession(); if(session == null){ throw new IllegalArgumentException("Source must be absolute to verify if it's writable"); } return session.getFeatureStore().isWritable(select.getFeatureTypeName().toString()); }else if(source instanceof TextStatement){ return false; }else{ throw new IllegalStateException("Source type is unknowned : " + source + "\n valid types ares Join and Selector"); } } /** * Explore the source and return a collection of all session used in this * source. * * @param source : source to explore * @param buffer : a collection buffer, can be null * @return a collection of sessions, never null but can be empty. */ public static Collection<Session> getSessions(final Source source, Collection<Session> buffer){ if(buffer == null){ buffer = new HashSet<Session>(); } if(source instanceof Selector){ final Session s = ((Selector)source).getSession(); if(s != null){ buffer.add(s); } }else if(source instanceof Join){ final Join j = (Join) source; getSessions(j.getLeft(), buffer); getSessions(j.getRight(), buffer); } return buffer; } public static boolean queryAll(final Query query){ if(query.getSource() instanceof TextStatement){ return true; } return query.retrieveAllProperties() && query.getCoordinateSystemReproject() == null && query.getCoordinateSystemReproject() == null && query.getFilter() == Filter.INCLUDE && query.getMaxFeatures() == null && query.getSortBy() == null && query.getStartIndex() == 0; } /** * Combine two queries in the way that the resulting query act * as if it was a sub query result. * For example if the original query has a start index of 10 and the * sub-query a start index of 5, the resulting startIndex will be 15. * The type name of the first query will override the one of the second. * * @param original * @param second * @return sub query */ public static Query subQuery(final Query original, final Query second){ if ( original==null || second==null ) { throw new NullArgumentException("Both query must not be null."); } final QueryBuilder qb = new QueryBuilder(); qb.setSource(original.getSource()); //use the more restrictive max features field--------------------------- Integer max = original.getMaxFeatures(); if(second.getMaxFeatures() != null){ if(max == null){ max = second.getMaxFeatures(); }else{ max = Math.min(max, second.getMaxFeatures()); } } qb.setMaxFeatures(max); //join attributes names------------------------------------------------- final String[] propNames = retainAttributes( original.getPropertyNames(), second.getPropertyNames()); qb.setProperties(propNames); //use second crs over original crs-------------------------------------- if(second.getCoordinateSystemReproject() != null){ qb.setCRS(second.getCoordinateSystemReproject()); }else{ qb.setCRS(original.getCoordinateSystemReproject()); } //join filters---------------------------------------------------------- Filter filter = original.getFilter(); Filter filter2 = second.getFilter(); if ( filter.equals(Filter.INCLUDE) ){ filter = filter2; } else if ( !filter2.equals(Filter.INCLUDE) ){ filter = FF.and(filter, filter2); } qb.setFilter(filter); //group start index ---------------------------------------------------- int start = original.getStartIndex() + second.getStartIndex(); qb.setStartIndex(start); //ordering ------------------------------------------------------------- final List<SortBy> sorts = new ArrayList<SortBy>(); SortBy[] sts = original.getSortBy(); if(sts != null){ sorts.addAll(Arrays.asList(sts)); } sts = second.getSortBy(); if(sts != null){ sorts.addAll(Arrays.asList(sts)); } if(sorts != null){ qb.setSortBy(sorts.toArray(new SortBy[sorts.size()])); } //hints of the second query--------------------------------------------- qb.setHints(second.getHints()); //copy the resolution parameter----------------------------------------- final double[] resFirst = original.getResolution(); final double[] resSecond = second.getResolution(); if(resFirst == null || Double.isNaN(resFirst[0])){ qb.setResolution(resSecond); }else{ qb.setResolution(resFirst); } //mix versions, second query version takes precedence. if(original.getVersionDate()!=null) qb.setVersionDate(original.getVersionDate()); if(original.getVersionLabel()!=null) qb.setVersionLabel(original.getVersionLabel()); if(second.getVersionDate()!=null) qb.setVersionDate(second.getVersionDate()); if(second.getVersionLabel()!=null) qb.setVersionLabel(second.getVersionLabel()); return qb.buildQuery(); } /** * Takes two {@link Query}objects and produce a new one by mixing the * restrictions of both of them. * * <p> * The policy to mix the queries components is the following: * * <ul> * <li> * typeName: type names MUST match (not checked if some or both queries * equals to <code>Query.ALL</code>) * </li> * <li> * handle: you must provide one since no sensible choice can be done * between the handles of both queries * </li> * <li> * maxFeatures: the lower of the two maxFeatures values will be used (most * restrictive) * </li> * <li> * attributeNames: the attributes of both queries will be joined in a * single set of attributes. IMPORTANT: only <b><i>explicitly</i></b> * requested attributes will be joint, so, if the method * <code>retrieveAllProperties()</code> of some of the queries returns * <code>true</code> it does not means that all the properties will be * joined. You must create the query with the names of the properties you * want to load. * </li> * <li> * filter: the filters of both queries are or'ed * </li> * <li> * <b>any other query property is ignored</b> and no guarantees are made of * their return values, so client code shall explicitly care of hints, startIndex, etc., * if needed. * </li> * </ul> * </p> * * @param firstQuery first query * @param secondQuery second query * * @return Query restricted to the limits of definitionQuery * * @throws NullPointerException if some of the queries is null * @throws IllegalArgumentException if the type names of both queries do * not match */ public static Query mixQueries(final Query firstQuery, final Query secondQuery) { if ( firstQuery==null || secondQuery==null ) { throw new NullArgumentException("Both query must not be null."); } if ((firstQuery.getTypeName() != null) && (secondQuery.getTypeName() != null)) { if (!firstQuery.getTypeName().equals(secondQuery.getTypeName())) { String msg = "Type names do not match: " + firstQuery.getTypeName() + " != " + secondQuery.getTypeName(); throw new IllegalArgumentException(msg); } } //none of the queries equals Query.ALL, mix them //use the more restrictive max features field final int maxFeatures = Math.min(firstQuery.getMaxFeatures(), secondQuery.getMaxFeatures()); //join attributes names final String[] propNames = joinAttributes(firstQuery.getPropertyNames(), secondQuery.getPropertyNames()); //join filters Filter filter = firstQuery.getFilter(); Filter filter2 = secondQuery.getFilter(); if ((filter == null) || filter.equals(Filter.INCLUDE)) { filter = filter2; } else if ((filter2 != null) && !filter2.equals(Filter.INCLUDE)) { filter = FF.and(filter, filter2); } int start = firstQuery.getStartIndex() + secondQuery.getStartIndex(); //build the mixed query final String typeName = firstQuery.getTypeName() != null ? firstQuery.getTypeName() : secondQuery.getTypeName(); final QueryBuilder builder = new QueryBuilder(); builder.setTypeName(typeName); builder.setFilter(filter); builder.setMaxFeatures(maxFeatures); builder.setProperties(propNames); builder.setStartIndex(start); //mix versions, second query version takes precedence. if(firstQuery.getVersionDate()!=null) builder.setVersionDate(firstQuery.getVersionDate()); if(firstQuery.getVersionLabel()!=null) builder.setVersionLabel(firstQuery.getVersionLabel()); if(secondQuery.getVersionDate()!=null) builder.setVersionDate(secondQuery.getVersionDate()); if(secondQuery.getVersionLabel()!=null) builder.setVersionLabel(secondQuery.getVersionLabel()); return builder.buildQuery(); } /** * Creates a set of attribute names from the two input lists of names, * maintaining the order of the first list and appending the non repeated * names of the second. * <p> * In the case where both lists are <code>null</code>, <code>null</code> * is returned. * </p> * * @param atts1 the first list of attribute names, who's order will be * maintained * @param atts2 the second list of attribute names, from wich the non * repeated names will be appended to the resulting list * * @return Set of attribute names from <code>atts1</code> and * <code>atts2</code> */ private static String[] joinAttributes(final String[] atts1, final String[] atts2) { if (atts1 == null && atts2 == null) { return null; } final List atts = new LinkedList(); if (atts1 != null) { atts.addAll(Arrays.asList(atts1)); } if (atts2 != null) { for (int i = 0; i < atts2.length; i++) { if (!atts.contains(atts2[i])) { atts.add(atts2[i]); } } } final String[] propNames = new String[atts.size()]; atts.toArray(propNames); return propNames; } /** * Creates a set of attribute names from the two input lists of names, * while keep only the attributes from the second list */ private static String[] retainAttributes(final String[] atts1, final String[] atts2) { if (atts1 == null && atts2 == null) { return null; } if(atts1 == null){ return atts2; } if(atts2 == null){ return atts1; } final List atts = new LinkedList(); final List lst1 = UnmodifiableArrayList.wrap(atts1); for (int i = 0; i < atts2.length; i++) { if (lst1.contains(atts2[i])) { atts.add(atts2[i]); } } final String[] propNames = new String[atts.size()]; atts.toArray(propNames); return propNames; } }