diff --git a/service-wfs3/src/main/java/org/oskari/service/wfs3/FilterToOAPIFCoreQuery.java b/service-wfs3/src/main/java/org/oskari/service/wfs3/FilterToOAPIFCoreQuery.java new file mode 100644 index 0000000000..34d7a029c7 --- /dev/null +++ b/service-wfs3/src/main/java/org/oskari/service/wfs3/FilterToOAPIFCoreQuery.java @@ -0,0 +1,414 @@ +package org.oskari.service.wfs3; + +import java.util.List; +import java.util.Map; + +import org.geotools.filter.FilterCapabilities; +import org.geotools.filter.visitor.SimplifyingFilterVisitor; +import org.geotools.geometry.jts.ReferencedEnvelope; +import org.opengis.filter.And; +import org.opengis.filter.ExcludeFilter; +import org.opengis.filter.Filter; +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.PropertyIsNil; +import org.opengis.filter.PropertyIsNotEqualTo; +import org.opengis.filter.PropertyIsNull; +import org.opengis.filter.expression.Add; +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.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; +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.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; + +import fi.nls.oskari.domain.map.OskariLayer; + +public class FilterToOAPIFCoreQuery implements FilterVisitor, ExpressionVisitor { + + private final OskariLayer layer; + private final FilterCapabilities capabilities; + private boolean insideAnd = false; + + public FilterToOAPIFCoreQuery(OskariLayer layer) { + this.layer = layer; + this.capabilities = createFilterCapabilities(layer); + } + + public Filter toQueryParameters(Filter filter, Map query) { + FilterCapabilities cap = capabilities; + OAPIFCoreFilterSplittingVisitor splitter = new OAPIFCoreFilterSplittingVisitor(cap); + filter.accept(splitter, null); + splitter.getFilterPre().accept(this, query); + return splitter.getFilterPost(); + } + + private FilterCapabilities createFilterCapabilities(OskariLayer layer) { + // TODO: Determine capabilities based on layer + FilterCapabilities capabilities = new FilterCapabilities(); + + capabilities.addType(FilterCapabilities.COMPARE_EQUALS); + capabilities.addType(FilterCapabilities.LOGIC_AND); + capabilities.addType(FilterCapabilities.SPATIAL_BBOX); + + return capabilities; + } + + @Override + public Object visitNullFilter(Object extraData) { + return extraData; + } + + @Override + public Object visit(ExcludeFilter filter, Object extraData) { + return null; + } + + @Override + public Object visit(IncludeFilter filter, Object extraData) { + return extraData; + } + + @Override + public Object visit(And filter, Object extraData) { + List children = filter.getChildren(); + if (children != null && !children.isEmpty()) { + if (insideAnd) { + throw new UnsupportedOperationException("Nested AND not supported"); + } + insideAnd = true; + for (Filter child : children) { + child.accept(this, extraData); + } + insideAnd = false; + } + return extraData; + } + + @Override + public Object visit(Or filter, Object extraData) { + Map query = (Map) extraData; + for (Filter f : filter.getChildren()) { + visit((PropertyIsEqualTo) f, query, true); + } + return extraData; + } + + @Override + public Object visit(PropertyIsEqualTo filter, Object extraData) { + visit(filter, (Map) extraData, false); + return extraData; + } + + private void visit(PropertyIsEqualTo filter, Map query, boolean insideOr) { + Expression e1 = filter.getExpression1(); + Expression e2 = filter.getExpression2(); + + PropertyName prop; + Literal literal; + if (e1 instanceof PropertyName && e2 instanceof Literal) { + prop = (PropertyName) e1; + literal = (Literal) e2; + } else if (e2 instanceof PropertyName && e1 instanceof Literal) { + prop = (PropertyName) e2; + literal = (Literal) e1; + } else { + throw new UnsupportedOperationException("Expressions must be one PropertyName and one Literal"); + } + + Object key = prop.accept(this, null); + Object value = literal.accept(this, null); + if (key == null || value == null) { + return; + } + String k = key.toString(); + String v = value.toString(); + if (insideOr) { + // foo = 1 OR foo = 2 => &foo=1,2 + query.compute(k, (__, curr) -> (curr == null) ? v : curr + "," + v); + } else { + String curr = query.get(k); + if (curr == null) { + query.put(k, v); + } else { + if (curr.equals(v)) { + // foo = 1 AND foo = 1, OK just ignore it(?) + return; + } + // foo = 1 AND foo = 2 => Doesn't make sense + // We probably should handle this before entering here, look into extending SimplifyingFilterVisitor + throw new UnsupportedOperationException("Multiple PropertyIsEqualTo filters for same PropertyName"); + } + } + } + + + @SuppressWarnings("unchecked") + @Override + public Object visit(BBOX filter, Object extraData) { + // TODO: Ignore other than "primary" geometry for now + ReferencedEnvelope bbox = (ReferencedEnvelope) filter.getBounds(); + Map query = (Map) extraData; + OskariWFS3Client.addBboxToQuery(layer, bbox, query); + return query; + } + + @Override + public Object visit(PropertyName expression, Object extraData) { + // TODO: Check this PropertyName is queryable + return expression.getPropertyName(); + } + + @Override + public Object visit(Literal expression, Object extraData) { + return expression.getValue(); + } + + @Override + public Object visit(PropertyIsBetween filter, Object extraData) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public Object visit(PropertyIsNotEqualTo filter, Object extraData) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public Object visit(PropertyIsGreaterThan filter, Object extraData) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public Object visit(PropertyIsGreaterThanOrEqualTo filter, Object extraData) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public Object visit(PropertyIsLessThan filter, Object extraData) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public Object visit(PropertyIsLessThanOrEqualTo filter, Object extraData) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public Object visit(PropertyIsLike filter, Object extraData) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public Object visit(PropertyIsNull filter, Object extraData) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public Object visit(Not filter, Object extraData) { + throw new UnsupportedOperationException("Not supported"); + } + + + @Override + public Object visit(Id filter, Object extraData) { + throw new UnsupportedOperationException("Not supported"); + } + + + @Override + public Object visit(PropertyIsNil filter, Object extraData) { + throw new UnsupportedOperationException("Not supported"); + } + + + @Override + public Object visit(Beyond filter, Object extraData) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public Object visit(Contains filter, Object extraData) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public Object visit(Crosses filter, Object extraData) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public Object visit(Disjoint filter, Object extraData) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public Object visit(DWithin filter, Object extraData) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public Object visit(Equals filter, Object extraData) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public Object visit(Intersects filter, Object extraData) { + throw new UnsupportedOperationException("Not supported"); + } + + + @Override + public Object visit(Overlaps filter, Object extraData) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public Object visit(Touches filter, Object extraData) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public Object visit(Within filter, Object extraData) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public Object visit(After after, Object extraData) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public Object visit(AnyInteracts anyInteracts, Object extraData) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public Object visit(Before before, Object extraData) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public Object visit(Begins begins, Object extraData) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public Object visit(BegunBy begunBy, Object extraData) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public Object visit(During during, Object extraData) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public Object visit(EndedBy endedBy, Object extraData) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public Object visit(Ends ends, Object extraData) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public Object visit(Meets meets, Object extraData) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public Object visit(MetBy metBy, Object extraData) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public Object visit(OverlappedBy overlappedBy, Object extraData) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public Object visit(TContains contains, Object extraData) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public Object visit(TEquals equals, Object extraData) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public Object visit(TOverlaps contains, Object extraData) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public Object visit(NilExpression expression, Object extraData) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public Object visit(Add expression, Object extraData) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public Object visit(Divide expression, Object extraData) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public Object visit(Function expression, Object extraData) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public Object visit(Multiply expression, Object extraData) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public Object visit(Subtract expression, Object extraData) { + throw new UnsupportedOperationException("Not supported"); + } + +} diff --git a/service-wfs3/src/main/java/org/oskari/service/wfs3/OAPIFCoreFilterSplittingVisitor.java b/service-wfs3/src/main/java/org/oskari/service/wfs3/OAPIFCoreFilterSplittingVisitor.java new file mode 100644 index 0000000000..3087a7913d --- /dev/null +++ b/service-wfs3/src/main/java/org/oskari/service/wfs3/OAPIFCoreFilterSplittingVisitor.java @@ -0,0 +1,114 @@ +package org.oskari.service.wfs3; + +import org.geotools.filter.FilterCapabilities; +import org.geotools.filter.visitor.PostPreProcessFilterSplittingVisitor; +import org.opengis.filter.Filter; +import org.opengis.filter.Or; +import org.opengis.filter.PropertyIsEqualTo; +import org.opengis.filter.expression.Expression; +import org.opengis.filter.expression.Literal; +import org.opengis.filter.expression.PropertyName; + +/** + * Split a Filter into pre-filter and post-filter. + * Pre-filter is the part the OAPIF server can handle and apply + * Post-filter is the part oskari-server has to handle and apply to the response from the OAPIF server + * + * This class overrides visit(PropertyIsEqualTo) method as the super class + * handles all SIMPLE_COMPARISONS_OPENGIS as same group and we don't want that + * since we only support PropertyIsEqualTo from that group + * + * Also override the visit(Or) as we want to be able to handle case where + * all children of the Or are simple PropertyIsEqualTo filters and that all + * of them share the same PropertyName + */ +class OAPIFCoreFilterSplittingVisitor extends PostPreProcessFilterSplittingVisitor { + + public OAPIFCoreFilterSplittingVisitor(FilterCapabilities fcs) { + super(fcs, null, null); + } + + @Override + public Object visit(PropertyIsEqualTo filter, Object notUsed) { + if (original == null) original = filter; + + int i = postStack.size(); + Expression leftValue = filter.getExpression1(); + Expression rightValue = filter.getExpression2(); + if (leftValue == null || rightValue == null) { + postStack.push(filter); + return null; + } + + leftValue.accept(this, null); + + if (i < postStack.size()) { + postStack.pop(); + postStack.push(filter); + + return null; + } + + rightValue.accept(this, null); + + if (i < postStack.size()) { + preStack.pop(); // left + postStack.pop(); + postStack.push(filter); + + return null; + } + + preStack.pop(); // left side + preStack.pop(); // right side + preStack.push(filter); + + return null; + } + + public Object visit(Or filter, Object notUsed) { + if (isSimpleIN(filter)) { + preStack.push(filter); + } else { + super.visit(filter, notUsed); + } + return null; + } + + private boolean isSimpleIN(Or filter) { + String prevName = null; + + for (Filter f : filter.getChildren()) { + if (!(f instanceof PropertyIsEqualTo)) { + return false; + } + + PropertyName prop = getPropertyName((PropertyIsEqualTo) f); + if (prop == null) { + return false; + } + String propName = prop.getPropertyName(); + + if (prevName == null) { + prevName = propName; + } else if (propName.equals(prevName)) { + continue; + } else { + return false; + } + } + + return true; + } + + private PropertyName getPropertyName(PropertyIsEqualTo eq) { + if (eq.getExpression1() instanceof PropertyName && eq.getExpression2() instanceof Literal) { + return (PropertyName) eq.getExpression1(); + } else if (eq.getExpression2() instanceof PropertyName && eq.getExpression1() instanceof Literal) { + return (PropertyName) eq.getExpression2(); + } else { + return null; + } + } + +} diff --git a/service-wfs3/src/main/java/org/oskari/service/wfs3/OskariWFS3Client.java b/service-wfs3/src/main/java/org/oskari/service/wfs3/OskariWFS3Client.java index 2116db6a99..a4a977bd01 100644 --- a/service-wfs3/src/main/java/org/oskari/service/wfs3/OskariWFS3Client.java +++ b/service-wfs3/src/main/java/org/oskari/service/wfs3/OskariWFS3Client.java @@ -17,8 +17,8 @@ import org.geotools.referencing.CRS; import org.json.JSONObject; import org.opengis.feature.simple.SimpleFeatureType; +import org.opengis.filter.Filter; import org.opengis.geometry.MismatchedDimensionException; -import org.opengis.referencing.FactoryException; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.TransformException; @@ -43,7 +43,15 @@ public class OskariWFS3Client { private static final Logger LOG = LogFactory.getLogger(OskariWFS3Client.class); - protected static final int PAGE_SIZE = 1000; + protected static final String ATTRIBUTE_PAGE_SIZE = "pageSize"; + protected static final String ATTRIBUTE_HARD_LIMIT = "hardLimit"; + + private static final int DEFAULT_PAGE_SIZE = 1000; + private static final int DEFAULT_HARD_LIMIT = 10_000; + private static final int MIN_PAGE_SIZE = 10; + private static final int MIN_HARD_LIMIT = 10; + private static final int MAX_PAGE_SIZE = 10_000; + private static final int MAX_HARD_LIMIT = 100_000; private static final String CONTENT_TYPE_GEOJSON = "application/geo+json"; private static final int MAX_REDIRECTS = 5; @@ -69,15 +77,11 @@ private OskariWFS3Client() {} public static SimpleFeatureCollection getFeatures(OskariLayer layer, ReferencedEnvelope bbox, CoordinateReferenceSystem crs) throws ServiceRuntimeException { - /* Servers can not yet implement this since no requirement URI is standardized yet - boolean crsExtension; - try { - crsExtension = checkCRSExtension(endPoint, user, pass); - } catch (IOException e) { - throw new ServiceRuntimeException("IOException occured", e); - } - */ - + return getFeatures(layer, bbox, crs, null); + } + + public static SimpleFeatureCollection getFeatures(OskariLayer layer, + ReferencedEnvelope bbox, CoordinateReferenceSystem crs, Filter filter) throws ServiceRuntimeException { String crsURI = getCrsURI(layer, crs); MathTransform transformCRS84ToTargetCRS = null; if (crsURI == null) { @@ -89,46 +93,46 @@ public static SimpleFeatureCollection getFeatures(OskariLayer layer, } } - String bboxCrsURI = null; - if (bbox != null) { - bboxCrsURI = getCrsURI(layer, bbox.getCoordinateReferenceSystem()); - if (bboxCrsURI == null) { - try { - // Server doesn't support bboxes CRS - transform bbox to CRS84 - bbox = transformEnvelope(bbox, getCRS84()); - } catch (Exception e) { - throw new ServiceRuntimeException("bbox coordinate transformation failure", e); - } - } - } - - String endPoint = layer.getUrl(); - String collectionId = layer.getName(); + String path = getItemsPath(layer.getUrl(), layer.getName()); String user = layer.getUsername(); String pass = layer.getPassword(); - // TODO: FIXME! - int hardLimit = 10000; - String path = getItemsPath(endPoint, collectionId); - Map query = getQueryParams(crsURI, bbox, bboxCrsURI, hardLimit); + int pageSize = DEFAULT_PAGE_SIZE; + int hardLimit = DEFAULT_HARD_LIMIT; + JSONObject attr = layer.getAttributes(); + if (attr != null) { + pageSize = clamp(attr.optInt(ATTRIBUTE_PAGE_SIZE, pageSize), MIN_PAGE_SIZE, MAX_PAGE_SIZE); + hardLimit = clamp(attr.optInt(ATTRIBUTE_HARD_LIMIT, hardLimit), MIN_HARD_LIMIT, MAX_HARD_LIMIT); + } + + Map query = getQueryParams(crsURI, pageSize); // attach any extra params added for layer (for example properties=[prop name we are interested in]) query.putAll(JSONHelper.getObjectAsMap(layer.getParams())); - Map headers = Collections.singletonMap("Accept", CONTENT_TYPE_GEOJSON); + Filter postFilter = null; + if (filter != null) { + postFilter = new FilterToOAPIFCoreQuery(layer).toQueryParameters(filter, query); + } else if (bbox != null) { + addBboxToQuery(layer, bbox, query); + } - List pages = new ArrayList<>(); - int numFeatures = 0; + Map headers = Collections.singletonMap("Accept", CONTENT_TYPE_GEOJSON); - SimpleFeatureType schema; try { + List pages = new ArrayList<>(); + int numFeatures = 0; + HttpURLConnection conn = IOHelper.getConnection(path, user, pass, query, headers); conn = IOHelper.followRedirect(conn, user, pass, query, headers, MAX_REDIRECTS); validateResponse(conn, CONTENT_TYPE_GEOJSON); Map geojson = readMap(conn); boolean ignoreGeometryProperties = true; - schema = GeoJSONSchemaDetector.getSchema(geojson, crs, ignoreGeometryProperties); + SimpleFeatureType schema = GeoJSONSchemaDetector.getSchema(geojson, crs, ignoreGeometryProperties); SimpleFeatureCollection sfc = GeoJSONReader2.toFeatureCollection(geojson, schema, transformCRS84ToTargetCRS); + if (postFilter != null) { + sfc = sfc.subCollection(postFilter); + } numFeatures += sfc.size(); pages.add(sfc); String next = getLinkHref(geojson, "next"); @@ -141,28 +145,51 @@ public static SimpleFeatureCollection getFeatures(OskariLayer layer, validateResponse(conn, CONTENT_TYPE_GEOJSON); geojson = readMap(conn); sfc = GeoJSONReader2.toFeatureCollection(geojson, schema, transformCRS84ToTargetCRS); + if (postFilter != null) { + sfc = sfc.subCollection(postFilter); + } numFeatures += sfc.size(); pages.add(sfc); next = getLinkHref(geojson, "next"); } + + if (pages.size() == 1) { + return pages.get(0); + } + return new PaginatedFeatureCollection(pages, schema, "FeatureCollection", hardLimit); } catch (IOException e) { throw new ServiceRuntimeException("IOException occured", e); } catch (MismatchedDimensionException | TransformException e) { throw new ServiceRuntimeException("Projection transformation failed", e); } + } + + private static int clamp(int value, int min, int max) { + return value > max ? max : value < min ? min : value; + } - if (pages.size() == 1) { - return pages.get(0); + static void addBboxToQuery(OskariLayer layer, ReferencedEnvelope bbox, Map query) { + String bboxCrsURI = getCrsURI(layer, bbox.getCoordinateReferenceSystem()); + if (bboxCrsURI != null) { + query.put("bbox-crs", bboxCrsURI); + } else { + // Server doesn't support bbox's CRS - transform bbox to CRS84 + bbox = transformEnvelope(bbox, getCRS84()); } - return new PaginatedFeatureCollection(pages, schema, "FeatureCollection", hardLimit); + query.put("bbox", String.format(Locale.US, "%f,%f,%f,%f", + bbox.getMinX(), bbox.getMinY(), + bbox.getMaxX(), bbox.getMaxY())); } - private static ReferencedEnvelope transformEnvelope(ReferencedEnvelope env, CoordinateReferenceSystem to) - throws TransformException, FactoryException { + private static ReferencedEnvelope transformEnvelope(ReferencedEnvelope env, CoordinateReferenceSystem to) { if (CRS.equalsIgnoreMetadata(env.getCoordinateReferenceSystem(), to)) { return env; } - return env.transform(to, true); + try { + return env.transform(to, true); + } catch (Exception e) { + throw new ServiceRuntimeException("bbox coordinate transformation failure", e); + } } /** @@ -233,23 +260,14 @@ public static String getItemsPath(String endPoint, String collectionId) { return getCollectionsPath(endPoint, collectionId) + "/items"; } - protected static Map getQueryParams(String crsURI, ReferencedEnvelope bbox, String bboxCrsURI, int hardLimit) + protected static Map getQueryParams(String crsURI, int limit) throws ServiceRuntimeException { // Linked not needed, but looks better when logging the requests Map parameters = new LinkedHashMap<>(); if (crsURI != null) { parameters.put("crs", crsURI); } - if (bbox != null) { - String bboxStr = String.format(Locale.US, "%f,%f,%f,%f", - bbox.getMinX(), bbox.getMinY(), - bbox.getMaxX(), bbox.getMaxY()); - parameters.put("bbox", bboxStr); - if (bboxCrsURI != null) { - parameters.put("bbox-crs", bboxCrsURI); - } - } - parameters.put("limit", Integer.toString(Math.min(PAGE_SIZE, hardLimit))); + parameters.put("limit", Integer.toString(limit)); return parameters; } diff --git a/service-wfs3/src/test/java/org/oskari/service/wfs3/FilterToOAPIFCoreTest.java b/service-wfs3/src/test/java/org/oskari/service/wfs3/FilterToOAPIFCoreTest.java new file mode 100644 index 0000000000..caacd31565 --- /dev/null +++ b/service-wfs3/src/test/java/org/oskari/service/wfs3/FilterToOAPIFCoreTest.java @@ -0,0 +1,151 @@ +package org.oskari.service.wfs3; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import org.geotools.factory.CommonFactoryFinder; +import org.geotools.geometry.jts.ReferencedEnvelope; +import org.geotools.referencing.CRS; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Test; +import org.opengis.filter.Filter; +import org.opengis.filter.FilterFactory; +import org.opengis.filter.PropertyIsNotEqualTo; +import org.opengis.filter.expression.Literal; +import org.opengis.referencing.FactoryException; +import org.opengis.referencing.NoSuchAuthorityCodeException; +import org.opengis.referencing.crs.CoordinateReferenceSystem; + +import fi.nls.oskari.domain.map.OskariLayer; + +public class FilterToOAPIFCoreTest { + + @Test + public void testBboxAndPropertyFilter() throws NoSuchAuthorityCodeException, FactoryException, JSONException { + OskariLayer layer = new OskariLayer(); + String tm35finURI = "http://www.opengis.net/def/crs/EPSG/0/3067"; + layer.setCapabilities(new JSONObject().put("crs-uri", new JSONArray(Arrays.asList(tm35finURI)))); + FilterToOAPIFCoreQuery f = new FilterToOAPIFCoreQuery(layer); + FilterFactory ff = CommonFactoryFinder.getFilterFactory(); + + double minEast = 500000.0; + double maxEast = 501000.0; + double minNorth = 6740263.0; + double maxNorth = 6741263.0; + CoordinateReferenceSystem tm35fin = CRS.decode("EPSG:3067"); + ReferencedEnvelope bbox = new ReferencedEnvelope(minEast, maxEast, minNorth, maxNorth, tm35fin); + + Filter foo = ff.equals(ff.property("foo"), ff.literal(50)); + Filter bar = ff.equals(ff.property("bar"), ff.literal(-1337.0)); + Filter baz = ff.equals(ff.property("baz"), ff.literal("qux")); + Filter bboxFilter = ff.bbox("any", + bbox.getMinX(), bbox.getMinY(), + bbox.getMaxX(), bbox.getMaxY(), + CRS.toSRS(bbox.getCoordinateReferenceSystem())); + Filter and = ff.and(Arrays.asList(bboxFilter, foo, bar, baz)); + + Map actual = new HashMap<>(); + Filter postFilter = f.toQueryParameters(and, actual); + + Map expected = new HashMap<>(); + expected.put("foo", "50"); + expected.put("bar", "-1337.0"); + expected.put("baz", "qux"); + expected.put("bbox", String.format("%f,%f,%f,%f", minEast, minNorth, maxEast, maxNorth)); + expected.put("bbox-crs", tm35finURI); + assertEquals(expected, actual); + + assertEquals(Filter.INCLUDE, postFilter); + } + + @Test + public void testNotEqualTo() throws NoSuchAuthorityCodeException, FactoryException, JSONException { + OskariLayer layer = new OskariLayer(); + String tm35finURI = "http://www.opengis.net/def/crs/EPSG/0/3067"; + layer.setCapabilities(new JSONObject().put("crs-uri", new JSONArray(Arrays.asList(tm35finURI)))); + FilterToOAPIFCoreQuery f = new FilterToOAPIFCoreQuery(layer); + FilterFactory ff = CommonFactoryFinder.getFilterFactory(); + + double minEast = 500000.0; + double maxEast = 501000.0; + double minNorth = 6740263.0; + double maxNorth = 6741263.0; + CoordinateReferenceSystem tm35fin = CRS.decode("EPSG:3067"); + ReferencedEnvelope bbox = new ReferencedEnvelope(minEast, maxEast, minNorth, maxNorth, tm35fin); + + Filter bboxFilter = ff.bbox("any", + bbox.getMinX(), bbox.getMinY(), + bbox.getMaxX(), bbox.getMaxY(), + CRS.toSRS(bbox.getCoordinateReferenceSystem())); + Filter eqFilter = ff.equals(ff.property("myprop"), ff.literal(50)); + Filter notEqFilter = ff.notEqual(ff.property("myprop2"), ff.literal(30)); + Filter and = ff.and(Arrays.asList(bboxFilter, eqFilter, notEqFilter)); + + Map actual = new HashMap<>(); + Filter postFilter = f.toQueryParameters(and, actual); + + Map expected = new HashMap<>(); + expected.put("myprop", "50"); + expected.put("bbox", String.format("%f,%f,%f,%f", minEast, minNorth, maxEast, maxNorth)); + expected.put("bbox-crs", tm35finURI); + assertEquals(expected, actual); + + if (!(postFilter instanceof PropertyIsNotEqualTo)) { + fail("Expected property is not equal to"); + } + PropertyIsNotEqualTo neq = (PropertyIsNotEqualTo) postFilter; + assertEquals(30, ((Literal) neq.getExpression2()).getValue()); + } + + @Test + public void testSimpleOr() throws NoSuchAuthorityCodeException, FactoryException, JSONException { + OskariLayer layer = new OskariLayer(); + String tm35finURI = "http://www.opengis.net/def/crs/EPSG/0/3067"; + layer.setCapabilities(new JSONObject().put("crs-uri", new JSONArray(Arrays.asList(tm35finURI)))); + FilterToOAPIFCoreQuery f = new FilterToOAPIFCoreQuery(layer); + FilterFactory ff = CommonFactoryFinder.getFilterFactory(); + + Filter eq1 = ff.equals(ff.property("myprop"), ff.literal(1)); + Filter eq2 = ff.equals(ff.property("myprop"), ff.literal(2)); + Filter eq3 = ff.equals(ff.property("myprop"), ff.literal(3)); + Filter or = ff.or(Arrays.asList(eq1, eq2, eq3)); + + Map actual = new HashMap<>(); + Filter postFilter = f.toQueryParameters(or, actual); + + Map expected = new HashMap<>(); + expected.put("myprop", "1,2,3"); + assertEquals(expected, actual); + + assertEquals(Filter.INCLUDE, postFilter); + } + + @Test + public void testNonSenseAnd() throws NoSuchAuthorityCodeException, FactoryException, JSONException { + OskariLayer layer = new OskariLayer(); + String tm35finURI = "http://www.opengis.net/def/crs/EPSG/0/3067"; + layer.setCapabilities(new JSONObject().put("crs-uri", new JSONArray(Arrays.asList(tm35finURI)))); + FilterToOAPIFCoreQuery f = new FilterToOAPIFCoreQuery(layer); + FilterFactory ff = CommonFactoryFinder.getFilterFactory(); + + Filter eq1 = ff.equals(ff.property("myprop"), ff.literal(1)); + Filter eq2 = ff.equals(ff.property("myprop"), ff.literal(2)); + Filter eq3 = ff.equals(ff.property("myprop"), ff.literal(3)); + Filter and = ff.and(Arrays.asList(eq1, eq2, eq3)); + + try { + f.toQueryParameters(and, new HashMap<>()); + fail(); + } catch (UnsupportedOperationException expected) { + // success + } + } + +} diff --git a/service-wfs3/src/test/java/org/oskari/service/wfs3/OskariWFS3ClientTest.java b/service-wfs3/src/test/java/org/oskari/service/wfs3/OskariWFS3ClientTest.java index 827abff573..215c7074d3 100644 --- a/service-wfs3/src/test/java/org/oskari/service/wfs3/OskariWFS3ClientTest.java +++ b/service-wfs3/src/test/java/org/oskari/service/wfs3/OskariWFS3ClientTest.java @@ -61,26 +61,6 @@ public void testGetFeaturesHardLimit() throws ServiceException, IOException { assertEquals(29, i); } - @Ignore("Depends on outside service, results might vary") - @Test - public void testGetFeaturesHardLimitPaging() throws ServiceException, IOException { - OskariLayer layer = new OskariLayer(); - layer.setUrl("https://beta-paikkatieto.maanmittauslaitos.fi/geographic-names/wfs3/v1/"); - layer.setName("places"); - CoordinateReferenceSystem crs = OskariWFS3Client.getCRS84(); - int limit = OskariWFS3Client.PAGE_SIZE + 10; - SimpleFeatureCollection sfc = OskariWFS3Client.getFeatures(layer, null, crs); - assertEquals(limit, sfc.size()); - int i = 0; - try (SimpleFeatureIterator it = sfc.features()) { - while (it.hasNext()) { - i++; - it.next(); - } - } - assertEquals(limit, i); - } - @Ignore("Depends on outside service, results might vary") @Test public void testGetFeaturesServiceSupportingCRS_EPSG3067() throws Exception {