/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2002-2008, 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.xml.filter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.Stack;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.filter.FilterType;
import org.geotools.filter.IllegalFilterException;
import org.geotools.filter.visitor.DuplicatingFilterVisitor;
import org.geotools.xml.XMLHandlerHints;
import org.opengis.filter.And;
import org.opengis.filter.BinaryLogicOperator;
import org.opengis.filter.ExcludeFilter;
import org.opengis.filter.Filter;
import org.opengis.filter.FilterFactory2;
import org.opengis.filter.FilterVisitor;
import org.opengis.filter.Id;
import org.opengis.filter.IncludeFilter;
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.PropertyIsNotEqualTo;
import org.opengis.filter.PropertyIsNull;
import org.opengis.filter.identity.FeatureId;
import org.opengis.filter.identity.Identifier;
import org.opengis.filter.spatial.BBOX;
import org.opengis.filter.spatial.Beyond;
import org.opengis.filter.spatial.Contains;
import org.opengis.filter.spatial.Crosses;
import org.opengis.filter.spatial.DWithin;
import org.opengis.filter.spatial.Disjoint;
import org.opengis.filter.spatial.DistanceBufferOperator;
import org.opengis.filter.spatial.Equals;
import org.opengis.filter.spatial.Intersects;
import org.opengis.filter.spatial.Overlaps;
import org.opengis.filter.spatial.Touches;
import org.opengis.filter.spatial.Within;
import org.opengis.filter.temporal.After;
import org.opengis.filter.temporal.AnyInteracts;
import org.opengis.filter.temporal.Before;
import org.opengis.filter.temporal.Begins;
import org.opengis.filter.temporal.BegunBy;
import org.opengis.filter.temporal.BinaryTemporalOperator;
import org.opengis.filter.temporal.During;
import org.opengis.filter.temporal.EndedBy;
import org.opengis.filter.temporal.Ends;
import org.opengis.filter.temporal.Meets;
import org.opengis.filter.temporal.MetBy;
import org.opengis.filter.temporal.OverlappedBy;
import org.opengis.filter.temporal.TContains;
import org.opengis.filter.temporal.TEquals;
import org.opengis.filter.temporal.TOverlaps;
/**
* Prepares a filter for xml encoded for interoperability with another system. It will behave
* differently depeding on the compliance level chosen. A new request will have to be made and the
* features will have to be tested again on the client side if there are any FidFilters in the
* filter. Consider the following to understand why:
*
* <pre>
* and {
* nullFilter
* or{
* fidFilter
* nullFilter
* }
* }
* </pre>
*
* for strict it would throw an exception, for low it would be left alone, but for Medium it would
* end up as:
*
* <pre>
* and{
* nullFilter
* nullFilter
* }
* </pre>
*
* and getFids() would return the fids in the fidFilter.
*
* So the final filter would (this is not standard but a common implementation) return the results
* of the and filter as well as all the features that match the fids. Which is more than the
* original filter would accept.
*
* The XML Document writer can operate at different levels of compliance. The geotools level is
* extremely flexible and forgiving.
*
* <p>
* All NOT(FidFilter) are changed to Filter.INCLUDE. So make sure that the filter is processed again
* on the client with the original filter
* </p>
*
* For a description of the difference Compliance levels that can be used see
* <ul>
* <li>{@link XMLHandlerHints#VALUE_FILTER_COMPLIANCE_LOW}</li>
* <li>{@link XMLHandlerHints#VALUE_FILTER_COMPLIANCE_MEDIUM}</li>
* <li>{@link XMLHandlerHints#VALUE_FILTER_COMPLIANCE_HIGH}</li>
* </ul>
*
* @author Jody
*
* @source $URL: http://svn.osgeo.org/geotools/trunk/modules/library/xml/src/main/java/org/geotools/xml/filter/FilterCompliancePreProcessor.java $
*/
public class FilterCompliancePreProcessor implements FilterVisitor {
private static final int LOW = 0; // XMLHandlerHints.VALUE_FILTER_COMPLIANCE_LOW
private static final int MEDIUM = 1; // XMLHandlerHints.VALUE_FILTER_COMPLIANCE_MEDIUM
private static final int HIGH = 2; // XMLHandlerHints.VALUE_FILTER_COMPLIANCE_HIGH
private int complianceInt;
/** Data collected during traversal */
private Stack<Data> current = new Stack<Data>();
FilterFactory2 ff = CommonFactoryFinder.getFilterFactory2(null);
private boolean requiresPostProcessing = false;
public FilterCompliancePreProcessor(Integer complianceLevel) {
if ((complianceLevel != LOW) && (complianceLevel != MEDIUM) && (complianceLevel != HIGH)) {
throw new IllegalArgumentException(
"compliance level must be one of: XMLHandlerHints.VALUE_FILTER_COMPLIANCE_LOOSE "
+ "XMLHandlerHints.VALUE_FILTER_COMPLIANCE_MEDIUM or "
+ "XMLHandlerHints.VALUE_FILTER_COMPLIANCE_MAXIMUM");
}
this.complianceInt = complianceLevel.intValue();
}
/**
* Gets the fid filter that contains all the fids.
*
* @return the fid filter that contains all the fids.
*/
public Id getFidFilter() {
if (current.isEmpty()) {
Set<FeatureId> empty = Collections.emptySet();
return ff.id(empty);
}
Data data = (Data) current.peek();
if (data.fids.size() > 0) {
Set<FeatureId> set = new HashSet<FeatureId>();
Set<String> fids = data.fids;
for (String fid : fids) {
set.add(ff.featureId(fid));
}
return ff.id(set);
} else {
Set<FeatureId> empty = Collections.emptySet();
return ff.id(empty);
}
}
/**
* Returns the filter that can be encoded.
*
* @return the filter that can be encoded.
*/
public org.opengis.filter.Filter getFilter() {
if (current.isEmpty()) {
return Filter.EXCLUDE;
}
return ((Data) this.current.peek()).filter;
}
// between
public Object visit(PropertyIsBetween filter, Object extraData) {
current.push(new Data(filter));
return extraData;
}
// BinaryComparisonOperator
public Object visit(PropertyIsEqualTo filter, Object extraData ) {
current.push(new Data(filter));
return extraData;
}
public Object visit(PropertyIsGreaterThan filter, Object extraData ) {
current.push(new Data(filter));
return extraData;
}
public Object visit(PropertyIsGreaterThanOrEqualTo filter, Object extraData ) {
current.push(new Data(filter));
return extraData;
}
public Object visit(PropertyIsLessThan filter, Object extraData ) {
current.push(new Data(filter));
return extraData;
}
public Object visit(PropertyIsLessThanOrEqualTo filter, Object extraData ) {
current.push(new Data(filter));
return extraData;
}
public Object visit(PropertyIsNotEqualTo filter, Object extraData ) {
current.push(new Data(filter));
return extraData;
}
// GeometryFilter
public Object visit(BBOX filter, Object extraData) {
current.push(new Data(filter));
return extraData;
}
public Object visit(Contains filter, Object extraData) {
current.push(new Data(filter));
return extraData;
}
public Object visit(Crosses filter, Object extraData) {
current.push(new Data(filter));
return extraData;
}
public Object visit(Disjoint filter, Object extraData) {
current.push(new Data(filter));
return extraData;
}
public Object visit(DistanceBufferOperator filter, Object extraData) {
current.push(new Data(filter));
return extraData;
}
public Object visit(Equals filter, Object extraData) {
current.push(new Data(filter));
return extraData;
}
public Object visit(Intersects filter, Object extraData) {
current.push(new Data(filter));
return extraData;
}
public Object visit(Overlaps filter, Object extraData) {
current.push(new Data(filter));
return extraData;
}
public Object visit(Touches filter, Object extraData) {
current.push(new Data(filter));
return extraData;
}
public Object visit(Within filter, Object extraData) {
current.push(new Data(filter));
return extraData;
}
public Object visit(Beyond filter, Object extraData) {
current.push(new Data(filter));
return extraData;
}
public Object visit(DWithin filter, Object extraData) {
current.push(new Data(filter));
return extraData;
}
// LikeFilter
public Object visit(PropertyIsLike filter, Object extraData) {
current.push(new Data(filter));
return extraData;
}
// LogicFilter
public Object visit(And filter, Object extraData) {
int startSize = current.size();
try {
switch (this.complianceInt) {
case FilterCompliancePreProcessor.LOW:
current.push(new Data(filter));
break;
case MEDIUM:
for (Filter child : filter.getChildren()) {
extraData = child.accept(this, extraData);
}
Data mediumFilter = createMediumLevelLogicFilter(FilterType.LOGIC_AND, startSize);
current.push(mediumFilter);
break;
case HIGH:
for (Filter child : filter.getChildren()) {
extraData = child.accept(this, extraData);
}
Data highFilter = createHighLevelLogicFilter(FilterType.LOGIC_AND, startSize);
current.push(highFilter);
break;
default:
break;
}
} catch (Exception e) {
if (e instanceof UnsupportedFilterException) {
throw (UnsupportedFilterException) e;
}
throw new UnsupportedFilterException("Exception creating filter", e);
}
return extraData;
}
public Object visit(Or filter, Object extraData) {
int startSize = current.size();
try {
switch (this.complianceInt) {
case LOW:
current.push(new Data(filter));
break;
case MEDIUM:
for (Filter child : filter.getChildren()) {
extraData = child.accept(this, extraData);
}
Data mediumFilter = createMediumLevelLogicFilter(FilterType.LOGIC_AND, startSize);
current.push(mediumFilter);
break;
case HIGH:
for (Filter child : filter.getChildren()) {
extraData = child.accept(this, extraData);
}
Data highFilter = createHighLevelLogicFilter(FilterType.LOGIC_AND, startSize);
current.push(highFilter);
break;
default:
break;
}
} catch (Exception e) {
if (e instanceof UnsupportedFilterException) {
throw (UnsupportedFilterException) e;
}
throw new UnsupportedFilterException("Exception creating filter", e);
}
return extraData;
}
public Object visit(Not filter, Object extraData) {
int startSize = 1;
Filter child;
try {
switch (this.complianceInt) {
case LOW:
current.push(new Data(filter));
break;
case MEDIUM:
child = filter.getFilter();
extraData = child.accept(this, extraData);
Data mediumFilter = createMediumLevelLogicFilter(FilterType.LOGIC_NOT, startSize);
current.push(mediumFilter);
break;
case HIGH:
child = filter.getFilter();
extraData = child.accept(this, extraData);
Data highFilter = createHighLevelLogicFilter(FilterType.LOGIC_AND, startSize);
current.push(highFilter);
break;
default:
break;
}
} catch (Exception e) {
if (e instanceof UnsupportedFilterException) {
throw (UnsupportedFilterException) e;
}
throw new UnsupportedFilterException("Exception creating filter", e);
}
return extraData;
};
private Data createMediumLevelLogicFilter(short filterType, int startOfFilterStack)
throws IllegalFilterException {
Data resultingFilter;
switch (filterType) {
case FilterType.LOGIC_AND: {
Set<String> fids = andFids(startOfFilterStack);
resultingFilter = buildFilter(filterType, startOfFilterStack);
resultingFilter.fids.addAll(fids);
if (resultingFilter.filter != Filter.EXCLUDE && !fids.isEmpty())
requiresPostProcessing = true;
break;
}
case FilterType.LOGIC_OR: {
Set fids = orFids(startOfFilterStack);
resultingFilter = buildFilter(filterType, startOfFilterStack);
resultingFilter.fids.addAll(fids);
break;
}
case FilterType.LOGIC_NOT:
resultingFilter = buildFilter(filterType, startOfFilterStack);
break;
default:
resultingFilter = buildFilter(filterType, startOfFilterStack);
break;
}
return resultingFilter;
}
private Set orFids(int startOfFilterStack) {
Set set = new HashSet();
for (int i = startOfFilterStack; i < current.size(); i++) {
Data data = (Data) current.get(i);
if (!data.fids.isEmpty()) {
set.addAll(data.fids);
}
}
return set;
}
private Set<String> andFids(int startOfFilterStack) {
if (!hasFidFilter(startOfFilterStack)) {
return Collections.emptySet();
}
Set<Data> toRemove = new HashSet<Data>();
List<Set<String>> fidSet = new ArrayList<Set<String>>();
boolean doRemove = true;
for (int i = startOfFilterStack; i < current.size(); i++) {
Data data = (Data) current.get(i);
if (data.fids.isEmpty()) {
toRemove.add(data);
} else {
fidSet.add(data.fids);
if (data.filter != Filter.EXCLUDE) {
doRemove = false;
}
}
}
if (doRemove) {
current.removeAll(toRemove);
}
if (fidSet.size() == 0) {
return Collections.emptySet();
}
if (fidSet.size() == 1) {
return fidSet.get(0);
}
HashSet<String> set = new HashSet<String>();
for (int i = 0; i < fidSet.size(); i++) {
Set<String> tmp = fidSet.get(i);
for (String fid : tmp) {
if (allContain(fid, fidSet)) {
set.add(fid);
}
}
}
return set;
}
private boolean allContain(String fid, List<Set<String>> fidSets) {
for (Set<String> tmp : fidSets) {
if (!tmp.contains(fid)) {
return false;
}
}
return true;
}
/**
*
* @param filterType LOGIC_NOT, LOGIC_AND or LOGIC_OR
* @param startOfFilterStack
* @return Data Stack data representing the genrated filter
* @throws IllegalFilterException
*/
private Data buildFilter(short filterType, int startOfFilterStack)
throws IllegalFilterException {
if (current.isEmpty()) {
return Data.ALL;
}
if (filterType == FilterType.LOGIC_NOT) {
return buildNotFilter(startOfFilterStack);
}
if (current.size() == (startOfFilterStack + 1)) {
return (Data) current.pop();
}
List<Filter> filterList = new ArrayList<Filter>();
while (current.size() > startOfFilterStack) {
Data data = (Data) current.pop();
if (data.filter != Filter.EXCLUDE) {
filterList.add(data.filter);
}
}
Filter f;
if (filterType == FilterType.LOGIC_AND) {
f = ff.and(filterList);
} else if (filterType == FilterType.LOGIC_OR) {
f = ff.or(filterList);
} else {
// not expected
f = null;
}
return new Data(compressFilter(filterType, f));
}
private Filter compressFilter(short filterType, Filter f)
throws IllegalFilterException {
Filter result;
int added = 0;
List<org.opengis.filter.Filter> resultList = new ArrayList<org.opengis.filter.Filter>();
switch (filterType) {
case FilterType.LOGIC_AND:
if (contains((And)f, Filter.EXCLUDE)) {
return Filter.EXCLUDE;
}
for (Filter item : ((And)f).getChildren()) {
org.opengis.filter.Filter filter = (org.opengis.filter.Filter) item;
if (filter == Filter.INCLUDE) {
continue;
}
added++;
resultList.add(filter);
}
if (resultList.isEmpty()) {
return Filter.EXCLUDE;
}
result = ff.and(resultList);
break;
case FilterType.LOGIC_OR:
if (contains((Or)f, Filter.INCLUDE)) {
return Filter.INCLUDE;
}
for (Object item : ((Or)f).getChildren()) {
org.opengis.filter.Filter filter = (org.opengis.filter.Filter) item;
if (filter == org.geotools.filter.Filter.ALL) {
continue;
}
added++;
resultList.add(filter);
}
if (resultList.isEmpty()) {
return Filter.EXCLUDE;
}
result = ff.or(resultList);
break;
default:
return Filter.EXCLUDE;
}
switch (added) {
case 0:
return Filter.EXCLUDE;
case 1:
if( result instanceof Not){
return ((Not)result).getFilter();
}
else {
return ((BinaryLogicOperator)result).getChildren().get(0);
}
default:
return result;
}
}
private boolean contains(BinaryLogicOperator f, org.opengis.filter.Filter toFind) {
for (Iterator iter = f.getChildren().iterator(); iter.hasNext();) {
if (toFind.equals(iter.next())) {
return true;
}
}
return false;
}
private Data buildNotFilter(int startOfFilterStack) {
if (current.size() > (startOfFilterStack + 1)) {
throw new UnsupportedFilterException("A not filter cannot have more than one filter");
} else {
Data tmp = (Data) current.pop();
Data data = new Data(ff.not(tmp.filter));
if (!tmp.fids.isEmpty()) {
data.filter = Filter.INCLUDE;
data.fids.clear();
requiresPostProcessing = true;
}
return data;
}
}
private Data createHighLevelLogicFilter(short filterType, int startOfFilterStack)
throws IllegalFilterException {
if (hasFidFilter(startOfFilterStack)) {
Set fids;
switch (filterType) {
case FilterType.LOGIC_AND:
fids = andFids(startOfFilterStack);
Data filter = buildFilter(filterType, startOfFilterStack);
filter.fids.addAll(fids);
return filter;
case FilterType.LOGIC_OR: {
if (hasNonFidFilter(startOfFilterStack)) {
throw new UnsupportedFilterException(
"Maximum compliance does not allow Logic filters to contain FidFilters");
}
fids = orFids(startOfFilterStack);
pop(startOfFilterStack);
Data data = new Data();
data.fids.addAll(fids);
return data;
}
case FilterType.LOGIC_NOT:
return buildFilter(filterType, startOfFilterStack);
default:
return Data.ALL;
}
} else {
return buildFilter(filterType, startOfFilterStack);
}
}
private void pop(int startOfFilterStack) {
while (current.size() > startOfFilterStack)
current.pop();
}
private boolean hasNonFidFilter(int startOfFilterStack) {
for (int i = startOfFilterStack; i < current.size(); i++) {
Data data = (Data) current.get(i);
if (data.filter != Filter.EXCLUDE) {
return true;
}
}
return false;
}
private boolean hasFidFilter(int startOfFilterStack) {
for (int i = startOfFilterStack; i < current.size(); i++) {
Data data = (Data) current.get(i);
if (!data.fids.isEmpty()) {
return true;
}
}
return false;
}
// NullFilter
public Object visit(PropertyIsNull filter, Object extraData) {
current.push(new Data(filter));
return extraData;
};
// FidFilter
public Object visit(Id filter, Object extraData) {
Data data = new Data();
for( Identifier identifier : filter.getIdentifiers() ){
FeatureId featureIdentifier = (FeatureId) identifier;
data.fids.add( featureIdentifier.getID() );
}
current.push(data);
return extraData;
}
// Include
public Object visit(IncludeFilter filter, Object extraData) {
current.push(new Data(filter));
return extraData;
}
// Exclude
public Object visit(ExcludeFilter filter, Object extraData) {
current.push(new Data(filter));
return extraData;
}
public Object visitNullFilter(Object extraData) {
// we will ignore null!
return extraData;
}
//Temporal
public Object visit(After after, Object extraData) {
return visit((BinaryTemporalOperator)after, extraData);
}
public Object visit(AnyInteracts anyInteracts, Object extraData) {
return visit((BinaryTemporalOperator)anyInteracts, extraData);
}
public Object visit(Before before, Object extraData) {
return visit((BinaryTemporalOperator)before, extraData);
}
public Object visit(Begins begins, Object extraData) {
return visit((BinaryTemporalOperator)begins, extraData);
}
public Object visit(BegunBy begunBy, Object extraData) {
return visit((BinaryTemporalOperator)begunBy, extraData);
}
public Object visit(During during, Object extraData) {
return visit((BinaryTemporalOperator)during, extraData);
}
public Object visit(EndedBy endedBy, Object extraData) {
return visit((BinaryTemporalOperator)endedBy, extraData);
}
public Object visit(Ends ends, Object extraData) {
return visit((BinaryTemporalOperator)ends, extraData);
}
public Object visit(Meets meets, Object extraData) {
return visit((BinaryTemporalOperator)meets, extraData);
}
public Object visit(MetBy metBy, Object extraData) {
return visit((BinaryTemporalOperator)metBy, extraData);
}
public Object visit(OverlappedBy overlappedBy, Object extraData) {
return visit((BinaryTemporalOperator)overlappedBy, extraData);
}
public Object visit(TContains contains, Object extraData) {
return visit((BinaryTemporalOperator)contains, extraData);
}
public Object visit(TEquals equals, Object extraData) {
return visit((BinaryTemporalOperator)equals, extraData);
}
public Object visit(TOverlaps contains, Object extraData) {
return visit((BinaryTemporalOperator)contains, extraData);
}
protected Object visit(BinaryTemporalOperator filter, Object data) {
current.push(new Data(filter));
return data;
}
private static class Data {
final public static Data NONE = new Data(Filter.EXCLUDE);
final public static Data ALL = new Data(Filter.INCLUDE);
final Set<String> fids = new HashSet<String>();
org.opengis.filter.Filter filter;
public Data() {
this(Filter.EXCLUDE);
}
public Data(Filter f) {
filter = f;
}
public String toString() {
return filter + ":" + fids;
}
}
/**
* Returns true if the filter was one where the request to the server is more general than the
* actual filter. See {@link XMLHandlerHints#VALUE_FILTER_COMPLIANCE_MEDIUM} and example of when
* this can happen.
*
* @return true if the filter was one where the request to the server is more general than the
* actual filter.
*/
public boolean requiresPostProcessing() {
return requiresPostProcessing;
}
}