/* * Geotoolkit - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2004-2008, Open Source Geospatial Foundation (OSGeo) * (C) 2009, 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.filter.visitor; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.regex.Pattern; import org.opengis.filter.And; import org.opengis.filter.Filter; import org.opengis.filter.FilterVisitor; import org.opengis.filter.Id; import org.opengis.filter.Or; import org.opengis.filter.PropertyIsEqualTo; import org.opengis.filter.expression.Literal; import org.opengis.filter.identity.FeatureId; import org.opengis.filter.identity.GmlObjectId; import org.opengis.filter.identity.Identifier; /** * Takes a filter and returns a simplified, equivalent one. At the moment the filter simplifies out * {@link Filter#INCLUDE} and {@link Filter#EXCLUDE} and deal with FID filter validation. * <p> * FID filter validation is meant to wipe out non valid feature ids from {@link Id} filters. This is * so in order to avoid sending feature ids down to DataStores that are not valid as per the * specific FeatureType fid structure. Since this is structure is usually FeatureStore specific, some * times being a strategy based on how the feature type primary key is generated, fid validation is * abstracted out to the {@link FIDValidator} interface so when a FeatureStore is about to send a query * down to the backend it van provide this visitor with a validator specific for the feature type * fid structure being queried. * </p> * <p> * By default all feature ids are valid. DataStores that want non valid fids to be wiped out should * set a {@link FIDValidator} through the {@link #setFIDValidator(FIDValidator)} method. * </p> * * @author Andrea Aime - OpenGeo * @author Gabriel Roldan (OpenGeo) * @module * @since 2.5.x * @version $Id$ */ public class SimplifyingFilterVisitor extends DuplicatingFilterVisitor { /** * Defines a simple means of assessing whether a feature id in an {@link Id} filter is * structurally valid and hence can be send down to the backend with confidence it will not * cause trouble, the most common one being filtering by pk number even if the type name prefix * does not match. */ public static interface FIDValidator { public boolean isValid(String fid); } /** * A 'null-object' fid validator that assumes any feature id in an {@link Id} filter is valid */ public static final FIDValidator ANY_FID_VALID = new FIDValidator() { @Override public boolean isValid(String fid) { return true; } }; /** * A FID validator that matches the fids with a given regular expression to determine the fid's * validity. * * @author Gabriel Roldan (OpenGeo) */ public static class RegExFIDValidator implements FIDValidator { private Pattern pattern; /** * @param regularExpression * a regular expression as used by the {@code java.util.regex} package */ public RegExFIDValidator(final String regularExpression) { pattern = Pattern.compile(regularExpression); } @Override public boolean isValid(final String fid) { return pattern.matcher(fid).matches(); } } /** * A convenient fid validator for the common case of a feature id being a composition of a * {@code <typename>.<number>} */ public static class TypeNameDotNumberFidValidator extends RegExFIDValidator { /** * @param typeName * the typename that will be used for a regular expression match in the form of * {@code <typename>.<number>} */ public TypeNameDotNumberFidValidator(final String typeName) { super(typeName + "\\.\\d+"); } } private FIDValidator fidValidator = ANY_FID_VALID; public void setFIDValidator(final FIDValidator validator) { this.fidValidator = validator == null ? ANY_FID_VALID : validator; } @Override public Object visit(final And filter, final Object extraData) { // scan, clone and simplify the children final List<Filter> children = filter.getChildren(); final List<Filter> newChildren = new ArrayList<Filter>(children.size()); for (Filter child : children) { final Filter cloned = (Filter) child.accept(this, extraData); // if any of the child filters is exclude, // the whole chain of AND is equivalent to EXCLUDE if(cloned == Filter.EXCLUDE) return Filter.EXCLUDE; // these can be skipped if(cloned == Filter.INCLUDE) continue; newChildren.add(cloned); } // we might end up with an empty list if(newChildren.size() == 0) return Filter.INCLUDE; // remove the logic we have only one filter if(newChildren.size() == 1) return newChildren.get(0); // else return the cloned and simplified up list return getFactory(extraData).and(newChildren); } @Override public Object visit(final Or filter, final Object extraData) { // scan, clone and simplify the children final List<Filter> children = filter.getChildren(); final List<Filter> newChildren = new ArrayList<Filter>(children.size()); Set<Identifier> mergedIds = null; Id regroupedIds = null; for (Filter child : children) { final Filter cloned = (Filter) child.accept(this, extraData); // if any of the child filters is include, // the whole chain of OR is equivalent to INCLUDE if(cloned == Filter.INCLUDE) return Filter.INCLUDE; // these can be skipped if(cloned == Filter.EXCLUDE) continue; if(cloned instanceof Id){ //merge id filters if(regroupedIds == null && mergedIds == null){ regroupedIds = (Id) cloned; }else{ if(mergedIds == null){ mergedIds = new HashSet<Identifier>(regroupedIds.getIdentifiers()); } regroupedIds = null; mergedIds.addAll( ((Id)cloned).getIdentifiers() ); } }else{ newChildren.add(cloned); } } if(regroupedIds != null){ newChildren.add(regroupedIds); }else if(mergedIds != null){ newChildren.add(ff.id(mergedIds)); } // we might end up with an empty list if(newChildren.size() == 0) return Filter.EXCLUDE; // remove the logic we have only one filter if(newChildren.size() == 1) return newChildren.get(0); // else return the cloned and simplified up list return getFactory(extraData).or(newChildren); } /** * Uses the current {@link FIDValidator} to wipe out illegal feature ids from the returned * filters. * * @return a filter containing only valid fids as per the current {@link FIDValidator}, may be * {@link Filter#EXCLUDE} if none matches or the filter is already empty */ @Override public Object visit(final Id filter, final Object extraData) { // if the set of ID is empty, it's actually equivalent to Filter.EXCLUDE if (filter.getIDs().size() == 0) { return Filter.EXCLUDE; } final Set<Identifier> validFids = new HashSet<Identifier>(); for (Identifier id : filter.getIdentifiers()) { if(id instanceof FeatureId || id instanceof GmlObjectId){ // both FeatureId an GmlObjectId.getID() return String, but Identifier.getID() // returns Object. Yet, FeatureId and GmlObjectId are the only known subclasses of // Identifier that apply to Feature land if (fidValidator.isValid((String)id.getID())) { validFids.add(id); } } } if (validFids.size() == 0) { return Filter.EXCLUDE; } else { return getFactory(extraData).id(validFids); } } @Override public Object visit(PropertyIsEqualTo filter, Object extraData) { if( filter.getExpression1() instanceof Literal && filter.getExpression2() instanceof Literal){ //we can preevaluate this one return (filter.evaluate(null)) ? Filter.INCLUDE : Filter.EXCLUDE; }else{ return super.visit(filter, extraData); } } }