GeoGson tooling
authorMathieu Baudier <mbaudier@argeo.org>
Wed, 13 Sep 2023 07:59:37 +0000 (09:59 +0200)
committerMathieu Baudier <mbaudier@argeo.org>
Wed, 13 Sep 2023 07:59:37 +0000 (09:59 +0200)
org.argeo.app.api/src/org/argeo/app/api/entity.xsd [new file with mode: 0644]
org.argeo.app.api/src/org/argeo/app/api/entityFeature.xsd [new file with mode: 0644]
org.argeo.app.core/src/org/argeo/app/core/SuiteContentNamespace.java
org.argeo.app.core/src/org/argeo/app/core/schemas/entity.xsd [deleted file]
org.argeo.app.geo/src/org/argeo/app/geo/GeoJSon.java [new file with mode: 0644]
org.argeo.app.geo/src/org/argeo/app/geo/GeoTools.java
org.argeo.app.geo/src/org/argeo/app/geo/GpxUtils.java
org.argeo.app.geo/src/org/argeo/app/geo/JTS.java [new file with mode: 0644]
org.argeo.app.geo/src/org/argeo/app/geo/http/WfsHttpHandler.java

diff --git a/org.argeo.app.api/src/org/argeo/app/api/entity.xsd b/org.argeo.app.api/src/org/argeo/app/api/entity.xsd
new file mode 100644 (file)
index 0000000..a2fbfcd
--- /dev/null
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
+       elementFormDefault="qualified" attributeFormDefault="unqualified"
+       targetNamespace="http://www.argeo.org/ns/entity"
+       xmlns:entity="http://www.argeo.org/ns/entity">
+
+       <xs:attribute name="date" type="xs:date" />
+
+       <xs:element name="local">
+               <xs:complexType>
+                       <xs:sequence>
+                               <xs:any minOccurs="0" maxOccurs="unbounded"
+                                       namespace="##local" processContents="lax" />
+                       </xs:sequence>
+                       <xs:anyAttribute namespace="##any"
+                               processContents="lax" />
+               </xs:complexType>
+       </xs:element>
+
+       <xs:element name="terms">
+               <xs:complexType>
+                       <xs:sequence minOccurs="0" maxOccurs="unbounded">
+                               <xs:element ref="entity:term"></xs:element>
+                       </xs:sequence>
+               </xs:complexType>
+       </xs:element>
+
+       <xs:element name="term">
+               <xs:complexType>
+                       <xs:sequence minOccurs="0" maxOccurs="unbounded">
+                               <xs:element ref="entity:term"></xs:element>
+                       </xs:sequence>
+                       <xs:anyAttribute namespace="##any"
+                               processContents="lax" />
+               </xs:complexType>
+       </xs:element>
+</xs:schema>
\ No newline at end of file
diff --git a/org.argeo.app.api/src/org/argeo/app/api/entityFeature.xsd b/org.argeo.app.api/src/org/argeo/app/api/entityFeature.xsd
new file mode 100644 (file)
index 0000000..805bd1a
--- /dev/null
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
+       xmlns="http://apaf.on.netiket.eu/ns/apaf"
+       targetNamespace="http://apaf.on.netiket.eu/ns/apaf"
+       xmlns:entity="http://www.argeo.org/ns/entity" xmlns:dav="DAV:"
+       xmlns:gml="http://www.opengis.net/gml">
+<!--   <xs:import -->
+<!--           schemaLocation="entity.xsd" -->
+<!--           namespace="http://www.argeo.org/ns/entity"></xs:import> -->
+       <!-- <xs:import -->
+       <!-- schemaLocation="https://schemas.opengis.net/gml/3.2.1/gml.xsd" -->
+       <!-- namespace="http://www.opengis.net/gml/3.2"></xs:import> -->
+       <xs:import namespace="http://www.opengis.net/gml"
+               schemaLocation="http://schemas.opengis.net/gml/3.1.1/base/gml.xsd" />
+
+       <xs:complexType name="entityFeatureType">
+               <xs:complexContent>
+                       <xs:extension base="gml:AbstractFeatureType">
+                               <xs:sequence>
+                                       <xs:element name="area" type="gml:PolygonPropertyType" />
+                                       <xs:element name="geopoint" type="gml:PointPropertyType" />
+                                       <xs:element name="path" type="xs:string" />
+                               </xs:sequence>
+                       </xs:extension>
+               </xs:complexContent>
+       </xs:complexType>
+
+       <xs:element name="entityFeature" type="entityFeatureType"
+               substitutionGroup="gml:_Feature" />
+
+       <!-- <xs:complexType name="TestFeatureCollectionType"> <xs:complexContent> 
+               <xs:extension base="gml:AbstractFeatureCollectionType" /> </xs:complexContent> 
+               </xs:complexType> <xs:element name="TestFeatureCollection" type="TestFeatureCollectionType" 
+               /> -->
+
+</xs:schema>
\ No newline at end of file
index 48c508b45c54c6697ad84569d7335578a5e30fec..6b5ab3c45bed02dd651c5acf76ca0871e86dd99e 100644 (file)
@@ -10,7 +10,7 @@ public enum SuiteContentNamespace implements ContentNamespace {
        //
        // ARGEO
        //
-       ENTITY("entity", "http://www.argeo.org/ns/entity", "entity.xsd", null),
+       ENTITY("entity", "http://www.argeo.org/ns/entity", "/org/argeo/app/api/entity.xsd", null),
        //
        ARGEO_DBK("argeodbk", "http://www.argeo.org/ns/argeodbk", null, null),
        //
@@ -62,7 +62,10 @@ public enum SuiteContentNamespace implements ContentNamespace {
                Objects.requireNonNull(namespace);
                this.namespace = namespace;
                if (resourceFileName != null) {
-                       resource = getClass().getResource(RESOURCE_BASE + resourceFileName);
+                       if (!resourceFileName.startsWith("/"))
+                               resource = getClass().getResource(RESOURCE_BASE + resourceFileName);
+                       else
+                               resource = getClass().getResource(resourceFileName);
                        Objects.requireNonNull(resource);
                }
                if (publicUrl != null)
diff --git a/org.argeo.app.core/src/org/argeo/app/core/schemas/entity.xsd b/org.argeo.app.core/src/org/argeo/app/core/schemas/entity.xsd
deleted file mode 100644 (file)
index a2fbfcd..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
-       elementFormDefault="qualified" attributeFormDefault="unqualified"
-       targetNamespace="http://www.argeo.org/ns/entity"
-       xmlns:entity="http://www.argeo.org/ns/entity">
-
-       <xs:attribute name="date" type="xs:date" />
-
-       <xs:element name="local">
-               <xs:complexType>
-                       <xs:sequence>
-                               <xs:any minOccurs="0" maxOccurs="unbounded"
-                                       namespace="##local" processContents="lax" />
-                       </xs:sequence>
-                       <xs:anyAttribute namespace="##any"
-                               processContents="lax" />
-               </xs:complexType>
-       </xs:element>
-
-       <xs:element name="terms">
-               <xs:complexType>
-                       <xs:sequence minOccurs="0" maxOccurs="unbounded">
-                               <xs:element ref="entity:term"></xs:element>
-                       </xs:sequence>
-               </xs:complexType>
-       </xs:element>
-
-       <xs:element name="term">
-               <xs:complexType>
-                       <xs:sequence minOccurs="0" maxOccurs="unbounded">
-                               <xs:element ref="entity:term"></xs:element>
-                       </xs:sequence>
-                       <xs:anyAttribute namespace="##any"
-                               processContents="lax" />
-               </xs:complexType>
-       </xs:element>
-</xs:schema>
\ No newline at end of file
diff --git a/org.argeo.app.geo/src/org/argeo/app/geo/GeoJSon.java b/org.argeo.app.geo/src/org/argeo/app/geo/GeoJSon.java
new file mode 100644 (file)
index 0000000..b590814
--- /dev/null
@@ -0,0 +1,81 @@
+package org.argeo.app.geo;
+
+import org.locationtech.jts.geom.Coordinate;
+import org.locationtech.jts.geom.Envelope;
+import org.locationtech.jts.geom.Geometry;
+import org.locationtech.jts.geom.LineString;
+import org.locationtech.jts.geom.LinearRing;
+import org.locationtech.jts.geom.Point;
+import org.locationtech.jts.geom.Polygon;
+
+import jakarta.json.stream.JsonGenerator;
+
+/**
+ * GeoJSon format.
+ * 
+ * @see https://datatracker.ietf.org/doc/html/rfc7946
+ */
+public class GeoJSon {
+       public static void writeBBox(JsonGenerator generator, Geometry geometry) {
+               generator.writeStartArray("bbox");
+               Envelope envelope = geometry.getEnvelopeInternal();
+               generator.write(envelope.getMinX());
+               generator.write(envelope.getMinY());
+               generator.write(envelope.getMaxX());
+               generator.write(envelope.getMaxY());
+               generator.writeEnd();
+       }
+
+       public static void writeGeometry(JsonGenerator generator, Geometry geometry) {
+               generator.writeStartObject("geometry");
+               if (geometry instanceof Point point) {
+                       generator.write("type", "Point");
+                       generator.writeStartArray("coordinates");
+                       writeCoordinate(generator, point.getCoordinate());
+                       generator.writeEnd();// coordinates array
+               } else if (geometry instanceof LineString lineString) {
+                       generator.write("type", "LineString");
+                       generator.writeStartArray("coordinates");
+                       writeCoordinates(generator, lineString.getCoordinates());
+                       generator.writeEnd();// coordinates array
+               } else if (geometry instanceof Polygon polygon) {
+                       generator.write("type", "Polygon");
+                       generator.writeStartArray("coordinates");
+                       LinearRing exteriorRing = polygon.getExteriorRing();
+                       generator.writeStartArray();
+                       writeCoordinates(generator, exteriorRing.getCoordinates());
+                       generator.writeEnd();
+                       for (int i = 0; i < polygon.getNumInteriorRing(); i++) {
+                               LinearRing interiorRing = polygon.getInteriorRingN(i);
+                               // TODO verify that holes are clockwise
+                               generator.writeStartArray();
+                               writeCoordinates(generator, interiorRing.getCoordinates());
+                               generator.writeEnd();
+                       }
+                       generator.writeEnd();// coordinates array
+               }
+               generator.writeEnd();// geometry object
+       }
+
+       public static void writeCoordinates(JsonGenerator generator, Coordinate[] coordinates) {
+               for (Coordinate coordinate : coordinates) {
+                       generator.writeStartArray();
+                       writeCoordinate(generator, coordinate);
+                       generator.writeEnd();
+               }
+       }
+
+       public static void writeCoordinate(JsonGenerator generator, Coordinate coordinate) {
+               generator.write(coordinate.getX());
+               generator.write(coordinate.getY());
+               double z = coordinate.getZ();
+               if (!Double.isNaN(z)) {
+                       generator.write(z);
+               }
+       }
+
+       /** singleton */
+       private GeoJSon() {
+       }
+
+}
index 166c686a4386089895da398f8b77325d389f59a8..dbaa985a4f50c13cd85428866703f95ef2644c95 100644 (file)
@@ -2,9 +2,7 @@ package org.argeo.app.geo;
 
 import org.argeo.api.cms.CmsLog;
 import org.geotools.factory.CommonFactoryFinder;
-import org.geotools.geometry.jts.JTSFactoryFinder;
 import org.geotools.styling.StyleFactory;
-import org.locationtech.jts.geom.GeometryFactory;
 import org.opengis.filter.FilterFactory2;
 
 /**
@@ -16,13 +14,11 @@ import org.opengis.filter.FilterFactory2;
 public class GeoTools {
        private final static CmsLog log = CmsLog.getLog(GeoTools.class);
 
-       public final static GeometryFactory GEOMETRY_FACTORY;
        public final static StyleFactory STYLE_FACTORY;
        public final static FilterFactory2 FILTER_FACTORY;
 
        static {
                try {
-                       GEOMETRY_FACTORY = JTSFactoryFinder.getGeometryFactory();
                        STYLE_FACTORY = CommonFactoryFinder.getStyleFactory();
                        FILTER_FACTORY = CommonFactoryFinder.getFilterFactory2();
                } catch (RuntimeException e) {
index 15f6b89675e5a03cf16bc03882271baa36ce6f5b..2bcff158712a38eb5a32059ea2a48fcf2e2ddc1f 100644 (file)
@@ -52,7 +52,7 @@ public class GpxUtils {
         */
        @SuppressWarnings("unchecked")
        public static <T> T parseGpxTrackTo(InputStream in, Class<T> clss) throws IOException {
-               GeometryFactory geometryFactory = GeoTools.GEOMETRY_FACTORY;
+               GeometryFactory geometryFactory = JTS.GEOMETRY_FACTORY;
                List<Coordinate> coordinates = new ArrayList<>();
                try {
                        SAXParserFactory factory = SAXParserFactory.newInstance();
diff --git a/org.argeo.app.geo/src/org/argeo/app/geo/JTS.java b/org.argeo.app.geo/src/org/argeo/app/geo/JTS.java
new file mode 100644 (file)
index 0000000..2623712
--- /dev/null
@@ -0,0 +1,27 @@
+package org.argeo.app.geo;
+
+import org.argeo.api.cms.CmsLog;
+import org.locationtech.jts.geom.GeometryFactory;
+
+/**
+ * Factories initialisation and workarounds for the JTS library. The idea is to
+ * code defensively around factory initialisation, API changes, and issues
+ * related to running in an OSGi environment. Rather see {@link GeoUtils} for
+ * functional static utilities.
+ */
+public class JTS {
+       private final static CmsLog log = CmsLog.getLog(JTS.class);
+
+       public final static GeometryFactory GEOMETRY_FACTORY;
+
+       static {
+               try {
+                       // GEOMETRY_FACTORY = JTSFactoryFinder.getGeometryFactory();
+                       GEOMETRY_FACTORY = new GeometryFactory();
+               } catch (RuntimeException e) {
+                       log.error("Basic JTS initialisation failed, geographical utilities are probably not available", e);
+                       throw e;
+               }
+       }
+
+}
index b10a10f4b186844500034fc65b97948c6696a648..d60d51d788249af5d334f4b632d7095da2e606a3 100644 (file)
@@ -1,26 +1,33 @@
 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.List;
 import java.util.Map;
+import java.util.concurrent.atomic.AtomicLong;
 import java.util.stream.Stream;
 
 import javax.xml.namespace.QName;
 
 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.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.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.http.HttpHeader;
 import org.argeo.cms.http.server.HttpServerUtils;
 import org.geotools.data.DataUtilities;
@@ -36,6 +43,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 +54,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
@@ -107,88 +119,139 @@ public class WfsHttpHandler implements HttpHandler {
 
                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);
+               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);
+                       }
+               }
+       }
 
-                       SimpleFeatureType featureType = gml.decodeSimpleFeatureType(schemaLocation,
-                                       new NameImpl(namespace, entityType + "Feature"));
+       protected void encodeCollectionAsGeoJSon(Stream<Content> features, OutputStream out) 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) -> {
+                       Geometry defaultGeometry = 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);
+                       GeoJSon.writeGeometry(generator, defaultGeometry);
+
+                       generator.writeStartObject("properties");
+                       writeTimeProperties(generator, c);
+                       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);
+                               }
+               });
+               generator.writeEnd();// features array
+               generator.writeEnd().close();
 
-//                     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);
+               log.debug("GeoJSon encoding took " + (System.currentTimeMillis() - begin) + " ms.");
+       }
 
-                       SimpleFeatureBuilder featureBuilder = new SimpleFeatureBuilder(featureType);
+       protected Geometry getDefaultGeometry(Content content) {
+               if (content.hasContentClass(EntityType.geopoint)) {
+                       double latitude = content.get(WGS84PosName.lat, Double.class).get();
+                       double longitude = content.get(WGS84PosName.lng, Double.class).get();
 
-                       DefaultFeatureCollection featureCollection = new DefaultFeatureCollection();
+                       Coordinate coordinate = new Coordinate(longitude, latitude);
+                       Point the_geom = JTS.GEOMETRY_FACTORY.createPoint(coordinate);
+                       return the_geom;
+               }
+               return null;
+       }
 
-                       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 String getFeatureId(Content content) {
+               String uuid = content.attr(LdapAttr.entryUUID);
+               return uuid;
+       }
 
-                                       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.lng, Double.class).get();
+       private final QName JCR_CREATED = NamespaceUtils.parsePrefixedName("jcr:created");
+
+       private final QName JCR_LAST_MODIFIED = NamespaceUtils.parsePrefixedName("jcr:lastModified");
+
+       protected void writeTimeProperties(JsonGenerator g, Content content) {
+               String creationDate = content.attr(DName.creationdate);
+               if (creationDate == null)
+                       creationDate = content.attr(JCR_CREATED);
+               if (creationDate != null)
+                       g.write(DName.creationdate.get(), creationDate);
+               String lastModified = content.attr(DName.getlastmodified);
+               if (lastModified == null)
+                       lastModified = content.attr(JCR_LAST_MODIFIED);
+               if (lastModified != null)
+                       g.write(DName.getlastmodified.get(), lastModified);
+       }
 
-                                       Coordinate coordinate = new Coordinate(longitude, latitude);
-                                       the_geom = GeoTools.GEOMETRY_FACTORY.createPoint(coordinate);
-                               }
+       protected 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<QName> contentClasses = content.getContentClasses();
+                       if (!contentClasses.isEmpty()) {
+                               generator.write("type", NamespaceUtils.toPrefixedName(contentClasses.get(0)));
+                       }
+               }
 
-//                             }
-                               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<AttributeDescriptor> 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);
+       protected void writeAttr(JsonGenerator g, Content content, String attr) {
+               writeAttr(g, content, NamespaceUtils.parsePrefixedName(attr));
+       }
 
-                               SimpleFeature feature = featureBuilder.buildFeature(uuid);
-                               featureCollection.add(feature);
+       protected void writeAttr(JsonGenerator g, Content content, QNamed attr) {
+               writeAttr(g, content, attr.qName());
+       }
 
-                       });
-                       gml.encode(exchange.getResponseBody(), featureCollection);
-                       exchange.getResponseBody().close();
+       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());
+               }
+       }
 
-               } else if ("application/json".equals(outputFormat)) {
+       protected void encodeCollectionAsGeoJSonOld(Stream<Content> 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 +266,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);
@@ -250,8 +313,83 @@ public class WfsHttpHandler implements HttpHandler {
                                        throw new UncheckedIOException(e);
                                }
                        });
-                       geoJSONWriter.close();
                }
+       }
+
+       protected void encodeCollectionAsGML(Stream<Content> 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.lng, 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<AttributeDescriptor> 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();
 
        }