/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2004-2015, Open Source Geospatial Foundation (OSGeo)
*
* 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.geotools.filter.visitor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import org.geotools.filter.FilterAttributeExtractor;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.FeatureType;
import org.opengis.filter.And;
import org.opengis.filter.BinaryComparisonOperator;
import org.opengis.filter.BinaryLogicOperator;
import org.opengis.filter.Filter;
import org.opengis.filter.FilterFactory2;
import org.opengis.filter.Id;
import org.opengis.filter.Not;
import org.opengis.filter.Or;
import org.opengis.filter.PropertyIsBetween;
import org.opengis.filter.PropertyIsEqualTo;
import org.opengis.filter.PropertyIsGreaterThan;
import org.opengis.filter.PropertyIsGreaterThanOrEqualTo;
import org.opengis.filter.PropertyIsLessThan;
import org.opengis.filter.PropertyIsLessThanOrEqualTo;
import org.opengis.filter.PropertyIsLike;
import org.opengis.filter.PropertyIsNil;
import org.opengis.filter.PropertyIsNotEqualTo;
import org.opengis.filter.PropertyIsNull;
import org.opengis.filter.expression.Expression;
import org.opengis.filter.expression.Literal;
import org.opengis.filter.expression.NilExpression;
import org.opengis.filter.expression.PropertyName;
import org.opengis.filter.expression.VolatileFunction;
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:
* <ul>
* <li>simplifies out {@link Filter#INCLUDE} and {@link Filter#EXCLUDE} in logical expressions</li>
* <li>removes double logic negations</li>
* <li>deal with FID filter validation removing invalid fids</li>
* <li>optimize out all non volatile functions that do not happen to use attributes, replacing them
* with literals</li>
* </ul>
* <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 DataStore 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 DataStore 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)
* @since 2.5.x
* @version $Id$
*
*
* @source $URL$
*/
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() {
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(String regularExpression) {
pattern = Pattern.compile(regularExpression);
}
public boolean isValid(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+");
}
}
FilterAttributeExtractor attributeExtractor = new FilterAttributeExtractor();
protected FeatureType featureType;
private FIDValidator fidValidator = ANY_FID_VALID;
private boolean rangeSimplicationEnabled = false;
public void setFIDValidator(FIDValidator validator) {
this.fidValidator = validator == null ? ANY_FID_VALID : validator;
}
public void setFeatureType(FeatureType featureType) {
this.featureType = featureType;
}
@Override
public Object visit(And filter, Object extraData) {
// drill down and flatten
List<Filter> filters = collect(filter, And.class, extraData, new ArrayList<Filter>());
filters = basicAndSimplification(filters);
filters = extraAndSimplification(extraData, filters);
// we might end up with an empty list
if (filters.size() == 0) {
return Filter.INCLUDE;
}
// remove the logic we have only one filter
if (filters.size() == 1) {
return filters.get(0);
}
return getFactory(extraData).and(filters);
}
protected List<Filter> basicAndSimplification(List<Filter> filters) {
// perform range simplifications (by intersection), if possible
if (rangeSimplicationEnabled && featureType != null && isSimpleFeature()) {
RangeCombiner combiner = new RangeCombiner.And(ff, featureType, filters);
filters = combiner.getReducedFilters();
}
// eliminate include and exclude
List<Filter> simplified = new ArrayList<Filter>(filters.size());
for (Filter child : filters) {
// if any of the child filters is exclude,
// the whole chain of AND is equivalent to
// EXCLUDE
if (child == Filter.EXCLUDE) {
return Arrays.asList((Filter) Filter.EXCLUDE);
}
// these can be skipped
if (child == Filter.INCLUDE) {
continue;
}
simplified.add(child);
}
// see if we have dual filters that can lead to Filter.Exclude, or duplicated filters
for (int i = 0; i < simplified.size(); i++) {
for (int j = i + 1; j < simplified.size();) {
Filter f1 = simplified.get(i);
Filter f2 = simplified.get(j);
if (f1.equals(f2)) {
simplified.remove(j);
} else if (dualFilters(f1, f2)) {
return Arrays.asList((Filter) Filter.EXCLUDE);
} else {
j++;
}
}
}
return simplified;
}
protected <T extends BinaryLogicOperator> List<Filter> collect(T filter, Class<T> type,
Object extraData,
List<Filter> collected) {
for (Filter child : filter.getChildren()) {
if (type.isInstance(child)) {
T and = (T) child;
collect(and, type, extraData, collected);
} else {
Filter cloned = (Filter) child.accept(this, extraData);
if (type.isInstance(cloned)) {
T and = (T) cloned;
collect(and, type, extraData, collected);
} else {
collected.add(cloned);
}
}
}
return collected;
}
/**
* Two filters are dual if the are the negation of each other (range based logic is handled
* separately)
*
* @param f1
* @param f2
* @return
*/
private boolean dualFilters(Filter f1, Filter f2) {
if (f1 instanceof Not) {
Not not = (Not) f1;
return f2.equals(not.getFilter());
} else if (f2 instanceof Not) {
Not not = (Not) f2;
return f1.equals(not.getFilter());
} else if ((f1 instanceof PropertyIsEqualTo && f2 instanceof PropertyIsNotEqualTo)
|| (f1 instanceof PropertyIsNotEqualTo && f2 instanceof PropertyIsEqualTo)) {
PropertyIsEqualTo e;
PropertyIsNotEqualTo ne;
if (f2 instanceof PropertyIsEqualTo) {
e = (PropertyIsEqualTo) f2;
ne = (PropertyIsNotEqualTo) f1;
} else {
e = (PropertyIsEqualTo) f1;
ne = (PropertyIsNotEqualTo) f2;
}
// the dual filter logic is correctly implemented only for single value attributes
if (!isSimpleFeature()) {
return false;
} else {
return (e.getExpression1().equals(ne.getExpression1()) && e.getExpression2()
.equals(ne.getExpression2()))
|| (e.getExpression2().equals(ne.getExpression1()) && e.getExpression1()
.equals(ne.getExpression2()));
}
}
return false;
}
@Override
public Object visit(Or filter, Object extraData) {
// scan, clone and simplify the children
List<Filter> filters = collect(filter, Or.class, extraData, new ArrayList<Filter>());
filters = basicOrSimplification(filters);
filters = extraOrSimplification(extraData, filters);
// we might end up with an empty list
if (filters.size() == 0) {
return Filter.EXCLUDE;
}
// remove the logic we have only one filter
if (filters.size() == 1) {
return filters.get(0);
}
// else return the cloned and simplified up list
return getFactory(extraData).or(filters);
}
protected List<Filter> basicOrSimplification(List<Filter> filters) {
// perform range simplifications (by intersection), if possible
if (rangeSimplicationEnabled && featureType != null && isSimpleFeature()) {
RangeCombiner combiner = new RangeCombiner.Or(ff, featureType, filters);
filters = combiner.getReducedFilters();
}
// eliminate include and exclude
List<Filter> simplified = new ArrayList<Filter>(filters.size());
for (Filter child : filters) {
// if any of the child filters is INCLUDE,
// the whole chain of OR is equivalent to
// INCLUDE
if (child == Filter.INCLUDE) {
return Arrays.asList((Filter) Filter.INCLUDE);
}
// these can be skipped
if (child == Filter.EXCLUDE) {
continue;
}
simplified.add(child);
}
// see if we have dual filters that can lead to Filter.Exclude, or duplicated filters
for (int i = 0; i < simplified.size(); i++) {
for (int j = i + 1; j < simplified.size();) {
Filter f1 = simplified.get(i);
Filter f2 = simplified.get(j);
if (f1.equals(f2)) {
simplified.remove(j);
} else if (dualFilters(f1, f2)) {
return Arrays.asList((Filter) Filter.INCLUDE);
} else {
j++;
}
}
}
return simplified;
}
protected List<Filter> extraAndSimplification(Object extraData, List<Filter> filters) {
return filters;
}
protected List<Filter> extraOrSimplification(Object extraData, List<Filter> filters) {
return filters;
}
/**
* 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(Id filter, Object extraData) {
// if the set of ID is empty, it's actually equivalent to Filter.EXCLUDE
if (filter.getIDs().size() == 0) {
return Filter.EXCLUDE;
}
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);
}
}
}
Filter validIdFilter;
if (validFids.size() == 0) {
validIdFilter = Filter.EXCLUDE;
} else {
validIdFilter = getFactory(extraData).id(validFids);
}
return validIdFilter;
}
public Object visit(Not filter, Object extraData) {
FilterFactory2 ff = getFactory(extraData);
Filter inner = filter.getFilter();
if (inner instanceof Not) {
// simplify out double negation
Not innerNot = (Not) inner;
return innerNot.getFilter().accept(this, extraData);
} else if(inner == Filter.INCLUDE) {
return Filter.EXCLUDE;
} else if(inner == Filter.EXCLUDE) {
return Filter.INCLUDE;
} else if (inner instanceof PropertyIsBetween && isSimpleFeature()) {
PropertyIsBetween pb = (PropertyIsBetween) inner.accept(this, extraData);
Filter lt = ff.less(pb.getExpression(), pb.getLowerBoundary());
Filter gt = ff.greater(pb.getExpression(), pb.getUpperBoundary());
return ff.or(lt, gt);
} else if (inner instanceof PropertyIsEqualTo && isSimpleFeature()) {
PropertyIsEqualTo pe = (PropertyIsEqualTo) inner.accept(this, extraData);
return ff.notEqual(pe.getExpression1(), pe.getExpression2(), pe.isMatchingCase());
} else if (inner instanceof PropertyIsNotEqualTo && isSimpleFeature()) {
PropertyIsNotEqualTo pe = (PropertyIsNotEqualTo) inner.accept(this, extraData);
return ff.equal(pe.getExpression1(), pe.getExpression2(), pe.isMatchingCase());
} else if (inner instanceof PropertyIsGreaterThan && isSimpleFeature()) {
PropertyIsGreaterThan pg = (PropertyIsGreaterThan) inner.accept(this, extraData);
return ff.lessOrEqual(pg.getExpression1(), pg.getExpression2(), pg.isMatchingCase());
} else if (inner instanceof PropertyIsGreaterThanOrEqualTo && isSimpleFeature()) {
PropertyIsGreaterThanOrEqualTo pg = (PropertyIsGreaterThanOrEqualTo) inner.accept(this,
extraData);
return ff.less(pg.getExpression1(), pg.getExpression2(), pg.isMatchingCase());
} else if (inner instanceof PropertyIsLessThan && isSimpleFeature()) {
PropertyIsLessThan pl = (PropertyIsLessThan) inner.accept(this, extraData);
return ff.greaterOrEqual(pl.getExpression1(), pl.getExpression2(), pl.isMatchingCase());
} else if (inner instanceof PropertyIsLessThanOrEqualTo && isSimpleFeature()) {
PropertyIsLessThanOrEqualTo pl = (PropertyIsLessThanOrEqualTo) inner.accept(this,
extraData);
return ff.greater(pl.getExpression1(), pl.getExpression2(), pl.isMatchingCase());
} else if (inner instanceof And) {
// De Morgan
And and = (And) inner;
List<Filter> children = and.getChildren();
List<Filter> negatedChildren = new ArrayList<>();
for (Filter child : children) {
negatedChildren.add((Filter) ff.not(child).accept(this, extraData));
}
return ff.or(negatedChildren);
} else if (inner instanceof Or) {
// De Morgan
Or or = (Or) inner;
List<Filter> children = or.getChildren();
List<Filter> negatedChildren = new ArrayList<>();
for (Filter child : children) {
negatedChildren.add((Filter) ff.not(child).accept(this, extraData));
}
return ff.and(negatedChildren);
} else {
return super.visit(filter, extraData);
}
}
/**
* Returns true if the target feature type is a simple feature one
*
* @return
*/
protected boolean isSimpleFeature() {
return featureType instanceof SimpleFeatureType;
}
public Object visit(org.opengis.filter.expression.Function function, Object extraData) {
// can't optimize out volatile functions
if (isVolatileFunction(function)) {
return super.visit(function, extraData);
}
// stable function, is it using attributes?
if (attributeExtractor == null) {
attributeExtractor = new FilterAttributeExtractor();
} else {
attributeExtractor.clear();
}
function.accept(attributeExtractor, null);
// if so we can replace it with a literal
if (attributeExtractor.isConstantExpression()) {
Object result = function.evaluate(null);
return ff.literal(result);
} else {
return super.visit(function, extraData);
}
}
/**
* Checks if a function is volatile in this context. By default it checks if the function
* implements the {@link VolatileFunction} interface, subclasses can override
*
* @param function
* @return
*/
protected boolean isVolatileFunction(org.opengis.filter.expression.Function function) {
return function instanceof VolatileFunction;
}
/**
* Tries to simplify the filter if it's not already a simple one.
*
* @param filter
* @return
*/
public static Filter simplify(Filter filter) {
return simplify(filter, null);
}
/**
* Tries to simplify the filter if it's not already a simple one
*
* @param filter
* @return
*/
public static Filter simplify(Filter filter, FeatureType featureType) {
// if already as simple as possible, or cannot be simplified anyways
if (filter == Filter.INCLUDE || filter == Filter.EXCLUDE || filter == null) {
return filter;
}
SimplifyingFilterVisitor visitor = new SimplifyingFilterVisitor();
visitor.setFeatureType(featureType);
return (Filter) filter.accept(visitor, null);
}
protected boolean isConstant(Expression ex) {
// quick common cases first
if (ex instanceof Literal) {
return true;
} else if (ex instanceof NilExpression) {
return true;
} else if (ex instanceof PropertyName) {
return false;
}
// ok, check for attribute dependencies and volatile functions then
attributeExtractor.clear();
ex.accept(attributeExtractor, null);
return attributeExtractor.isConstantExpression();
}
public Object visit(PropertyIsBetween filter, Object extraData) {
PropertyIsBetween clone = (PropertyIsBetween) super.visit(filter, extraData);
if (isConstant(clone.getExpression()) && isConstant(clone.getLowerBoundary())
&& isConstant(clone.getUpperBoundary())) {
return staticFilterEvaluate(clone);
} else {
return clone;
}
}
private Object staticFilterEvaluate(Filter filter) {
if (filter.evaluate(null)) {
return Filter.INCLUDE;
} else {
return Filter.EXCLUDE;
}
}
public Object visit(PropertyIsEqualTo filter, Object extraData) {
return simplifyBinaryComparisonOperator((BinaryComparisonOperator) super.visit(filter,
extraData));
}
private Object simplifyBinaryComparisonOperator(BinaryComparisonOperator clone) {
if (isConstant(clone.getExpression1()) && isConstant(clone.getExpression2())) {
return staticFilterEvaluate(clone);
} else {
return clone;
}
}
public Object visit(PropertyIsNotEqualTo filter, Object extraData) {
return simplifyBinaryComparisonOperator((BinaryComparisonOperator) super.visit(filter,
extraData));
}
public Object visit(PropertyIsGreaterThan filter, Object extraData) {
return simplifyBinaryComparisonOperator((BinaryComparisonOperator) super.visit(filter,
extraData));
}
public Object visit(PropertyIsGreaterThanOrEqualTo filter, Object extraData) {
return simplifyBinaryComparisonOperator((BinaryComparisonOperator) super.visit(filter,
extraData));
}
public Object visit(PropertyIsLessThan filter, Object extraData) {
return simplifyBinaryComparisonOperator((BinaryComparisonOperator) super.visit(filter,
extraData));
}
public Object visit(PropertyIsLessThanOrEqualTo filter, Object extraData) {
return simplifyBinaryComparisonOperator((BinaryComparisonOperator) super.visit(filter,
extraData));
}
@Override
public Object visit(PropertyIsLike filter, Object extraData) {
PropertyIsLike clone = (PropertyIsLike) super.visit(filter, extraData);
if (isConstant(clone.getExpression())) {
return staticFilterEvaluate(clone);
} else {
return clone;
}
}
@Override
public Object visit(PropertyIsNil filter, Object extraData) {
PropertyIsNil clone = (PropertyIsNil) super.visit(filter, extraData);
if (isConstant(clone.getExpression())) {
return staticFilterEvaluate(clone);
} else {
return clone;
}
}
@Override
public Object visit(PropertyIsNull filter, Object extraData) {
PropertyIsNull clone = (PropertyIsNull) super.visit(filter, extraData);
if (isConstant(clone.getExpression())) {
return staticFilterEvaluate(clone);
} else {
return clone;
}
}
public boolean isRangeSimplicationEnabled() {
return rangeSimplicationEnabled;
}
/**
* Enables/disable range simplification. Range simplification can figure out that the logic
* combination of multiple ranges against the same property can be turned into a single range, a
* INCLUDE, or a EXCLUDE, but it requires the range boundaries to be of the same type as the
*
* @param rangeSimplicationEnabled
*/
public void setRangeSimplicationEnabled(boolean rangeSimplicationEnabled) {
this.rangeSimplicationEnabled = rangeSimplicationEnabled;
}
}