diff --git a/deegree-core/deegree-core-base/src/main/java/org/deegree/filter/Filters.java b/deegree-core/deegree-core-base/src/main/java/org/deegree/filter/Filters.java index 33bcc281b4..f08291da8b 100644 --- a/deegree-core/deegree-core-base/src/main/java/org/deegree/filter/Filters.java +++ b/deegree-core/deegree-core-base/src/main/java/org/deegree/filter/Filters.java @@ -35,7 +35,9 @@ ----------------------------------------------------------------------------*/ package org.deegree.filter; +import static java.util.Arrays.asList; import static org.deegree.filter.Filter.Type.OPERATOR_FILTER; +import static org.deegree.filter.Operator.Type.LOGICAL; import java.util.Arrays; import java.util.Collections; @@ -117,6 +119,26 @@ public class Filters { private static Logger LOG = LoggerFactory.getLogger( Filters.class ); + /** + * Extract all operators from an And-filter (which can all be filtered individually, as each operator can only + * narrow the result). + * + * @return list of operators, can be null (not an And-filter) + */ + public static List extractAndOperands( final Filter filter ) { + if ( filter != null && filter.getType() == OPERATOR_FILTER ) { + final OperatorFilter operatorFilter = (OperatorFilter) filter; + final Operator operator = operatorFilter.getOperator(); + if ( operator.getType() == LOGICAL ) { + final LogicalOperator logicalOperator = (LogicalOperator) operator; + if ( logicalOperator.getSubType() == LogicalOperator.SubType.AND ) { + return asList( logicalOperator.getParams() ); + } + } + } + return null; + } + /** * Tries to extract a {@link BBOX} constraint from the given {@link Filter} that can be used as a pre-filtering step * to narrow the result set. diff --git a/deegree-core/deegree-core-sqldialect/deegree-sqldialect-commons/src/main/java/org/deegree/sqldialect/filter/AbstractWhereBuilder.java b/deegree-core/deegree-core-sqldialect/deegree-sqldialect-commons/src/main/java/org/deegree/sqldialect/filter/AbstractWhereBuilder.java index dac80c144b..88a5e1f1e1 100644 --- a/deegree-core/deegree-core-sqldialect/deegree-sqldialect-commons/src/main/java/org/deegree/sqldialect/filter/AbstractWhereBuilder.java +++ b/deegree-core/deegree-core-sqldialect/deegree-sqldialect-commons/src/main/java/org/deegree/sqldialect/filter/AbstractWhereBuilder.java @@ -38,6 +38,7 @@ import static java.sql.Types.BOOLEAN; import static org.deegree.commons.tom.primitive.BaseType.STRING; import static org.deegree.commons.xml.CommonNamespaces.XSINS; +import static org.deegree.filter.Filters.extractAndOperands; import static org.deegree.filter.Filters.extractPrefilterBBoxConstraint; import java.sql.ResultSet; @@ -71,6 +72,7 @@ import org.deegree.filter.expression.Function; import org.deegree.filter.expression.Literal; import org.deegree.filter.expression.ValueReference; +import org.deegree.filter.logical.And; import org.deegree.filter.logical.LogicalOperator; import org.deegree.filter.logical.Not; import org.deegree.filter.sort.SortProperty; @@ -101,16 +103,8 @@ * {@link #getPostFilter()}/{@link #getPostSortCriteria()} return not null and the objects extracted from * the corresponding {@link ResultSet} must be filtered/sorted in memory to guarantee the requested constraints/order. *

- *

- * TODO: Implement partial backend filtering / sorting. Currently, filtering / sorting is performed completely by the - * database or by the post filter / criteria (if any property name has been encountered that could not be - * mapped). - *

* * @author Markus Schneider - * @author last edited by: $Author: mschneider $ - * - * @version $Revision: 31370 $, $Date: 2011-07-28 19:37:13 +0200 (Do, 28. Jul 2011) $ */ public abstract class AbstractWhereBuilder { @@ -125,7 +119,7 @@ public abstract class AbstractWhereBuilder { protected final SortProperty[] sortCrit; /** Keeps track of all generated table aliases. */ - protected final TableAliasManager aliasManager = new TableAliasManager(); + protected TableAliasManager aliasManager = new TableAliasManager(); /** Keeps track of all successfully mapped property names. */ protected final List propNameMappingList = new ArrayList(); @@ -161,37 +155,54 @@ protected AbstractWhereBuilder( SQLDialect dialect, PropertyNameMapper mapper, O } /** - * Invokes the building of the internal variables that store filter and sort criteria. + * Builds internal variables that store filter and sort criteria. * - * @param allowPartialMappings - * if false, any unmappable expression will cause an {@link UnmappableException} to be thrown + * @param allowPostFiltering + * if false, any unmappable expression will cause an {@link UnmappableException} to be + * thrown * @throws FilterEvaluationException * @throws UnmappableException * if allowPartialMappings is false and an expression could not be mapped to the db */ - protected void build( boolean allowPartialMappings ) + protected void build( final boolean allowPostFiltering ) throws FilterEvaluationException, UnmappableException { if ( filter != null ) { try { whereClause = toProtoSQL( filter.getOperator() ); } catch ( UnmappableException e ) { - if ( !allowPartialMappings ) { + LOG.trace( "Stack trace:", e ); + if ( !allowPostFiltering ) { throw e; } - LOG.debug( "Unable to map full filter to WHERE-clause. Trying mapping of bbox constraint only." ); - LOG.trace( "Stack trace:", e ); - - BBOX preFilterBBox = extractPrefilterBBoxConstraint( filter ); - if ( preFilterBBox != null ) { - try { - whereClause = toProtoSQL( preFilterBBox ); - } catch ( UnmappableException e2 ) { - LOG.warn( "Unable to map any filter constraints to WHERE-clause. Fallback to full memory filtering." ); - LOG.trace( "Stack trace:", e2 ); + LOG.debug( "Unable to map full filter to WHERE-clause. Separating SQL and post filter parts." ); + propNameMappingList.clear(); + aliasManager = new TableAliasManager(); + final List andOperands = extractAndOperands( filter ); + if ( andOperands == null ) { + postFilter = filter; + LOG.debug( "Not an AND filter. Using complete filter as post filter." ); + } else { + final List mappableOperands = new ArrayList(); + final List unmappableOperands = new ArrayList(); + for ( final Operator operand : andOperands ) { + if ( isMappable( operand ) ) { + mappableOperands.add( operand ); + } else { + unmappableOperands.add( operand ); + } + } + addPrefilterBBoxIfNoGeometryConstraintMapped( filter, mappableOperands ); + final OperatorFilter sqlFilter = createFilter( mappableOperands ); + if ( sqlFilter != null ) { + propNameMappingList.clear(); + aliasManager = new TableAliasManager(); + whereClause = toProtoSQL( sqlFilter.getOperator() ); } + postFilter = createFilter( unmappableOperands ); + LOG.debug( "SQL filter part: " + mappableOperands.size() + " operand(s)" ); + LOG.debug( "Post filter part: " + unmappableOperands.size() + " operand(s)" ); } - postFilter = filter; } catch ( FilterEvaluationException e ) { throw e; } catch ( RuntimeException e ) { @@ -204,7 +215,7 @@ protected void build( boolean allowPartialMappings ) try { orderByClause = toProtoSQL( sortCrit ); } catch ( UnmappableException e ) { - if ( !allowPartialMappings ) { + if ( !allowPostFiltering ) { throw e; } LOG.debug( "Unable to map sort criteria to ORDER BY-clause. Setting post order criteria.", e ); @@ -219,6 +230,37 @@ protected void build( boolean allowPartialMappings ) } } + private boolean isMappable( final Operator operand ) { + try { + toProtoSQL( operand ); + } catch ( final Exception e ) { + return false; + } + return true; + } + + private void addPrefilterBBoxIfNoGeometryConstraintMapped( final OperatorFilter filter, + final List operands ) { + final Filter mappedFilter = createFilter( operands ); + final BBOX mappedBbox = extractPrefilterBBoxConstraint( mappedFilter ); + if ( mappedBbox == null ) { + final BBOX preFilterBBox = extractPrefilterBBoxConstraint( filter ); + if ( preFilterBBox != null ) { + operands.add( preFilterBBox ); + } + } + } + + private OperatorFilter createFilter( List andOperands ) { + if ( andOperands.isEmpty() ) { + return null; + } + if ( andOperands.size() == 1 ) { + return new OperatorFilter( andOperands.get( 0 ) ); + } + return new OperatorFilter( new And( andOperands.toArray( new Operator[andOperands.size()] ) ) ); + } + /** * Returns the expression for the SQL-WHERE clause. * diff --git a/deegree-core/deegree-core-sqldialect/deegree-sqldialect-commons/src/test/java/org/deegree/sqldialect/filter/MockPropertyNameMapper.java b/deegree-core/deegree-core-sqldialect/deegree-sqldialect-commons/src/test/java/org/deegree/sqldialect/filter/MockPropertyNameMapper.java new file mode 100644 index 0000000000..1cb3555563 --- /dev/null +++ b/deegree-core/deegree-core-sqldialect/deegree-sqldialect-commons/src/test/java/org/deegree/sqldialect/filter/MockPropertyNameMapper.java @@ -0,0 +1,35 @@ +package org.deegree.sqldialect.filter; + +import org.deegree.filter.expression.ValueReference; + +/** + * Mock implementation of {@link PropertyNameMapper} that maps any {@link ValueReference} to a column with the same + * name, except for {@link ValueReference}s that start with "UNMAPPABLE_". + * + * @author Markus schneider + * + * @since 3.4 + */ +public class MockPropertyNameMapper implements PropertyNameMapper { + + @Override + public PropertyNameMapping getSpatialMapping( ValueReference propName, TableAliasManager aliasManager ) { + if ( isMappable( propName ) ) { + return new PropertyNameMapping( null, null, propName.getAsQName().getLocalPart(), null ); + } + return null; + } + + @Override + public PropertyNameMapping getMapping( ValueReference propName, TableAliasManager aliasManager ) { + if ( isMappable( propName ) ) { + return new PropertyNameMapping( null, null, propName.getAsQName().getLocalPart(), null ); + } + return null; + } + + private boolean isMappable( final ValueReference propName ) { + return !propName.getAsQName().getLocalPart().startsWith( "UNMAPPABLE" ); + } + +} diff --git a/deegree-core/deegree-core-sqldialect/deegree-sqldialect-commons/src/test/java/org/deegree/sqldialect/filter/MockWhereBuilder.java b/deegree-core/deegree-core-sqldialect/deegree-sqldialect-commons/src/test/java/org/deegree/sqldialect/filter/MockWhereBuilder.java new file mode 100644 index 0000000000..35f84a9673 --- /dev/null +++ b/deegree-core/deegree-core-sqldialect/deegree-sqldialect-commons/src/test/java/org/deegree/sqldialect/filter/MockWhereBuilder.java @@ -0,0 +1,28 @@ +package org.deegree.sqldialect.filter; + +import org.deegree.filter.FilterEvaluationException; +import org.deegree.filter.OperatorFilter; +import org.deegree.filter.sort.SortProperty; +import org.deegree.filter.spatial.SpatialOperator; +import org.deegree.sqldialect.filter.expression.SQLOperation; + +/** + * Mock implementation of {@link AbstractWhereBuilder} that uses the {@link MockPropertyNameMapper}. + * + * @author Markus schneider + * + * @since 3.4 + */ +class MockWhereBuilder extends AbstractWhereBuilder { + + MockWhereBuilder( final OperatorFilter filter, final SortProperty[] sortCrit ) throws FilterEvaluationException { + super( null, new MockPropertyNameMapper(), filter, sortCrit ); + } + + @Override + protected SQLOperation toProtoSQL( final SpatialOperator op ) + throws UnmappableException, FilterEvaluationException { + return null; + } + +} \ No newline at end of file diff --git a/deegree-core/deegree-core-sqldialect/deegree-sqldialect-commons/src/test/java/org/deegree/sqldialect/filter/WhereBuilderTest.java b/deegree-core/deegree-core-sqldialect/deegree-sqldialect-commons/src/test/java/org/deegree/sqldialect/filter/WhereBuilderTest.java new file mode 100644 index 0000000000..07968cfd31 --- /dev/null +++ b/deegree-core/deegree-core-sqldialect/deegree-sqldialect-commons/src/test/java/org/deegree/sqldialect/filter/WhereBuilderTest.java @@ -0,0 +1,80 @@ +package org.deegree.sqldialect.filter; + +import static org.deegree.filter.MatchAction.ANY; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import java.util.ArrayList; +import java.util.Collection; + +import javax.xml.namespace.QName; + +import org.deegree.commons.tom.primitive.PrimitiveValue; +import org.deegree.filter.FilterEvaluationException; +import org.deegree.filter.Operator; +import org.deegree.filter.OperatorFilter; +import org.deegree.filter.comparison.PropertyIsEqualTo; +import org.deegree.filter.expression.Literal; +import org.deegree.filter.expression.ValueReference; +import org.deegree.filter.logical.And; +import org.junit.Test; + +/** + * Tests for {@link AbstractWhereBuilder}. + * + * @author Markus Schneider + * + * @since 3.4 + */ +public class WhereBuilderTest { + + @Test + public void buildUsingFullMapping() + throws FilterEvaluationException, UnmappableException { + final OperatorFilter filter = createAndFilter( 2, 0 ); + final MockWhereBuilder wb = new MockWhereBuilder( filter, null ); + wb.build( true ); + + // full filter was mapped to SQL + final String actualSql = wb.getWhere().getSQL().toString(); + assertEquals( "(MAPPABLE_0 = ? AND MAPPABLE_1 = ?)", actualSql ); + + // post filter is null + final OperatorFilter actualPostFilter = wb.getPostFilter(); + assertNull( actualPostFilter ); + } + + @Test + public void buildUsingPartialMapping() + throws FilterEvaluationException, UnmappableException { + final OperatorFilter filter = createAndFilter( 1, 1 ); + final MockWhereBuilder wb = new MockWhereBuilder( filter, null ); + wb.build( true ); + + // one part of filter was mapped to SQL + final String actualSql = wb.getWhere().getSQL().toString(); + assertEquals( "MAPPABLE_0 = ?", actualSql ); + + // remaining part of filter was extracted as post filter + final OperatorFilter actualPostFilter = wb.getPostFilter(); + final PropertyIsEqualTo operator = (PropertyIsEqualTo) actualPostFilter.getOperator(); + final ValueReference ref = (ValueReference) operator.getParameter1(); + assertEquals( "UNMAPPABLE_0", ref.getAsQName().getLocalPart() ); + } + + private OperatorFilter createAndFilter( final int numMappedOperands, final int numUnmappedOperands ) { + final Collection operators = new ArrayList(); + for ( int i = 0; i < numMappedOperands; i++ ) { + final ValueReference param1 = new ValueReference( new QName( "MAPPABLE_" + i ) ); + final Literal param2 = new Literal( "VALUE" ); + operators.add( new PropertyIsEqualTo( param1, param2, true, ANY ) ); + } + for ( int i = 0; i < numUnmappedOperands; i++ ) { + final ValueReference param1 = new ValueReference( new QName( "UNMAPPABLE_" + i ) ); + final Literal param2 = new Literal( "VALUE" ); + operators.add( new PropertyIsEqualTo( param1, param2, true, ANY ) ); + } + final And and = new And( operators.toArray( new Operator[operators.size()] ) ); + return new OperatorFilter( and ); + } +}