Skip to content

Commit

Permalink
Basic time filtering and composition of filters in AND when multiple …
Browse files Browse the repository at this point in the history
…are found
  • Loading branch information
aaime committed May 17, 2018
1 parent 6be1318 commit 91cd3df
Show file tree
Hide file tree
Showing 9 changed files with 294 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ private WFSInfo getService() {
}

@Override
public Object getFeature(GetFeatureType request) {
public FeatureCollectionResponse getFeature(org.geoserver.wfs3.GetFeatureType request) {
// If the server has any more results available than it returns (the number it returns is
// less than or equal to the requested/default/maximum limit) then the server will include a
// link to the next set of results.
Expand All @@ -111,7 +111,7 @@ public Object getFeature(GetFeatureType request) {
request.setStartIndex(BigInteger.ZERO);
}


// delegate execution to WFS 2.0
FeatureCollectionResponse response = wfs20.getFeature(request);
return response;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/* (c) 2018 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.wfs3;

import net.opengis.wfs20.impl.GetFeatureTypeImpl;

/**
* This class extends WFS 2.0 GetFeatureType just to allow having a custom KVP reader for it
*/
public class GetFeatureType extends GetFeatureTypeImpl {

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
package org.geoserver.wfs3;

import io.swagger.v3.oas.models.OpenAPI;
import net.opengis.wfs20.GetFeatureType;
import org.geoserver.wfs.request.FeatureCollectionResponse;
import org.geoserver.wfs3.response.CollectionDocument;
import org.geoserver.wfs3.response.CollectionsDocument;
import org.geoserver.wfs3.response.ConformanceDocument;
Expand Down Expand Up @@ -47,5 +47,5 @@ public interface WebFeatureService30 {
* @param request
* @return
*/
Object getFeature(GetFeatureType request);
FeatureCollectionResponse getFeature(GetFeatureType request);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/* (c) 2018 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.wfs3.kvp;

import org.eclipse.emf.ecore.EObject;
import org.geoserver.catalog.Catalog;
import org.geoserver.catalog.FeatureTypeInfo;
import org.geoserver.catalog.NamespaceInfo;
import org.geoserver.config.GeoServer;
import org.geoserver.wfs3.GetFeatureType;
import org.geotools.util.DateRange;
import org.opengis.feature.type.FeatureType;
import org.opengis.filter.Filter;
import org.opengis.filter.FilterFactory;
import org.opengis.filter.expression.Literal;
import org.opengis.filter.expression.PropertyName;
import org.opengis.filter.identity.FeatureId;
import org.opengis.filter.spatial.BBOX;
import org.opengis.geometry.Envelope;

import javax.xml.namespace.QName;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

public class GetFeatureKvpRequestReader extends org.geoserver.wfs.kvp.GetFeatureKvpRequestReader {

public GetFeatureKvpRequestReader(GeoServer geoServer, FilterFactory filterFactory) {
super(GetFeatureType.class, null, geoServer, filterFactory);
}

@Override
public Object read(Object request, Map kvp, Map rawKvp) throws Exception {
GetFeatureType gf = (GetFeatureType) super.read(request, kvp, rawKvp);
Filter filter = getFullFilter(kvp);
querySet(gf, "filter", Collections.singletonList(filter));
return gf;
}

@Override
public Object createRequest() {
return new org.geoserver.wfs3.GetFeatureType();
}

/**
* Finds all filter expressions and combines them in "AND"
*
* @param kvp
* @return
*/
private Filter getFullFilter(Map kvp) throws IOException {
List<Filter> filters = new ArrayList<>();
// check the various filters, considering that only one feature type at a time can be
// used in WFS3
if (kvp.containsKey("filter")) {
List<Filter> list = (List) kvp.get("filter");
filters.add(list.get(0));
}
if (kvp.containsKey("cql_filter")) {
List<Filter> list = (List) kvp.get("cql_filter");
filters.add(list.get(0));
}
if (kvp.containsKey("featureId") || kvp.containsKey("resourceId")) {
List<String> featureIdList = (List) kvp.get("featureId");
Set<FeatureId> ids =
featureIdList
.stream()
.map(id -> filterFactory.featureId(id))
.collect(Collectors.toSet());
filters.add(filterFactory.id(ids));
}
if (kvp.containsKey("bbox")) {
Envelope bbox = (Envelope) kvp.get("bbox");
BBOX bboxFilter = bboxFilter((com.vividsolutions.jts.geom.Envelope) bbox);
filters.add(bboxFilter);
}
if (kvp.containsKey("time")) {
Object timeSpecification = kvp.get("time");
QName typeName = (QName) ((List) ((List) kvp.get("typeName")).get(0)).get(0);
List<String> timeProperties = getTimeProperties(typeName);
Filter filter = buildTimeFilter(timeSpecification, timeProperties);
filters.add(filter);
}
return mergeFiltersAnd(filters);
}

private Filter buildTimeFilter(Object timeSpec, List<String> timeProperties) {
List<Filter> filters = new ArrayList<>();
for (String timeProperty : timeProperties) {
PropertyName property = filterFactory.property(timeProperty);
Filter filter;
if (timeSpec instanceof Date) {
filter = filterFactory.equals(property, filterFactory.literal(timeSpec));
} else if (timeSpec instanceof DateRange) {
Literal before = filterFactory.literal(((DateRange) timeSpec).getMinValue());
Literal after = filterFactory.literal(((DateRange) timeSpec).getMaxValue());
filter = filterFactory.between(property, before, after);
} else {
throw new IllegalArgumentException("Cannot build time filter out of " + timeSpec);
}

filters.add(filter);
}

return mergeFiltersOr(filters);
}

private Filter mergeFiltersAnd(List<Filter> filters) {
if (filters.isEmpty()) {
return Filter.INCLUDE;
} else if (filters.size() == 1) {
return filters.get(0);
} else {
return filterFactory.and(filters);
}
}

private Filter mergeFiltersOr(List<Filter> filters) {
if (filters.isEmpty()) {
return Filter.EXCLUDE;
} else if (filters.size() == 1) {
return filters.get(0);
} else {
return filterFactory.or(filters);
}
}

private List<String> getTimeProperties(QName typeName) throws IOException {
Catalog catalog = geoServer.getCatalog();
NamespaceInfo ns = catalog.getNamespaceByURI(typeName.getNamespaceURI());
FeatureTypeInfo ft = catalog.getFeatureTypeByName(ns, typeName.getLocalPart());
if (ft == null) {
return Collections.emptyList();
}
FeatureType schema = ft.getFeatureType();
return schema.getDescriptors()
.stream()
.filter(pd -> Date.class.isAssignableFrom(pd.getType().getBinding()))
.map(pd -> pd.getName().getLocalPart())
.collect(Collectors.toList());
}

/**
* In WFS3 it's possible to have multiple filter KVPs, and they have to be combined in AND
*
* @param kvp
* @param keys
* @param request
*/
@Override
protected void ensureMutuallyExclusive(Map kvp, String[] keys, EObject request) {
// no op, we actually want to handle multiple filters
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package org.geoserver.wfs3.kvp;

import org.geoserver.ows.KvpParser;
import org.geoserver.ows.kvp.TimeParser;
import org.geoserver.platform.ServiceException;
import org.geotools.util.Version;

import java.text.ParseException;
import java.util.Collection;
import java.util.List;

/**
* WFS specific version of time parsing, turns a time spec into a single time or date range
*/
public class TimeKvpParser extends KvpParser {

TimeParser parser = new TimeParser();

/**
* Creates the parser specifying the name of the key to latch to.
*
* @param key The key whose associated value to parse.
*/
public TimeKvpParser() {
super("time", Object.class);
setVersion(new Version("3.0.0"));
setService("WFS");
}

@SuppressWarnings({ "unchecked", "rawtypes" })
public Object parse(String value) throws ParseException {
Collection times = parser.parse(value);
if (times.isEmpty() || times.size() > 1) {
throw new ServiceException(
"Invalid time specification, must be a single time, or a time range",
ServiceException.INVALID_PARAMETER_VALUE,
"time");
}

return times.iterator().next();
}


}
5 changes: 5 additions & 0 deletions src/community/wfs3/src/main/resources/applicationContext.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,15 @@
</bean>

<!-- KVP parsing -->
<bean id="wfs3TimeKvpParser" class="org.geoserver.wfs3.kvp.TimeKvpParser"/>
<bean id="apiKvpRequestReader" class="org.geoserver.wfs3.kvp.APIRequestKVPReader"/>
<bean id="contentKvpRequestReader" class="org.geoserver.wfs3.kvp.LandingPageRequestKVPReader"/>
<bean id="collectionsKvpRequestReader" class="org.geoserver.wfs3.kvp.CollectionsRequestKVPReader"/>
<bean id="conformanceKvpRequestReader" class="org.geoserver.wfs3.kvp.ConformanceRequestKVPReader"/>
<bean id="getFeatureKvpRequestReader" class="org.geoserver.wfs3.kvp.GetFeatureKvpRequestReader">
<constructor-arg index="0" ref="geoServer"/>
<constructor-arg index="1" ref="filterFactory"/>
</bean>

<!-- response generation -->
<bean id="apiResponse" class="org.geoserver.wfs3.response.APIDocumentResponse">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,68 @@ public void testGetLayerAsGeoJson() throws Exception {
List selfRels = json.read("links[?(@.type == 'application/geo+json')].rel");
assertEquals(1, selfRels.size());
assertEquals("self", selfRels.get(0));
// check altenate link
// check alternate link
List alternatefRels = json.read("links[?(@.type == 'application/json')].rel");
assertEquals(1, alternatefRels.size());
assertEquals("alternate", alternatefRels.get(0));
}

@Test
public void testBBoxFilter() throws Exception {
String roadSegments = getEncodedName(MockData.PRIMITIVEGEOFEATURE);
DocumentContext json =
getAsJSONPath("wfs3/collections/" + roadSegments + "/items?bbox=35,0,60,3", 200);
assertEquals("FeatureCollection", json.read("type", String.class));
// should return only f002 and f003
assertEquals(2, (int) json.read("features.length()", Integer.class));
assertEquals(
1, json.read("features[?(@.id == 'PrimitiveGeoFeature.f001')]", List.class).size());
assertEquals(
1, json.read("features[?(@.id == 'PrimitiveGeoFeature.f002')]", List.class).size());
}

@Test
public void testTimeFilter() throws Exception {
String roadSegments = getEncodedName(MockData.PRIMITIVEGEOFEATURE);
DocumentContext json =
getAsJSONPath("wfs3/collections/" + roadSegments + "/items?time=2006-10-25", 200);
assertEquals("FeatureCollection", json.read("type", String.class));
// should return only f001
assertEquals(1, (int) json.read("features.length()", Integer.class));
assertEquals(
1, json.read("features[?(@.id == 'PrimitiveGeoFeature.f001')]", List.class).size());
}

@Test
public void testTimeRangeFilter() throws Exception {
String roadSegments = getEncodedName(MockData.PRIMITIVEGEOFEATURE);
DocumentContext json =
getAsJSONPath(
"wfs3/collections/" + roadSegments + "/items?time=2006-09-01/2006-10-23",
200);
assertEquals("FeatureCollection", json.read("type", String.class));
assertEquals(2, (int) json.read("features.length()", Integer.class));
assertEquals(
1, json.read("features[?(@.id == 'PrimitiveGeoFeature.f002')]", List.class).size());
assertEquals(
1, json.read("features[?(@.id == 'PrimitiveGeoFeature.f003')]", List.class).size());
}

@Test
public void testCombinedSpaceTimeFilter() throws Exception {
String roadSegments = getEncodedName(MockData.PRIMITIVEGEOFEATURE);
DocumentContext json =
getAsJSONPath(
"wfs3/collections/"
+ roadSegments
+ "/items?time=2006-09-01/2006-10-23&bbox=35,0,60,3",
200);
assertEquals("FeatureCollection", json.read("type", String.class));
assertEquals(1, (int) json.read("features.length()", Integer.class));
assertEquals(
1, json.read("features[?(@.id == 'PrimitiveGeoFeature.f002')]", List.class).size());
}

@Test
public void testSingleFeatureAsGeoJson() throws Exception {
String roadSegments = getEncodedName(MockData.ROAD_SEGMENTS);
Expand All @@ -47,8 +103,9 @@ public void testSingleFeatureAsGeoJson() throws Exception {
assertEquals(1, selfRels.size());
assertEquals("self", selfRels.get(0));
String href = (String) ((List) json.read(geoJsonLinkPath + "href")).get(0);
String expected = "http://localhost:8080/geoserver/wfs3/collections/cite__RoadSegments" +
"/items/RoadSegments.1107532045088?f=application%2Fgeo%2Bjson";
String expected =
"http://localhost:8080/geoserver/wfs3/collections/cite__RoadSegments"
+ "/items/RoadSegments.1107532045088?f=application%2Fgeo%2Bjson";
assertEquals(expected, href);
// check alternate link
List alternatefRels = json.read("links[?(@.type == 'application/json')].rel");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,19 +232,18 @@ protected void handleBBOX(Map kvp, EObject eObject) throws Exception {
Query q = it.next();

List typeName = q.getTypeNames();
Filter filter = null;

Filter filter;
if (typeName.size() > 1) {
//TODO: not sure what to do here, just going to and them up
List and = new ArrayList(typeName.size());

for (Iterator t = typeName.iterator(); t.hasNext(); ) {
and.add(bboxFilter((QName) t.next(), bbox));
and.add(bboxFilter(bbox));
}

filter = filterFactory.and(and);
} else {
filter = bboxFilter((QName) typeName.get(0), bbox);
filter = bboxFilter(bbox);
}

filters.add(filter);
Expand Down Expand Up @@ -282,7 +281,7 @@ QName getTypeNameFromFeatureId(String fid) throws Exception {
* @param kvp
* @param keys
*/
private void ensureMutuallyExclusive(Map kvp, String[] keys, EObject request) {
protected void ensureMutuallyExclusive(Map kvp, String[] keys, EObject request) {
for (int i = 0; i < keys.length; i++) {
if (kvp.containsKey(keys[i])) {
for (int j = i + 1; j < keys.length; j++) {
Expand Down Expand Up @@ -338,7 +337,7 @@ QName checkTypeName(QName qName, NamespaceSupport namespaces, EObject request) {
return qName;
}

BBOX bboxFilter(QName typeName, Envelope bbox) throws Exception {
protected BBOX bboxFilter(Envelope bbox) {
//JD: use "" so that it applies to all geometries
String name = "";

Expand Down

0 comments on commit 91cd3df

Please sign in to comment.