From 11cb9f2f0b38a5b19edcdeb4fe5e9c2dc8ac43b0 Mon Sep 17 00:00:00 2001 From: Mathieu Baudier Date: Sun, 24 Sep 2023 11:34:48 +0200 Subject: [PATCH] Make WFS more extensible --- org.argeo.app.geo/OSGI-INF/wfsHttpHandler.xml | 1 + .../argeo/app/geo/http/FeatureAdapter.java | 34 ++++ .../argeo/app/geo/http/WfsHttpHandler.java | 145 +++++++++++++----- .../src/org/argeo/app/geo/http/WfsUtils.java | 16 ++ 4 files changed, 154 insertions(+), 42 deletions(-) create mode 100644 org.argeo.app.geo/src/org/argeo/app/geo/http/FeatureAdapter.java create mode 100644 org.argeo.app.geo/src/org/argeo/app/geo/http/WfsUtils.java diff --git a/org.argeo.app.geo/OSGI-INF/wfsHttpHandler.xml b/org.argeo.app.geo/OSGI-INF/wfsHttpHandler.xml index d5646f2..356fa03 100644 --- a/org.argeo.app.geo/OSGI-INF/wfsHttpHandler.xml +++ b/org.argeo.app.geo/OSGI-INF/wfsHttpHandler.xml @@ -6,4 +6,5 @@ + diff --git a/org.argeo.app.geo/src/org/argeo/app/geo/http/FeatureAdapter.java b/org.argeo.app.geo/src/org/argeo/app/geo/http/FeatureAdapter.java new file mode 100644 index 0000000..22e1445 --- /dev/null +++ b/org.argeo.app.geo/src/org/argeo/app/geo/http/FeatureAdapter.java @@ -0,0 +1,34 @@ +package org.argeo.app.geo.http; + +import javax.xml.namespace.QName; + +import org.argeo.api.acr.Content; +import org.argeo.api.acr.search.AndFilter; +import org.argeo.app.api.EntityType; +import org.argeo.app.api.WGS84PosName; +import org.argeo.app.geo.JTS; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.Point; + +import jakarta.json.stream.JsonGenerator; + +public interface FeatureAdapter { + default Geometry getDefaultGeometry(Content c, QName targetFeature) { + // TODO deal with more defaults + // TODO deal with target feature + if (c.hasContentClass(EntityType.geopoint)) { + double latitude = c.get(WGS84PosName.lat, Double.class).get(); + double longitude = c.get(WGS84PosName.lon, Double.class).get(); + + Coordinate coordinate = new Coordinate(longitude, latitude); + Point the_geom = JTS.GEOMETRY_FACTORY.createPoint(coordinate); + return the_geom; + } + return null; + } + + void writeProperties(JsonGenerator g, Content content, QName targetFeature); + + void addConstraintsForFeature(AndFilter filter, QName targetFeature); +} diff --git a/org.argeo.app.geo/src/org/argeo/app/geo/http/WfsHttpHandler.java b/org.argeo.app.geo/src/org/argeo/app/geo/http/WfsHttpHandler.java index 92f41ac..8b2d159 100644 --- a/org.argeo.app.geo/src/org/argeo/app/geo/http/WfsHttpHandler.java +++ b/org.argeo.app.geo/src/org/argeo/app/geo/http/WfsHttpHandler.java @@ -6,8 +6,11 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.UncheckedIOException; import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Stream; @@ -17,8 +20,8 @@ import org.argeo.api.acr.Content; import org.argeo.api.acr.ContentSession; import org.argeo.api.acr.DName; import org.argeo.api.acr.NamespaceUtils; -import org.argeo.api.acr.QNamed; import org.argeo.api.acr.ldap.LdapAttr; +import org.argeo.api.acr.search.AndFilter; import org.argeo.api.acr.spi.ProvidedRepository; import org.argeo.api.cms.CmsLog; import org.argeo.app.api.EntityName; @@ -30,6 +33,7 @@ import org.argeo.app.geo.GpxUtils; import org.argeo.app.geo.JTS; import org.argeo.cms.http.HttpHeader; import org.argeo.cms.http.server.HttpServerUtils; +import org.argeo.cms.util.LangUtils; import org.geotools.data.DataUtilities; import org.geotools.data.geojson.GeoJSONWriter; import org.geotools.feature.DefaultFeatureCollection; @@ -67,6 +71,8 @@ public class WfsHttpHandler implements HttpHandler { final static String TYPE_NAMES = "typeNames"; final static String CQL_FILTER = "cql_filter"; + private final Map featureAdapters = new HashMap<>(); + @Override public void handle(HttpExchange exchange) throws IOException { String path = HttpServerUtils.subPath(exchange); @@ -74,9 +80,9 @@ public class WfsHttpHandler implements HttpHandler { // Content content = session.get(path); Map> parameters = HttpServerUtils.parseParameters(exchange); - String cql = parameters.containsKey(CQL_FILTER) ? parameters.get(CQL_FILTER).get(0) : null; - String typeNamesStr = parameters.containsKey(TYPE_NAMES) ? parameters.get(TYPE_NAMES).get(0) : null; - String outputFormat = parameters.containsKey(OUTPUT_FORMAT) ? parameters.get(OUTPUT_FORMAT).get(0) : null; + String cql = getKvpParameter(parameters, CQL_FILTER); + String typeNamesStr = getKvpParameter(parameters, TYPE_NAMES); + String outputFormat = getKvpParameter(parameters, OUTPUT_FORMAT); if (outputFormat == null) { outputFormat = "application/json"; } @@ -93,17 +99,19 @@ public class WfsHttpHandler implements HttpHandler { default -> throw new IllegalArgumentException("Unexpected value: " + outputFormat); } - QName[] typeNames; + List typeNames = new ArrayList<>(); if (typeNamesStr != null) { String[] arr = typeNamesStr.split(","); - typeNames = new QName[arr.length]; for (int i = 0; i < arr.length; i++) { - typeNames[i] = NamespaceUtils.parsePrefixedName(arr[i]); + typeNames.add(NamespaceUtils.parsePrefixedName(arr[i])); } } else { - typeNames = new QName[] { EntityType.local.qName() }; + typeNames.add(EntityType.local.qName()); } + if (typeNames.size() > 1) + throw new UnsupportedOperationException("Only one type name is currently supported"); + Stream res = session.search((search) -> { if (cql != null) { CqlUtils.filter(search.from(path), cql); @@ -111,10 +119,15 @@ public class WfsHttpHandler implements HttpHandler { search.from(path).where((and) -> { }); } - search.getWhere().any((f) -> { - for (QName typeName : typeNames) - f.isContentClass(typeName); - }); +// search.getWhere().any((f) -> { + for (QName typeName : typeNames) { + FeatureAdapter featureAdapter = featureAdapters.get(typeName); + if (featureAdapter == null) + throw new IllegalStateException("No feature adapter found for " + typeName); + // f.isContentClass(typeName); + featureAdapter.addConstraintsForFeature((AndFilter) search.getWhere(), typeName); + } +// }); }); exchange.sendResponseHeaders(200, 0); @@ -124,12 +137,46 @@ public class WfsHttpHandler implements HttpHandler { if ("GML3".equals(outputFormat)) { encodeCollectionAsGML(res, out); } else if ("application/json".equals(outputFormat)) { - encodeCollectionAsGeoJSon(res, out); + encodeCollectionAsGeoJSon(res, out, typeNames); } } } - protected void encodeCollectionAsGeoJSon(Stream features, OutputStream out) throws IOException { + /** + * Retrieve KVP (keyword-value pairs) parameters, which are lower case, as per + * specifications. + * + * @see https://docs.ogc.org/is/09-025r2/09-025r2.html#19 + */ + protected String getKvpParameter(Map> parameters, String key) { + Objects.requireNonNull(key, "KVP key cannot be null"); + // let's first try the default (CAML case) which should be more efficient + List values = parameters.get(key); + if (values == null) { + // then let's do an ignore case comparison of the key + keys: for (String k : parameters.keySet()) { + if (key.equalsIgnoreCase(k)) { + values = parameters.get(k); + break keys; + } + } + } + if (values == null) // nothing was found + return null; + if (values.size() != 1) { + // although not completely clear from the standard, we assume keys must be + // unique + // since lists are defined here + // https://docs.ogc.org/is/09-026r2/09-026r2.html#10 + throw new IllegalArgumentException("Key " + key + " as multiple values"); + } + String value = values.get(0); + assert value != null; + return value; + } + + protected void encodeCollectionAsGeoJSon(Stream features, OutputStream out, List typeNames) + throws IOException { long begin = System.currentTimeMillis(); AtomicLong count = new AtomicLong(0); JsonGenerator generator = Json.createGenerator(out); @@ -137,7 +184,16 @@ public class WfsHttpHandler implements HttpHandler { generator.write("type", "FeatureCollection"); generator.writeStartArray("features"); features.forEach((c) -> { - Geometry defaultGeometry = getDefaultGeometry(c); + // TODO deal with multiple type names + FeatureAdapter featureAdapter = null; + QName typeName = null; + if (!typeNames.isEmpty()) { + typeName = typeNames.get(0); + featureAdapter = featureAdapters.get(typeName); + } + + Geometry defaultGeometry = featureAdapter != null ? featureAdapter.getDefaultGeometry(c, typeName) + : getDefaultGeometry(c); if (defaultGeometry == null) return; generator.writeStartObject(); @@ -151,6 +207,8 @@ public class WfsHttpHandler implements HttpHandler { generator.writeStartObject("properties"); writeTimeProperties(generator, c); writeProperties(generator, c); + if (featureAdapter != null) + featureAdapter.writeProperties(generator, c, typeName); generator.writeEnd();// properties object generator.writeEnd();// feature object @@ -217,33 +275,6 @@ public class WfsHttpHandler implements HttpHandler { } - protected void writeAttr(JsonGenerator g, Content content, String attr) { - writeAttr(g, content, NamespaceUtils.parsePrefixedName(attr)); - } - - protected void writeAttr(JsonGenerator g, Content content, QNamed attr) { - writeAttr(g, content, attr.qName()); - } - - protected void writeAttr(JsonGenerator g, Content content, QName attr) { - // String value = content.attr(attr); - Object value = content.get(attr); - if (value != null) { - // TODO specify NamespaceContext - String key = NamespaceUtils.toPrefixedName(attr); - if (value instanceof Double v) - g.write(key, v); - else if (value instanceof Long v) - g.write(key, v); - else if (value instanceof Integer v) - g.write(key, v); - else if (value instanceof Boolean v) - g.write(key, v); - else - g.write(key, value.toString()); - } - } - protected void encodeCollectionAsGeoJSonOld(Stream features, OutputStream out) throws IOException { // BODY PROCESSING @@ -393,6 +424,36 @@ public class WfsHttpHandler implements HttpHandler { } + /* + * DEPENDENCY INJECTION + */ + + public void addFeatureAdapter(FeatureAdapter featureAdapter, Map properties) { + List typeNames = LangUtils.toStringList(properties.get(TYPE_NAMES)); + if (typeNames.isEmpty()) { + log.warn("FeatureAdapter " + featureAdapter.getClass() + " does not declare type names. Ignoring it..."); + return; + } + + for (String tn : typeNames) { + QName typeName = NamespaceUtils.parsePrefixedName(tn); + featureAdapters.put(typeName, featureAdapter); + } + } + + public void removeFeatureAdapter(FeatureAdapter featureAdapter, Map properties) { + List typeNames = LangUtils.toStringList(properties.get(TYPE_NAMES)); + if (!typeNames.isEmpty()) { + // ignore if noe type name declared + return; + } + + for (String tn : typeNames) { + QName typeName = NamespaceUtils.parsePrefixedName(tn); + featureAdapters.remove(typeName); + } + } + public void setContentRepository(ProvidedRepository contentRepository) { this.contentRepository = contentRepository; } diff --git a/org.argeo.app.geo/src/org/argeo/app/geo/http/WfsUtils.java b/org.argeo.app.geo/src/org/argeo/app/geo/http/WfsUtils.java new file mode 100644 index 0000000..f9876d9 --- /dev/null +++ b/org.argeo.app.geo/src/org/argeo/app/geo/http/WfsUtils.java @@ -0,0 +1,16 @@ +package org.argeo.app.geo.http; + +import javax.xml.namespace.NamespaceContext; + +/** Utilities around the WFS specifications. */ +public class WfsUtils { + + public static NamespaceContext parseNamespacesKvpParameter() { + // TODO deal with multiple namespaces + return null; + } + + /** singleton */ + private WfsUtils() { + } +} -- 2.30.2