diff --git a/deegree-core/deegree-core-layer/src/main/java/org/deegree/layer/LayerQuery.java b/deegree-core/deegree-core-layer/src/main/java/org/deegree/layer/LayerQuery.java index 6473a18b9d..75173a293f 100644 --- a/deegree-core/deegree-core-layer/src/main/java/org/deegree/layer/LayerQuery.java +++ b/deegree-core/deegree-core-layer/src/main/java/org/deegree/layer/LayerQuery.java @@ -36,11 +36,16 @@ package org.deegree.layer; import static java.lang.Integer.parseInt; +import static java.util.Arrays.asList; import static org.deegree.commons.utils.MapUtils.DEFAULT_PIXEL_SIZE; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Map; +import org.deegree.commons.utils.Pair; import org.deegree.filter.OperatorFilter; import org.deegree.geometry.Envelope; import org.deegree.geometry.GeometryFactory; @@ -57,6 +62,12 @@ */ public class LayerQuery { + public static final String FILTERPROPERTY = "FILTERPROPERTY"; + + public static final String FILTERVALUE = "FILTERVALUE"; + + public static final String RADIUS = "RADIUS"; + private final Envelope envelope; private final int width, height; @@ -192,7 +203,7 @@ public int getLayerRadius() { } public Envelope calcClickBox( int radius ) { - radius = parameters.get( "RADIUS" ) == null ? radius : parseInt( parameters.get( "RADIUS" ) ); + radius = parameters.get( RADIUS ) == null ? radius : parseInt( parameters.get( RADIUS ) ); GeometryFactory fac = new GeometryFactory(); double dw = envelope.getSpan0() / width; double dh = envelope.getSpan1() / height; @@ -204,4 +215,30 @@ public Envelope calcClickBox( int radius ) { envelope.getMax().get1() - ( y - r2 ) * dh }, envelope.getCoordinateSystem() ); } -} + + /** + * Returns the additional request parameters used for filtering. + * + * @return the property (first) and the values (second) to filter for, null if at least one parameter is null or empty + */ + public Pair> requestFilter() { + String filterProperty = parameters.get( FILTERPROPERTY ); + String filterValue = parameters.get( FILTERVALUE ); + if ( filterProperty == null || filterProperty.isEmpty() || filterValue == null || filterValue.isEmpty() ) + return null; + List filterValues = parseFilterValues( filterValue ); + return new Pair>( filterProperty, filterValues ); + } + + private List parseFilterValues( String filterValue ) { + List filterValues = new ArrayList(); + String[] splittedFilterValue = filterValue.split( "," ); + for ( String value : splittedFilterValue ) { + String trimmedValue = value.trim(); + if ( !trimmedValue.isEmpty() ) + filterValues.add( trimmedValue ); + } + return filterValues; + } + +} \ No newline at end of file diff --git a/deegree-core/deegree-core-layer/src/test/java/org/deegree/layer/LayerQueryTest.java b/deegree-core/deegree-core-layer/src/test/java/org/deegree/layer/LayerQueryTest.java new file mode 100644 index 0000000000..1ccc365549 --- /dev/null +++ b/deegree-core/deegree-core-layer/src/test/java/org/deegree/layer/LayerQueryTest.java @@ -0,0 +1,124 @@ +package org.deegree.layer; + +import org.deegree.commons.utils.Pair; +import org.deegree.cs.exceptions.UnknownCRSException; +import org.deegree.cs.persistence.CRSManager; +import org.deegree.geometry.Envelope; +import org.deegree.geometry.SimpleGeometryFactory; +import org.deegree.geometry.standard.DefaultEnvelope; +import org.hamcrest.CoreMatchers; +import org.junit.Assert; +import org.junit.Test; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.deegree.layer.LayerQuery.FILTERPROPERTY; +import static org.deegree.layer.LayerQuery.FILTERVALUE; +import static org.hamcrest.CoreMatchers.hasItem; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.junit.Assert.*; + +/** + * @author Lyn Goltz + */ +public class LayerQueryTest { + + @Test + public void requestFilter() + throws Exception { + String filterProperty = "type"; + String filterValue = "one"; + LayerQuery layerQuery = createLayerQuery( filterProperty, filterValue ); + + Pair> requestFilter = layerQuery.requestFilter(); + assertThat( requestFilter.getFirst(), is( filterProperty ) ); + assertThat( requestFilter.getSecond().size(), is( 1 ) ); + assertThat( requestFilter.getSecond(), hasItem( filterValue ) ); + } + + @Test + public void requestFilter_MultipleValues() + throws Exception { + String filterProperty = "type"; + String filterValue = "one, two"; + LayerQuery layerQuery = createLayerQuery( filterProperty, filterValue ); + + Pair> requestFilter = layerQuery.requestFilter(); + assertThat( requestFilter.getFirst(), is( filterProperty ) ); + assertThat( requestFilter.getSecond().size(), is( 2 ) ); + assertThat( requestFilter.getSecond(), hasItem( "one" ) ); + assertThat( requestFilter.getSecond(), hasItem( "two" ) ); + } + + @Test + public void requestFilter_MultipleValuesWithMissing() + throws Exception { + String filterProperty = "type"; + String filterValue = "one,,two"; + LayerQuery layerQuery = createLayerQuery( filterProperty, filterValue ); + + Pair> requestFilter = layerQuery.requestFilter(); + assertThat( requestFilter.getFirst(), is( filterProperty ) ); + assertThat( requestFilter.getSecond().size(), is( 2 ) ); + assertThat( requestFilter.getSecond(), hasItem( "one" ) ); + assertThat( requestFilter.getSecond(), hasItem( "two" ) ); + } + + @Test + public void requestFilter_NullValue() + throws Exception { + String filterProperty = null; + String filterValue = "one, two"; + LayerQuery layerQuery = createLayerQuery( filterProperty, filterValue ); + + Pair> requestFilter = layerQuery.requestFilter(); + assertThat( requestFilter, is( nullValue() ) ); + } + + @Test + public void requestFilter_NullProperty() + throws Exception { + String filterProperty = "type"; + String filterValue = null; + LayerQuery layerQuery = createLayerQuery( filterProperty, filterValue ); + + Pair> requestFilter = layerQuery.requestFilter(); + assertThat( requestFilter, is( nullValue() ) ); + } + + @Test + public void requestFilter_EmptyValue() + throws Exception { + String filterProperty = ""; + String filterValue = "one, two"; + LayerQuery layerQuery = createLayerQuery( filterProperty, filterValue ); + + Pair> requestFilter = layerQuery.requestFilter(); + assertThat( requestFilter, is( nullValue() ) ); + } + + @Test + public void requestFilter_EMptyProperty() + throws Exception { + String filterProperty = "type"; + String filterValue = ""; + LayerQuery layerQuery = createLayerQuery( filterProperty, filterValue ); + + Pair> requestFilter = layerQuery.requestFilter(); + assertThat( requestFilter, is( nullValue() ) ); + } + + private LayerQuery createLayerQuery( String filterProperty, String filterValue ) + throws UnknownCRSException { + Map parameters = new HashMap(); + parameters.put( FILTERPROPERTY, filterProperty ); + parameters.put( FILTERVALUE, filterValue ); + Envelope envelope = new SimpleGeometryFactory().createEnvelope( 5, 12, 6, 11, + CRSManager.lookup( "EPSG:4326" ) ); + return new LayerQuery( envelope, 300, 200, 1, 2, 3, null, null, parameters, null, null, null, 1 ); + } + +} \ No newline at end of file diff --git a/deegree-layers/deegree-layers-feature/pom.xml b/deegree-layers/deegree-layers-feature/pom.xml index fd6e865b41..67ca02c25e 100644 --- a/deegree-layers/deegree-layers-feature/pom.xml +++ b/deegree-layers/deegree-layers-feature/pom.xml @@ -55,6 +55,14 @@ deegree-featurestore-commons ${project.version} + + junit + junit + + + org.mockito + mockito-core + diff --git a/deegree-layers/deegree-layers-feature/src/main/java/org/deegree/layer/persistence/feature/FeatureLayer.java b/deegree-layers/deegree-layers-feature/src/main/java/org/deegree/layer/persistence/feature/FeatureLayer.java index 488c49531d..c0973e40a3 100644 --- a/deegree-layers/deegree-layers-feature/src/main/java/org/deegree/layer/persistence/feature/FeatureLayer.java +++ b/deegree-layers/deegree-layers-feature/src/main/java/org/deegree/layer/persistence/feature/FeatureLayer.java @@ -40,17 +40,6 @@ ----------------------------------------------------------------------------*/ package org.deegree.layer.persistence.feature; -import static org.deegree.filter.Filters.addBBoxConstraint; -import static org.deegree.layer.persistence.feature.FilterBuilder.buildFilterForMap; -import static org.deegree.style.utils.Styles.getStyleFilters; -import static org.slf4j.LoggerFactory.getLogger; - -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import javax.xml.namespace.QName; - import org.deegree.commons.ows.exception.OWSException; import org.deegree.feature.persistence.FeatureStore; import org.deegree.feature.persistence.query.Query; @@ -69,6 +58,16 @@ import org.deegree.style.utils.Styles; import org.slf4j.Logger; +import javax.xml.namespace.QName; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.deegree.filter.Filters.addBBoxConstraint; +import static org.deegree.layer.persistence.feature.FilterBuilder.buildFilterForMap; +import static org.deegree.style.utils.Styles.getStyleFilters; +import static org.slf4j.LoggerFactory.getLogger; + /** * * @author Andreas Schmitz @@ -129,13 +128,16 @@ public FeatureLayerData mapQuery( final LayerQuery query, List headers ) geomProp = null; } + QName ftName = featureType == null ? style.getFeatureType() : featureType; if ( ftName != null && featureStore.getSchema().getFeatureType( ftName ) == null ) { LOG.warn( "FeatureType '" + ftName + "' is not known to the FeatureStore." ); return null; } - filter = Filters.repair( filter, AppSchemas.collectProperyNames( featureStore.getSchema(), ftName ) ); + Set propertyNames = AppSchemas.collectProperyNames( featureStore.getSchema(), ftName ); + filter = FilterBuilder.appendRequestFilter( filter, query, propertyNames ); + filter = Filters.repair( filter, propertyNames ); QueryBuilder builder = new QueryBuilder( featureStore, filter, ftName, bbox, query, geomProp, sortBy, getMetadata().getName() ); diff --git a/deegree-layers/deegree-layers-feature/src/main/java/org/deegree/layer/persistence/feature/FilterBuilder.java b/deegree-layers/deegree-layers-feature/src/main/java/org/deegree/layer/persistence/feature/FilterBuilder.java index a510e26a48..b831bb8af6 100644 --- a/deegree-layers/deegree-layers-feature/src/main/java/org/deegree/layer/persistence/feature/FilterBuilder.java +++ b/deegree-layers/deegree-layers-feature/src/main/java/org/deegree/layer/persistence/feature/FilterBuilder.java @@ -41,19 +41,20 @@ Occam Labs UG (haftungsbeschränkt) ----------------------------------------------------------------------------*/ package org.deegree.layer.persistence.feature; -import static org.deegree.feature.types.property.GeometryPropertyType.CoordinateDimension.DIM_2; -import static org.deegree.feature.types.property.GeometryPropertyType.CoordinateDimension.DIM_2_OR_3; - -import java.util.LinkedList; -import java.util.List; - import org.deegree.commons.ows.exception.OWSException; import org.deegree.commons.tom.gml.property.PropertyType; +import org.deegree.commons.tom.primitive.PrimitiveValue; +import org.deegree.commons.utils.Pair; import org.deegree.feature.types.FeatureType; import org.deegree.feature.types.property.GeometryPropertyType; +import org.deegree.filter.Expression; import org.deegree.filter.Filters; +import org.deegree.filter.MatchAction; import org.deegree.filter.Operator; import org.deegree.filter.OperatorFilter; +import org.deegree.filter.comparison.ComparisonOperator; +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.deegree.filter.logical.Or; @@ -63,24 +64,35 @@ Occam Labs UG (haftungsbeschränkt) import org.deegree.style.se.unevaluated.Style; import org.deegree.style.utils.Styles; +import javax.xml.namespace.QName; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import static org.deegree.feature.types.property.GeometryPropertyType.CoordinateDimension.DIM_2; +import static org.deegree.feature.types.property.GeometryPropertyType.CoordinateDimension.DIM_2_OR_3; + /** * Responsible for building feature layer filters. - * + * * @author Andreas Schmitz * @author last edited by: $Author: stranger $ - * * @version $Revision: $, $Date: $ */ class FilterBuilder { - static OperatorFilter buildFilterForMap(OperatorFilter filter, Style style, LayerQuery query, DimensionFilterBuilder dimFilterBuilder, List headers) throws OWSException{ + static OperatorFilter buildFilterForMap( OperatorFilter filter, Style style, LayerQuery query, + DimensionFilterBuilder dimFilterBuilder, List headers ) + throws OWSException { style = style.filter( query.getScale() ); filter = Filters.and( filter, Styles.getStyleFilters( style, query.getScale() ) ); filter = Filters.and( filter, query.getFilter() ); filter = Filters.and( filter, dimFilterBuilder.getDimensionFilter( query.getDimensions(), headers ) ); return filter; } - + static OperatorFilter buildFilter( Operator operator, FeatureType ft, Envelope clickBox ) { if ( ft == null ) { if ( operator == null ) { @@ -113,11 +125,62 @@ private static LinkedList findOperators( FeatureType ft, Envelope clic LinkedList list = new LinkedList(); for ( PropertyType pt : ft.getPropertyDeclarations() ) { if ( pt instanceof GeometryPropertyType - && ( ( (GeometryPropertyType) pt ).getCoordinateDimension() == DIM_2 || ( (GeometryPropertyType) pt ).getCoordinateDimension() == DIM_2_OR_3 ) ) { + && ( ( (GeometryPropertyType) pt ).getCoordinateDimension() == DIM_2 || + ( (GeometryPropertyType) pt ).getCoordinateDimension() == DIM_2_OR_3 ) ) { list.add( new Intersects( new ValueReference( pt.getName() ), clickBox ) ); } } return list; } -} + static OperatorFilter appendRequestFilter( OperatorFilter filter, LayerQuery query, Set propertyNames ) { + OperatorFilter requestFilter = buildRequestFilter( query, propertyNames ); + return Filters.and( filter, requestFilter ); + } + + static OperatorFilter buildRequestFilter( LayerQuery layerQuery, Set propertyNames ) { + Pair> requestFilter = layerQuery.requestFilter(); + if ( requestFilter == null ) + return null; + + List operators = createOperatorsIfPropertyIsKnown( requestFilter, propertyNames ); + if ( operators.isEmpty() ) + return null; + if ( operators.size() == 1 ) + return new OperatorFilter( operators.get( 0 ) ); + return new OperatorFilter( new Or( operators.toArray( new Operator[operators.size()] ) ) ); + } + + private static List createOperatorsIfPropertyIsKnown( Pair> requestFilter, + Set propertyNames ) { + String filterProperty = requestFilter.getFirst(); + if ( propertyIsknown( filterProperty, propertyNames ) ) { + return createOperatorsIfPropertyIsKnown( requestFilter, filterProperty ); + } + return Collections.emptyList(); + } + + private static List createOperatorsIfPropertyIsKnown( Pair> requestFilter, + String filterProperty ) { + List operators = new ArrayList(); + List filterValues = requestFilter.getSecond(); + for ( String filterValue : filterValues ) { + Expression filterPropertyExpression = new ValueReference( new QName( filterProperty ) ); + Expression filterValueExpression = new Literal( filterValue ); + PropertyIsEqualTo isEqualTo = new PropertyIsEqualTo( filterPropertyExpression, + filterValueExpression, false, + MatchAction.ALL ); + operators.add( isEqualTo ); + } + return operators; + } + + private static boolean propertyIsknown( String filterProperty, Set propertyNames ) { + for ( QName propertyName : propertyNames ) { + if ( filterProperty.equals( propertyName.getLocalPart() ) ) + return true; + } + return false; + } + +} \ No newline at end of file diff --git a/deegree-layers/deegree-layers-feature/src/test/java/org/deegree/layer/persistence/feature/FilterBuilderTest.java b/deegree-layers/deegree-layers-feature/src/test/java/org/deegree/layer/persistence/feature/FilterBuilderTest.java new file mode 100644 index 0000000000..89516aaf82 --- /dev/null +++ b/deegree-layers/deegree-layers-feature/src/test/java/org/deegree/layer/persistence/feature/FilterBuilderTest.java @@ -0,0 +1,98 @@ +package org.deegree.layer.persistence.feature; + +import org.deegree.commons.utils.Pair; +import org.deegree.filter.OperatorFilter; +import org.deegree.filter.comparison.PropertyIsEqualTo; +import org.deegree.filter.logical.Or; +import org.deegree.layer.LayerQuery; +import org.junit.Test; +import org.mockito.Mockito; + +import javax.xml.namespace.QName; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.junit.Assert.assertThat; + +/** + * @author Lyn Goltz + */ +public class FilterBuilderTest { + + @Test + public void testBuildRequestFilter() { + String filterProperty = "type"; + List filterValues = Collections.singletonList( "one" ); + + LayerQuery layerQuery = mockLayerQuery( filterProperty, filterValues ); + Set propertyNames = createPropertyNames( filterProperty ); + OperatorFilter operatorFilter = FilterBuilder.buildRequestFilter( layerQuery, propertyNames ); + + assertThat( operatorFilter.getOperator(), instanceOf( PropertyIsEqualTo.class ) ); + } + + @Test + public void testBuildRequestFilter_MultipleValues() { + String filterProperty = "type"; + List filterValues = Arrays.asList( new String[] { "one", "two" } ); + LayerQuery layerQuery = mockLayerQuery( filterProperty, filterValues ); + Set propertyNames = createPropertyNames( filterProperty ); + OperatorFilter operatorFilter = FilterBuilder.buildRequestFilter( layerQuery, propertyNames ); + + assertThat( operatorFilter.getOperator(), instanceOf( Or.class ) ); + } + + @Test + public void testBuildRequestFilter_EmptyValues() { + String filterProperty = "type"; + List filterValues = Collections.emptyList(); + LayerQuery layerQuery = mockLayerQuery( filterProperty, filterValues ); + Set propertyNames = createPropertyNames( filterProperty ); + OperatorFilter operatorFilter = FilterBuilder.buildRequestFilter( layerQuery, propertyNames ); + + assertThat( operatorFilter, is( nullValue() ) ); + } + + @Test + public void testBuildRequestFilter_NullRequestFilter() { + LayerQuery layerQuery = mockLayerQuery( null ); + Set propertyNames = createPropertyNames( "abc" ); + OperatorFilter operatorFilter = FilterBuilder.buildRequestFilter( layerQuery, propertyNames ); + + assertThat( operatorFilter, is( nullValue() ) ); + } + + @Test + public void testBuildRequestFilter_PropertyNotKnown() { + String filterProperty = "type"; + List filterValues = Collections.singletonList( "one" ); + + LayerQuery layerQuery = mockLayerQuery( filterProperty, filterValues ); + Set propertyNames = createPropertyNames( "anothertype" ); + OperatorFilter operatorFilter = FilterBuilder.buildRequestFilter( layerQuery, propertyNames ); + + assertThat( operatorFilter, is( nullValue() ) ); + } + + private LayerQuery mockLayerQuery( String filterProperty, List filterValues ) { + Pair> requestFilter = new Pair>( filterProperty, filterValues ); + + return mockLayerQuery( requestFilter ); + } + + private LayerQuery mockLayerQuery( Pair> requestFilter ) { + LayerQuery layerQueryMock = Mockito.mock( LayerQuery.class ); + Mockito.when( layerQueryMock.requestFilter() ).thenReturn( requestFilter ); + return layerQueryMock; + } + + private Set createPropertyNames( String filterProperty ) { + return Collections.singleton( new QName( filterProperty ) ); + } + +} \ No newline at end of file diff --git a/deegree-services/deegree-webservices-handbook/src/main/asciidoc/webservices.adoc b/deegree-services/deegree-webservices-handbook/src/main/asciidoc/webservices.adoc index 699f33a801..78817a9107 100644 --- a/deegree-services/deegree-webservices-handbook/src/main/asciidoc/webservices.adoc +++ b/deegree-services/deegree-webservices-handbook/src/main/asciidoc/webservices.adoc @@ -1460,6 +1460,9 @@ actual extent returned will not be changed!). Use values like 1.1 to enlarge the envelope by 5% in each direction (this would be 10% in total). +With the two vendorspecific parameter FILTERPROPERTY and FILTERVALUE you can request rendering just a defined list of features. Each feature to be rendered will be identified by the value of a given property. The name of the property is defined by the parameter filterproperty. The name of the property is not qualified so all properties with the given local name will be considered. A list of valid property values will be defined using parameter filtervalue, multiple values must be comma separated. Each layer - or better its underlying data source - requested by a GeMap will evaluated for having a feature with a property with given name and one of the defined values. Just the features matching this filter condition will be rendered. It's quite natural that only layer with an underlying Feature-DataSource can be filtered. The other parameters addressed in the GetMap (e.g. the style) request are not effected by this parameters. If the filter cannot be applied to the layer, e.g. cause it is a raster layer or the data source does not have a matching property, the filter will be ignored. If one the parameters is missing or the value empty, the filter is not applied. +Example: FILTERPROPERTY=type&FILTERVALUE=stone,wood + [[anchor-xml-request-encoding]] ==== XML request encoding