X-Git-Url: https://git.argeo.org/?a=blobdiff_plain;f=org.argeo.app.geo%2Fsrc%2Forg%2Fargeo%2Fapp%2Fgeo%2Fhttp%2FWfsHttpHandler.java;h=84b9893082f3f324c9e2952fd8afaf82ef81cdcd;hb=b96d17f3eb275a97109cc160db1cfd3731a01d31;hp=b10a10f4b186844500034fc65b97948c6696a648;hpb=7877822780b4d4b98e117e403a4c34034807dd54;p=gpl%2Fargeo-suite.git 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 b10a10f..84b9893 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 @@ -1,11 +1,17 @@ package org.argeo.app.geo.http; +import java.io.BufferedOutputStream; import java.io.IOException; 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; import javax.xml.namespace.QName; @@ -14,15 +20,21 @@ import org.argeo.api.acr.Content; import org.argeo.api.acr.ContentSession; import org.argeo.api.acr.NamespaceUtils; 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; import org.argeo.app.api.EntityType; import org.argeo.app.api.WGS84PosName; +import org.argeo.app.api.geo.FeatureAdapter; import org.argeo.app.geo.CqlUtils; -import org.argeo.app.geo.GeoTools; +import org.argeo.app.geo.GeoJson; import org.argeo.app.geo.GpxUtils; +import org.argeo.app.geo.JTS; +import org.argeo.cms.acr.json.AcrJsonUtils; 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; @@ -36,6 +48,7 @@ import org.geotools.wfs.GML.Version; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Point; import org.locationtech.jts.geom.Polygon; import org.opengis.feature.GeometryAttribute; import org.opengis.feature.simple.SimpleFeature; @@ -46,8 +59,12 @@ import org.opengis.feature.type.Name; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; +import jakarta.json.Json; +import jakarta.json.stream.JsonGenerator; + /** A partially implemented WFS 2.0 server. */ public class WfsHttpHandler implements HttpHandler { + private final static CmsLog log = CmsLog.getLog(WfsHttpHandler.class); private ProvidedRepository contentRepository; // HTTP parameters @@ -55,6 +72,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); @@ -62,9 +81,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"; } @@ -81,114 +100,175 @@ 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); } else { - search.from(path).where((and) -> { - }); + search.from(path); } - 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); - if ("GML3".equals(outputFormat)) { - String entityType = "apafField"; - URL schemaLocation = new URL("http://localhost:7070/pkg/eu.netiket.on.apaf/apaf.xsd"); - String namespace = "http://apaf.on.netiket.eu/ns/apaf"; - - GML gml = new GML(Version.WFS1_1); - gml.setCoordinateReferenceSystem(DefaultGeographicCRS.WGS84); - gml.setNamespace("local", namespace); - - SimpleFeatureType featureType = gml.decodeSimpleFeatureType(schemaLocation, - new NameImpl(namespace, entityType + "Feature")); - -// CoordinateReferenceSystem crs=DefaultGeographicCRS.WGS84; -// QName featureName = new QName(namespace,"apafFieldFeature"); -// GMLConfiguration configuration = new GMLConfiguration(); -// FeatureType parsed = GTXML.parseFeatureType(configuration, featureName, crs); -// SimpleFeatureType featureType = DataUtilities.simple(parsed); - - SimpleFeatureBuilder featureBuilder = new SimpleFeatureBuilder(featureType); + final int BUFFER_SIZE = 100 * 1024; + try (BufferedOutputStream out = new BufferedOutputStream(exchange.getResponseBody(), BUFFER_SIZE)) { + if ("GML3".equals(outputFormat)) { + encodeCollectionAsGML(res, out); + } else if ("application/json".equals(outputFormat)) { + encodeCollectionAsGeoJSon(res, out, typeNames); + } + } + } - DefaultFeatureCollection featureCollection = new DefaultFeatureCollection(); + /** + * 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; + } - res.forEach((c) -> { -// boolean gpx = false; - Geometry the_geom = null; - Polygon the_area = null; -// if (gpx) { - Content area = c.getContent("gpx/area.gpx").orElse(null); - if (area != null) { + 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); + generator.writeStartObject(); + generator.write("type", "FeatureCollection"); + generator.writeStartArray("features"); + features.forEach((c) -> { + // TODO deal with multiple type names + FeatureAdapter featureAdapter = null; + QName typeName = null; + if (!typeNames.isEmpty()) { + typeName = typeNames.get(0); + featureAdapter = featureAdapters.get(typeName); + } - try (InputStream in = area.open(InputStream.class)) { - the_area = GpxUtils.parseGpxTrackTo(in, Polygon.class); - } catch (IOException e) { - throw new UncheckedIOException("Cannot parse " + c, e); - } + Geometry defaultGeometry = featureAdapter != null ? featureAdapter.getDefaultGeometry(c, typeName) + : getDefaultGeometry(c); + if (defaultGeometry == null) + return; + generator.writeStartObject(); + generator.write("type", "Feature"); + String featureId = getFeatureId(c); + if (featureId != null) + generator.write("id", featureId); +// GeoJson.writeBBox(generator, defaultGeometry); + generator.writeStartObject(GeoJson.GEOMETRY); + GeoJson.writeGeometry(generator, defaultGeometry); + generator.writeEnd();// geometry object + + generator.writeStartObject(GeoJson.PROPERTIES); + AcrJsonUtils.writeTimeProperties(generator, c); + if (featureAdapter != null) + featureAdapter.writeProperties(generator, c, typeName); + else + writeProperties(generator, c); + generator.writeEnd();// properties object + + generator.writeEnd();// feature object + + if (count.incrementAndGet() % 10 == 0) + try { + out.flush(); + } catch (IOException e) { + throw new UncheckedIOException(e); } -// } else { - if (c.hasContentClass(EntityType.geopoint)) { - double latitude = c.get(WGS84PosName.lat, Double.class).get(); - double longitude = c.get(WGS84PosName.lng, Double.class).get(); + }); + generator.writeEnd();// features array + generator.writeEnd().close(); - Coordinate coordinate = new Coordinate(longitude, latitude); - the_geom = GeoTools.GEOMETRY_FACTORY.createPoint(coordinate); - } + log.debug("GeoJSon encoding took " + (System.currentTimeMillis() - begin) + " ms."); + } -// } - if (the_geom != null) - featureBuilder.set(new NameImpl(namespace, "geopoint"), the_geom); - if (the_area != null) - featureBuilder.set(new NameImpl(namespace, "area"), the_area); - - List attrDescs = featureType.getAttributeDescriptors(); - for (AttributeDescriptor attrDesc : attrDescs) { - if (attrDesc instanceof GeometryAttribute) - continue; - Name name = attrDesc.getName(); - QName qName = new QName(name.getNamespaceURI(), name.getLocalPart()); - String value = c.attr(qName); - if (value == null) { - value = c.attr(name.getLocalPart()); - } - if (value != null) { - featureBuilder.set(name, value); - } - } + protected Geometry getDefaultGeometry(Content content) { + if (content.hasContentClass(EntityType.geopoint)) { + double latitude = content.get(WGS84PosName.lat, Double.class).get(); + double longitude = content.get(WGS84PosName.lon, Double.class).get(); - String uuid = c.attr(LdapAttr.entryUUID); + Coordinate coordinate = new Coordinate(longitude, latitude); + Point the_geom = JTS.GEOMETRY_FACTORY.createPoint(coordinate); + return the_geom; + } + return null; + } - SimpleFeature feature = featureBuilder.buildFeature(uuid); - featureCollection.add(feature); + protected String getFeatureId(Content content) { + String uuid = content.attr(LdapAttr.entryUUID); + return uuid; + } - }); - gml.encode(exchange.getResponseBody(), featureCollection); - exchange.getResponseBody().close(); + public void writeProperties(JsonGenerator generator, Content content) { + String path = content.getPath(); + generator.write("path", path); + if (content.hasContentClass(EntityType.local)) { + String type = content.attr(EntityName.type); + generator.write("type", type); + } else { + List contentClasses = content.getContentClasses(); + if (!contentClasses.isEmpty()) { + generator.write("type", NamespaceUtils.toPrefixedName(contentClasses.get(0))); + } + } + + } - } else if ("application/json".equals(outputFormat)) { + protected void encodeCollectionAsGeoJSonOld(Stream features, OutputStream out) throws IOException { - // BODY PROCESSING - GeoJSONWriter geoJSONWriter = new GeoJSONWriter(exchange.getResponseBody()); + // BODY PROCESSING + try (GeoJSONWriter geoJSONWriter = new GeoJSONWriter(out)) { geoJSONWriter.setPrettyPrinting(true); + geoJSONWriter.setEncodeFeatureBounds(true); - boolean gpx = false; + boolean gpx = true; SimpleFeatureType TYPE; try { if (gpx) @@ -203,7 +283,7 @@ public class WfsHttpHandler implements HttpHandler { SimpleFeatureBuilder featureBuilder = new SimpleFeatureBuilder(TYPE); GeometryFactory geometryFactory = JTSFactoryFinder.getGeometryFactory(); - res.forEach((c) -> { + features.forEach((c) -> { Geometry the_geom; if (gpx) {// experimental Content area = c.getContent("gpx/area.gpx").orElse(null); @@ -220,7 +300,7 @@ public class WfsHttpHandler implements HttpHandler { return; double latitude = c.get(WGS84PosName.lat, Double.class).get(); - double longitude = c.get(WGS84PosName.lng, Double.class).get(); + double longitude = c.get(WGS84PosName.lon, Double.class).get(); Coordinate coordinate = new Coordinate(longitude, latitude); the_geom = geometryFactory.createPoint(coordinate); @@ -250,9 +330,114 @@ public class WfsHttpHandler implements HttpHandler { throw new UncheckedIOException(e); } }); - geoJSONWriter.close(); + } + } + + protected void encodeCollectionAsGML(Stream features, OutputStream out) throws IOException { + String entityType = "entity"; + URL schemaLocation = getClass().getResource("/org/argeo/app/api/entity.xsd"); + String namespace = "http://www.argeo.org/ns/entity"; + + GML gml = new GML(Version.WFS1_1); + gml.setCoordinateReferenceSystem(DefaultGeographicCRS.WGS84); + gml.setNamespace("local", namespace); + + SimpleFeatureType featureType = gml.decodeSimpleFeatureType(schemaLocation, + new NameImpl(namespace, entityType + "Feature")); + +// CoordinateReferenceSystem crs=DefaultGeographicCRS.WGS84; +// QName featureName = new QName(namespace,"apafFieldFeature"); +// GMLConfiguration configuration = new GMLConfiguration(); +// FeatureType parsed = GTXML.parseFeatureType(configuration, featureName, crs); +// SimpleFeatureType featureType = DataUtilities.simple(parsed); + + SimpleFeatureBuilder featureBuilder = new SimpleFeatureBuilder(featureType); + + DefaultFeatureCollection featureCollection = new DefaultFeatureCollection(); + + features.forEach((c) -> { +// boolean gpx = false; + Geometry the_geom = null; + Polygon the_area = null; +// if (gpx) { + Content area = c.getContent("gpx/area.gpx").orElse(null); + if (area != null) { + + try (InputStream in = area.open(InputStream.class)) { + the_area = GpxUtils.parseGpxTrackTo(in, Polygon.class); + } catch (IOException e) { + throw new UncheckedIOException("Cannot parse " + c, e); + } + } +// } else { + 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); + the_geom = JTS.GEOMETRY_FACTORY.createPoint(coordinate); + } + +// } + if (the_geom != null) + featureBuilder.set(new NameImpl(namespace, "geopoint"), the_geom); + if (the_area != null) + featureBuilder.set(new NameImpl(namespace, "area"), the_area); + + List attrDescs = featureType.getAttributeDescriptors(); + for (AttributeDescriptor attrDesc : attrDescs) { + if (attrDesc instanceof GeometryAttribute) + continue; + Name name = attrDesc.getName(); + QName qName = new QName(name.getNamespaceURI(), name.getLocalPart()); + String value = c.attr(qName); + if (value == null) { + value = c.attr(name.getLocalPart()); + } + if (value != null) { + featureBuilder.set(name, value); + } + } + + String uuid = c.attr(LdapAttr.entryUUID); + + SimpleFeature feature = featureBuilder.buildFeature(uuid); + featureCollection.add(feature); + + }); + gml.encode(out, featureCollection); + out.close(); + + } + + /* + * 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) {