/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2004-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.filter.visitor; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.Stack; import java.util.logging.Logger; import org.geotools.factory.CommonFactoryFinder; import org.geotools.filter.FilterCapabilities; import org.geotools.filter.IllegalFilterException; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.filter.And; import org.opengis.filter.BinaryComparisonOperator; import org.opengis.filter.ExcludeFilter; import org.opengis.filter.Filter; import org.opengis.filter.FilterFactory; 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.expression.Add; import org.opengis.filter.expression.BinaryExpression; import org.opengis.filter.expression.Divide; import org.opengis.filter.expression.Expression; import org.opengis.filter.expression.ExpressionVisitor; import org.opengis.filter.expression.Function; import org.opengis.filter.expression.Literal; import org.opengis.filter.expression.Multiply; import org.opengis.filter.expression.NilExpression; import org.opengis.filter.expression.PropertyName; import org.opengis.filter.expression.Subtract; import org.opengis.filter.spatial.BBOX; import org.opengis.filter.spatial.Beyond; import org.opengis.filter.spatial.BinarySpatialOperator; 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.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; /** * Determines what queries can be processed server side and which can be processed client side. * * IMPLEMENTATION NOTE: * This class is implemented as a stack processor. If you're curious how it works, compare it with * the old SQLUnpacker class, which did the same thing using recursion in a more straightforward * way. * * Here's a non-implementors best-guess at the algorithm: * Starting at the top of the filter, split each filter into * its constituent parts. If the given FilterCapabilities * support the given operator, then keep checking downwards. * * The key is in knowing whether or not something "down the tree" * from you wound up being supported or not. This is where the * stacks come in. Right before handing off to accept() the sub- * filters, we count how many things are currently on the "can * be proccessed by the underlying datastore" stack (the preStack) * and we count how many things are currently on the "need to be post- * processed" stack. * * After the accept() call returns, we look again at the preStack.size() * and postStack.size(). If the postStack has grown, that means that there * was stuff down in the accept()-ed filter that wasn't supportable. * Usually this means that our filter isn't supportable, but not always. * * In some cases a sub-filter being unsupported isn't necessarily bad, * as we can 'unpack' OR statements into AND statements * (DeMorgans rule/modus poens) and still * see if we can handle the other side of the OR. Same with NOT and * certain kinds of AND statements. * * In addition this class supports the case where we're doing an split * in the middle of a client-side transaction. I.e. imagine doing a * <Transaction> against a WFS-T where you have to filter against * actions that happened previously in the transaction. That's what * the ClientTransactionAccessor interface does, and this class splits * filters while respecting the information about deletes and updates * that have happened previously in the Transaction. I can't say with * certainty exactly how the logic for that part of this works, but * the test suite does seem to test it and the tests do pass. * * * @author dzwiers * @author commented and ported from gt to ogc filters by saul.farber * * @source $URL$ * @deprecated use {@link CapabilitiesFilterSplitter} instead for geoapi FilterCapabilities */ public class PostPreProcessFilterSplittingVisitor implements FilterVisitor, ExpressionVisitor { private static final Logger logger=org.geotools.util.logging.Logging.getLogger("org.geotools.filter"); /** * The stack holding the bits of the filter that are not processable * by something with the given {@link FilterCapabilities} */ private Stack postStack = new Stack(); /** * The stack holding the bits of the filter that <b>are</b> processable * by something with the given {@link FilterCapabilities} */ private Stack preStack = new Stack(); /** * Operates similar to postStack. When a update is determined to affect an attribute expression the update * filter is pushed on to the stack, then ored with the filter that contains the expression. */ private Set changedStack=new HashSet(); /** * The given filterCapabilities that we're splitting on. */ private FilterCapabilities fcs = null; private SimpleFeatureType parent = null; private Filter original = null; /** * If we're in the middle of a client-side transaction, this object * will help us figure out what we need to handle from updates/deletes * that we're tracking client-side. */ private ClientTransactionAccessor transactionAccessor; private FilterFactory ff = CommonFactoryFinder.getFilterFactory(null); private PostPreProcessFilterSplittingVisitor() { // do nothing } /** * Create a new instance. * @param fcs The FilterCapabilties that describes what Filters/Expressions the server can process. * @param parent The FeatureType that this filter involves. Why is this needed? * @param transactionAccessor If the transaction is handled on the client and not the server then different filters * must be sent to the server. This class provides a generic way of obtaining the information from the transaction. */ public PostPreProcessFilterSplittingVisitor(FilterCapabilities fcs, SimpleFeatureType parent, ClientTransactionAccessor transactionAccessor) { this.fcs = fcs; this.parent = parent; this.transactionAccessor = transactionAccessor; } /** * Gets the filter that cannot be sent to the server and must be post-processed on the client by geotools. * * @return the filter that cannot be sent to the server and must be post-processed on the client by geotools. */ public Filter getFilterPost() { if (!changedStack.isEmpty()) // Return the original filter to ensure that // correct features are filtered return original ; if (postStack.size() > 1) { logger.warning("Too many post stack items after run: " + postStack.size()); } // JE: Changed to peek because get implies that the value can be retrieved multiple times Filter f = postStack.isEmpty() ? Filter.INCLUDE : (Filter) postStack.peek(); return f; } /** * Gets the filter that can be sent to the server for pre-processing. * * @return the filter that can be sent to the server for pre-processing. */ public Filter getFilterPre() { FilterFactory ff = CommonFactoryFinder.getFilterFactory(null); if (preStack.isEmpty()) { return Filter.INCLUDE; } if (preStack.size() > 1) { logger.warning("Too many pre stack items after run: " + preStack.size()); } // JE: Changed to peek because get implies that the value can be retrieved multiple times Filter f = preStack.isEmpty() ? Filter.INCLUDE : (Filter) preStack.peek(); // deal with deletes here !!! if(transactionAccessor != null){ if(f != null && f!=Filter.EXCLUDE){ Filter deleteFilter = transactionAccessor.getDeleteFilter(); if( deleteFilter!=null ){ if (deleteFilter == Filter.EXCLUDE) f=Filter.EXCLUDE; else f=ff.and(f,ff.not(deleteFilter)); } } } if( changedStack.isEmpty()) return f; Iterator iter=changedStack.iterator(); Filter updateFilter=(Filter) iter.next(); while( iter.hasNext() ){ Filter next=(Filter) iter.next(); if( next == Filter.INCLUDE){ updateFilter=next; break; }else{ updateFilter=(Filter) ff.or(updateFilter,next); } } if( updateFilter == Filter.INCLUDE || f==Filter.INCLUDE ) return Filter.INCLUDE; return ff.or(f,updateFilter); } /** * @see FilterVisitor#visit(IncludeFilter, Object) * * @param filter the {@link Filter} to visit */ public void visit( IncludeFilter filter ) { return; } /** * @see FilterVisitor#visit(ExcludeFilter, Object) * * @param filter the {@link Filter} to visit */ public void visit( ExcludeFilter filter ) { if (fcs.supports(Filter.EXCLUDE)) { preStack.push(filter); } else { postStack.push(filter); } } /** * @see FilterVisitor#visit(PropertyIsBetween, Object) * * NOTE: This method is extra documented as an example of how * all the other methods are implemented. If you want to know how this * class works read this method first! * * @param filter the {@link Filter} to visit */ public Object visit(PropertyIsBetween filter, Object extradata) { if( original==null ) original=filter; // Do we support this filter type at all? if (fcs.supports(PropertyIsBetween.class)) { //Yes, we do. Now, can we support the sub-filters? //first, remember how big the current list of "I can't support these" // filters is. int i = postStack.size(); Expression lowerBound = filter.getLowerBoundary(); Expression expr = filter.getExpression(); Expression upperBound = filter.getUpperBoundary(); if(lowerBound==null || upperBound==null || expr==null ){ //Well, one of the boundaries is null, so I guess // we're saying that *no* datastore could support this. postStack.push(filter); return null; } //Ok, here's the magic. We know how big our list of "can't support" //filters is. Now we send off the lowerBound Expression to see if //it can be supported. lowerBound.accept(this, null); //Now we're back, and we check. Did the postStack get bigger? if (i < postStack.size()) { //Yes, it did. Well, that means we can't support //this particular filter. Let's back out anything that was //added by the lowerBound.accept() and add ourselves. postStack.pop(); //lowerBound.accept()'s bum filter postStack.push(filter); return null; } //Aha! The postStack didn't get any bigger, so we're still //all good. Now try again with the middle expression itself... expr.accept(this, null); //Did postStack get bigger? if (i < postStack.size()) { //Yes, it did. So that means we can't support //this particular filter. We need to back out what we've //done, which is BOTH the lowerbounds filter *and* the //thing that was added by expr.accept() when it failed. preStack.pop(); // lowerBound.accept()'s success postStack.pop(); // expr.accept()'s bum filter postStack.push(filter); return null; } //Same deal again... upperBound.accept(this, null); if (i < postStack.size()) { // post process it postStack.pop(); // upperBound.accept()'s bum filter preStack.pop(); // expr.accept()'s success preStack.pop(); // lowerBound.accept()'s success postStack.push(filter); return null; } //Well, by getting here it means that postStack didn't get //taller, even after accepting all three middle filters. This //means that this whole filter is totally pre-filterable. //Let's clean up the pre-stack (which got one added to it //for the success at each of the three above .accept() calls) //and add us to the stack. preStack.pop(); // upperBounds.accept()'s success preStack.pop(); // expr.accept()'s success preStack.pop(); // lowerBounds.accept()'s success //finally we add ourselves to the "can be pre-proccessed" filter //stack. Now when we return we've added exactly one thing to //the preStack...namely, the given filter. preStack.push(filter); } else { // No, we don't support this filter. // So we push it onto the postStack, saying // "Hey, here's one more filter that we don't support. // Someone who called us may look at this and say, // "Hmm, I called accept() on this filter and now // the postStack is taller than it was...I guess this // filter wasn't accepted. postStack.push(filter); } return null; } public Object visit(PropertyIsEqualTo filter, Object notUsed) { visitBinaryComparisonOperator(filter); return null; } public Object visit(PropertyIsGreaterThan filter, Object notUsed) { visitBinaryComparisonOperator(filter); return null; } public Object visit(PropertyIsGreaterThanOrEqualTo filter, Object notUsed) { visitBinaryComparisonOperator(filter); return null; } public Object visit(PropertyIsLessThan filter, Object notUsed) { visitBinaryComparisonOperator(filter); return null; } public Object visit(PropertyIsLessThanOrEqualTo filter, Object notUsed) { visitBinaryComparisonOperator(filter); return null; } public Object visit(PropertyIsNotEqualTo filter, Object notUsed) { visitBinaryComparisonOperator(filter); return null; } private void visitBinaryComparisonOperator(BinaryComparisonOperator filter) { if( original==null ) original=filter; // supports it as a group -- no need to check the type if (!fcs.supports(FilterCapabilities.SIMPLE_COMPARISONS_OPENGIS) ) { postStack.push(filter); return; } int i = postStack.size(); Expression leftValue = filter.getExpression1(); Expression rightValue = filter.getExpression2(); if( leftValue==null || rightValue==null ){ postStack.push(filter); return; } leftValue.accept(this, null); if (i < postStack.size()) { postStack.pop(); postStack.push(filter); return; } rightValue.accept(this, null); if (i < postStack.size()) { preStack.pop(); // left postStack.pop(); postStack.push(filter); return; } preStack.pop(); // left side preStack.pop(); // right side preStack.push(filter); } public Object visit(BBOX filter, Object notUsed) { if (!fcs.supports(BBOX.class)) { postStack.push(filter); } else { preStack.push(filter); } return null; } public Object visit(Beyond filter, Object notUsed) { visitBinarySpatialOperator(filter); return null; } public Object visit(Contains filter, Object notUsed) { visitBinarySpatialOperator(filter); return null; } public Object visit(Crosses filter, Object notUsed) { visitBinarySpatialOperator(filter); return null; } public Object visit(Disjoint filter, Object notUsed) { visitBinarySpatialOperator(filter); return null; } public Object visit(DWithin filter, Object notUsed) { visitBinarySpatialOperator(filter); return null; } public Object visit(Equals filter, Object notUsed) { visitBinarySpatialOperator(filter); return null; } public Object visit(Intersects filter, Object notUsed) { visitBinarySpatialOperator(filter); return null; } public Object visit(Overlaps filter, Object notUsed) { visitBinarySpatialOperator(filter); return null; } public Object visit(Touches filter, Object notUsed) { visitBinarySpatialOperator(filter); return null; } public Object visit(Within filter, Object notUsed) { visitBinarySpatialOperator(filter); return null; } private void visitBinarySpatialOperator(BinarySpatialOperator filter) { if( original==null ) original=filter; Class [] spatialOps = new Class[] { Beyond.class, Contains.class, Crosses.class, Disjoint.class, DWithin.class, Equals.class, Intersects.class, Overlaps.class, Touches.class, Within.class }; for (int i = 0; i < spatialOps.length; i++) { if (spatialOps[i].isAssignableFrom(filter.getClass())) { if (!fcs.supports(spatialOps[i])) { postStack.push(filter); return; } else { //fcs supports this filter, no need to check the rest break; } } } // TODO check against tranasaction ? int i = postStack.size(); Expression leftGeometry, rightGeometry; leftGeometry = ((BinarySpatialOperator)filter).getExpression1(); rightGeometry = ((BinarySpatialOperator)filter).getExpression2(); if( leftGeometry==null || rightGeometry==null ){ postStack.push(filter); return; } leftGeometry.accept(this,null); if (i < postStack.size()) { postStack.pop(); postStack.push(filter); return; } rightGeometry.accept(this,null); if (i < postStack.size()) { preStack.pop(); // left postStack.pop(); postStack.push(filter); return; } preStack.pop(); // left side preStack.pop(); // right side preStack.push(filter); } public Object visit(PropertyIsLike filter, Object notUsed) { if( original==null ) original=filter; if (!fcs.supports(PropertyIsLike.class) ) { postStack.push(filter); return null; } int i = postStack.size(); filter.getExpression().accept(this, null); if (i < postStack.size()) { postStack.pop(); postStack.push(filter); return null; } preStack.pop(); // value preStack.push(filter); return null; } public Object visit(And filter, Object notUsed) { visitLogicOperator(filter, And.class); return null; } public Object visit(Not filter, Object notUsed) { visitLogicOperator(filter, Not.class); return null; } public Object visit(Or filter, Object notUsed) { visitLogicOperator(filter, Or.class); return null; } private void visitLogicOperator(Filter filter, Class filterInterface) { if( original==null ) original=filter; if (!fcs.supports(filterInterface)) { postStack.push(filter); return; } int i = postStack.size(); int j = preStack.size(); if (filter instanceof Not) { if (((Not)filter).getFilter() != null) { Filter next = ((Not)filter).getFilter(); next.accept(this, null); if (i < postStack.size()) { // since and can split filter into both pre and post parts // the parts have to be combined since ~(A^B) == ~A | ~B // combining is easy since filter==combined result however both post and pre stacks // must be cleared since both may have components of the filter popToSize(postStack,i); popToSize(preStack, j); postStack.push(filter); }else{ popToSize(preStack,j); preStack.push(filter); } } } else { if (filter instanceof Or) { Filter orReplacement; try { orReplacement = translateOr((Or)filter); orReplacement.accept(this, null); } catch (IllegalFilterException e) { popToSize(preStack,j); postStack.push(filter); return; } if( postStack.size()>i ){ popToSize(postStack,i); postStack.push(filter); return; } preStack.pop(); preStack.push(filter); } else { // it's an AND Iterator it = ((And)filter).getChildren().iterator(); while (it.hasNext()) { Filter next = (Filter) it.next(); next.accept(this, null); } //combine the unsupported and add to the top if (i < postStack.size()) { if (filter instanceof And) { Filter f = (Filter) postStack.pop(); while (postStack.size() > i) f= ff.and(f, (Filter) postStack.pop()); postStack.push(f); if(j<preStack.size()){ f = (Filter)preStack.pop(); while (preStack.size() > j) f=ff.and(f, (Filter) preStack.pop()); preStack.push(f); } } else { logger.warning( "LogicFilter found which is not 'and, or, not"); popToSize(postStack, i); popToSize(preStack, j); postStack.push(filter); } } else { popToSize(preStack,j); preStack.push(filter); } } } } private void popToSize(Stack stack, int j) { while( j<stack.size() ){ stack.pop(); } } public Object visitNullFilter(Object notUsed) { return null; } public Object visit(IncludeFilter filter, Object notUsed) { return null; } public Object visit(ExcludeFilter filter, Object notUsed) { if (fcs.supports(Filter.EXCLUDE)) { preStack.push(filter); } else { postStack.push(filter); } return null; } public Object visit(PropertyIsNull filter, Object notUsed) { if( original==null ) original=filter; if (!fcs.supports( PropertyIsNull.class) ) { postStack.push(filter); return null; } int i = postStack.size(); ((PropertyIsNull)filter).getExpression().accept(this, null); if (i < postStack.size()) { postStack.pop(); postStack.push(filter); } preStack.pop(); // null preStack.push(filter); return null; } public Object visit(Id filter, Object notUsed) { if( original==null ) original=filter; // figure out how to check that this is top level. // otherwise this is fine if (!postStack.isEmpty()) { postStack.push(filter); } preStack.push(filter); return null; } public Object visit(PropertyName expression, Object notUsed) { //JD: use an expression to get at the attribute type intead of accessing directly if (parent != null && expression.evaluate( parent ) == null ) { throw new IllegalArgumentException("Property '" + expression.getPropertyName() + "' could not be found in " + parent.getTypeName()); } if(transactionAccessor!=null){ Filter updateFilter= (Filter) transactionAccessor.getUpdateFilter(expression.getPropertyName()); if( updateFilter!=null ){ changedStack.add(updateFilter); preStack.push(updateFilter); }else preStack.push(expression); } else{ preStack.push(expression); } return null; } public Object visit(Literal expression, Object notUsed) { if (expression.getValue() == null) { postStack.push(expression); } preStack.push(expression); return null; } public Object visit(Add filter, Object notUsed) { visitMathExpression(filter); return null; } public Object visit(Divide filter, Object notUsed) { visitMathExpression(filter); return null; } public Object visit(Multiply filter, Object notUsed) { visitMathExpression(filter); return null; } public Object visit(Subtract filter, Object notUsed) { visitMathExpression(filter); return null; } private void visitMathExpression(BinaryExpression expression) { if (!fcs.supports(Add.class) && !fcs.supports(Subtract.class) && !fcs.supports(Multiply.class) && !fcs.supports(Divide.class)) { postStack.push(expression); return; } int i = postStack.size(); Expression leftValue = expression.getExpression1(); Expression rightValue = expression.getExpression2(); if( leftValue==null || rightValue==null ){ postStack.push(expression); return; } leftValue.accept(this, null); if (i < postStack.size()) { postStack.pop(); postStack.push(expression); return; } rightValue.accept(this, null); if (i < postStack.size()) { preStack.pop(); // left postStack.pop(); postStack.push(expression); return; } preStack.pop(); // left side preStack.pop(); // right side preStack.push(expression); } /** * * @see org.geotools.filter.FilterVisitor#visit(org.geotools.filter.FunctionExpression) */ public Object visit(Function expression, Object notUsed) { if (!fcs.supports(expression.getClass()) ) { postStack.push(expression); return null; } if (expression.getName() == null) { postStack.push(expression); return null; } int i = postStack.size(); int j = preStack.size(); for (int k = 0; k < expression.getParameters().size(); k++) { ((Expression)expression.getParameters().get(i)).accept(this, null); if (i < postStack.size()) { while(j<preStack.size()) preStack.pop(); postStack.pop(); postStack.push(expression); return null; } } while(j<preStack.size()) preStack.pop(); preStack.push(expression); return null; } public Object visit(NilExpression nilExpression, Object notUsed) { postStack.push(nilExpression); return null; } private Filter translateOr(Or filter) throws IllegalFilterException { if (!(filter instanceof Or)) { return filter; } // a|b == ~~(a|b) negative introduction // ~(a|b) == (~a + ~b) modus ponens // ~~(a|b) == ~(~a + ~b) substitution // a|b == ~(~a + ~b) negative simpilification Iterator i = filter.getChildren().iterator(); List translated = new ArrayList(); while (i.hasNext()) { Filter f = (Filter) i.next(); if (f instanceof Not) { // simplify it Not logic = (Not) f; Filter next = logic.getFilter(); translated.add(next); } else { translated.add(ff.not(f)); } } Filter and = ff.and(translated); return ff.not(and); } }