GeoJSON HTTP service
authorMathieu Baudier <mbaudier@argeo.org>
Thu, 31 Aug 2023 12:20:47 +0000 (14:20 +0200)
committerMathieu Baudier <mbaudier@argeo.org>
Thu, 31 Aug 2023 12:20:47 +0000 (14:20 +0200)
Makefile
org.argeo.app.geo.js/package-lock.json
org.argeo.app.geo/.project
org.argeo.app.geo/OSGI-INF/geoJsonHttpHandler.xml [new file with mode: 0644]
org.argeo.app.geo/bnd.bnd
org.argeo.app.geo/src/org/argeo/app/geo/CqlUtils.java [new file with mode: 0644]
org.argeo.app.geo/src/org/argeo/app/internal/geo/http/GeoJsonHttpHandler.java [new file with mode: 0644]
org.argeo.app.geo/src/org/argeo/app/internal/geo/http/GmlHttpHandler.java [new file with mode: 0644]

index 092541769fb3a9b741ca56c673e15072fc76cda4..c41b17e8ac563f5e4e3cb1f638a37a857cbea72f 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -48,7 +48,7 @@ clean:
 
 ## WEB
 web:
-       cd org.argeo.app.geo.js && npm run build-prod
+       cd org.argeo.app.geo.js && npm run build
 
 npm-install:
        cd org.argeo.app.geo.js && npm install
index 775cf049337b825120b4b75e7b112c90ec0af7cd..68f78432ab00401af591202d30e6045adee07bf1 100644 (file)
@@ -24,9 +24,9 @@
                        }
                },
                "node_modules/@babel/parser": {
-                       "version": "7.22.13",
-                       "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.13.tgz",
-                       "integrity": "sha512-3l6+4YOvc9wx7VlCSw4yQfcBo01ECA8TicQfbnCPuCEpRQrf+gTUyGdxNw+pyTUyywp6JRD1w0YQs9TpBXYlkw==",
+                       "version": "7.22.14",
+                       "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.14.tgz",
+                       "integrity": "sha512-1KucTHgOvaw/LzCVrEOAyXkr9rQlp0A1HiHRYnSUE9dmb8PvPW7o5sscg+5169r54n3vGlbx6GevTE/Iw/P3AQ==",
                        "dev": true,
                        "bin": {
                                "parser": "bin/babel-parser.js"
                        "dev": true
                },
                "node_modules/@types/linkify-it": {
-                       "version": "3.0.2",
-                       "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz",
-                       "integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==",
+                       "version": "3.0.3",
+                       "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.3.tgz",
+                       "integrity": "sha512-pTjcqY9E4nOI55Wgpz7eiI8+LzdYnw3qxXCfHyBDdPbYvbyLgWLJGh8EdPvqawwMK1Uo1794AUkkR38Fr0g+2g==",
                        "dev": true
                },
                "node_modules/@types/markdown-it": {
                        }
                },
                "node_modules/caniuse-lite": {
-                       "version": "1.0.30001524",
-                       "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001524.tgz",
-                       "integrity": "sha512-Jj917pJtYg9HSJBF95HVX3Cdr89JUyLT4IZ8SvM5aDRni95swKgYi3TgYLH5hnGfPE/U1dg6IfZ50UsIlLkwSA==",
+                       "version": "1.0.30001525",
+                       "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001525.tgz",
+                       "integrity": "sha512-/3z+wB4icFt3r0USMwxujAqRvaD/B7rvGTsKhbhSQErVrJvkZCLhgNLJxU8MevahQVH6hCU9FsHdNUFbiwmE7Q==",
                        "dev": true,
                        "funding": [
                                {
                        "dev": true
                },
                "node_modules/electron-to-chromium": {
-                       "version": "1.4.505",
-                       "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.505.tgz",
-                       "integrity": "sha512-0A50eL5BCCKdxig2SsCXhpuztnB9PfUgRMojj5tMvt8O54lbwz3t6wNgnpiTRosw5QjlJB7ixhVyeg8daLQwSQ==",
+                       "version": "1.4.506",
+                       "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.506.tgz",
+                       "integrity": "sha512-xxGct4GPAKSRlrLBtJxJFYy74W11zX6PO9GyHgl/U+2s3Dp0ZEwAklDfNHXOWcvH7zWMpsmgbR0ggEuaYAVvHA==",
                        "dev": true
                },
                "node_modules/emoji-regex": {
                        }
                },
                "node_modules/glob": {
-                       "version": "10.3.3",
-                       "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.3.tgz",
-                       "integrity": "sha512-92vPiMb/iqpmEgsOoIDvTjc50wf9CCCvMzsi6W0JLPeUKE8TWP1a73PgqSrqy7iAZxaSD1YdzU7QZR5LF51MJw==",
+                       "version": "10.3.4",
+                       "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.4.tgz",
+                       "integrity": "sha512-6LFElP3A+i/Q8XQKEvZjkEWEOTgAIALR9AO2rwT8bgPhDd1anmqDJDZ6lLddI4ehxxxR1S5RIqKe1uapMQfYaQ==",
                        "dev": true,
                        "dependencies": {
                                "foreground-child": "^3.1.0",
        },
        "dependencies": {
                "@babel/parser": {
-                       "version": "7.22.13",
-                       "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.13.tgz",
-                       "integrity": "sha512-3l6+4YOvc9wx7VlCSw4yQfcBo01ECA8TicQfbnCPuCEpRQrf+gTUyGdxNw+pyTUyywp6JRD1w0YQs9TpBXYlkw==",
+                       "version": "7.22.14",
+                       "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.14.tgz",
+                       "integrity": "sha512-1KucTHgOvaw/LzCVrEOAyXkr9rQlp0A1HiHRYnSUE9dmb8PvPW7o5sscg+5169r54n3vGlbx6GevTE/Iw/P3AQ==",
                        "dev": true
                },
                "@colors/colors": {
                        "dev": true
                },
                "@types/linkify-it": {
-                       "version": "3.0.2",
-                       "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz",
-                       "integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==",
+                       "version": "3.0.3",
+                       "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.3.tgz",
+                       "integrity": "sha512-pTjcqY9E4nOI55Wgpz7eiI8+LzdYnw3qxXCfHyBDdPbYvbyLgWLJGh8EdPvqawwMK1Uo1794AUkkR38Fr0g+2g==",
                        "dev": true
                },
                "@types/markdown-it": {
                        }
                },
                "caniuse-lite": {
-                       "version": "1.0.30001524",
-                       "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001524.tgz",
-                       "integrity": "sha512-Jj917pJtYg9HSJBF95HVX3Cdr89JUyLT4IZ8SvM5aDRni95swKgYi3TgYLH5hnGfPE/U1dg6IfZ50UsIlLkwSA==",
+                       "version": "1.0.30001525",
+                       "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001525.tgz",
+                       "integrity": "sha512-/3z+wB4icFt3r0USMwxujAqRvaD/B7rvGTsKhbhSQErVrJvkZCLhgNLJxU8MevahQVH6hCU9FsHdNUFbiwmE7Q==",
                        "dev": true
                },
                "catharsis": {
                        "dev": true
                },
                "electron-to-chromium": {
-                       "version": "1.4.505",
-                       "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.505.tgz",
-                       "integrity": "sha512-0A50eL5BCCKdxig2SsCXhpuztnB9PfUgRMojj5tMvt8O54lbwz3t6wNgnpiTRosw5QjlJB7ixhVyeg8daLQwSQ==",
+                       "version": "1.4.506",
+                       "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.506.tgz",
+                       "integrity": "sha512-xxGct4GPAKSRlrLBtJxJFYy74W11zX6PO9GyHgl/U+2s3Dp0ZEwAklDfNHXOWcvH7zWMpsmgbR0ggEuaYAVvHA==",
                        "dev": true
                },
                "emoji-regex": {
                        "dev": true
                },
                "glob": {
-                       "version": "10.3.3",
-                       "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.3.tgz",
-                       "integrity": "sha512-92vPiMb/iqpmEgsOoIDvTjc50wf9CCCvMzsi6W0JLPeUKE8TWP1a73PgqSrqy7iAZxaSD1YdzU7QZR5LF51MJw==",
+                       "version": "10.3.4",
+                       "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.4.tgz",
+                       "integrity": "sha512-6LFElP3A+i/Q8XQKEvZjkEWEOTgAIALR9AO2rwT8bgPhDd1anmqDJDZ6lLddI4ehxxxR1S5RIqKe1uapMQfYaQ==",
                        "dev": true,
                        "requires": {
                                "foreground-child": "^3.1.0",
index 674edda9430a723a96b3fcfdb135c0e6cee8d4e1..a41594e7ce35515aaca7fdb038a32aeacb02ef2c 100644 (file)
                        <arguments>
                        </arguments>
                </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.pde.ds.core.builder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
        </buildSpec>
        <natures>
                <nature>org.eclipse.pde.PluginNature</nature>
diff --git a/org.argeo.app.geo/OSGI-INF/geoJsonHttpHandler.xml b/org.argeo.app.geo/OSGI-INF/geoJsonHttpHandler.xml
new file mode 100644 (file)
index 0000000..4fbec8f
--- /dev/null
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" immediate="true">
+   <implementation class="org.argeo.app.internal.geo.http.GeoJsonHttpHandler"/>
+   <service>
+      <provide interface="com.sun.net.httpserver.HttpHandler"/>
+   </service>
+   <property name="context.path" type="String" value="/api/geojson/" />
+   <property name="context.public" type="String" value="true" />
+   <reference bind="setContentRepository" cardinality="1..1" interface="org.argeo.api.acr.spi.ProvidedRepository" name="ProvidedRepository" policy="static"/>
+</scr:component>
index 665728497c4c60e1164b37ca90e305ef890b7333..2076ba17ea8ab7b3178e7a12f63b506b0cdf03c9 100644 (file)
@@ -2,3 +2,6 @@ Import-Package:\
 tech.units.indriya.unit,\
 com.fasterxml.jackson.core,\
 *
+
+Service-Component:\
+OSGI-INF/geoJsonHttpHandler.xml,\
diff --git a/org.argeo.app.geo/src/org/argeo/app/geo/CqlUtils.java b/org.argeo.app.geo/src/org/argeo/app/geo/CqlUtils.java
new file mode 100644 (file)
index 0000000..8d6c138
--- /dev/null
@@ -0,0 +1,60 @@
+package org.argeo.app.geo;
+
+import org.argeo.api.acr.NamespaceUtils;
+import org.argeo.api.acr.search.AndFilter;
+import org.argeo.api.acr.search.BasicSearch;
+import org.argeo.api.acr.search.ContentFilter;
+import org.geotools.filter.text.cql2.CQL;
+import org.geotools.filter.text.cql2.CQLException;
+import org.opengis.filter.And;
+import org.opengis.filter.Filter;
+import org.opengis.filter.PropertyIsEqualTo;
+import org.opengis.filter.expression.Literal;
+import org.opengis.filter.expression.PropertyName;
+
+public class CqlUtils {
+
+       public final static String CQL_FILTER = "cql_filter";
+
+       public static void filter(BasicSearch search, String cql) {
+               try {
+                       filter(search, CQL.toFilter(cql));
+               } catch (CQLException e) {
+                       throw new IllegalArgumentException("Cannot parse CQL: " + cql, e);
+               }
+       }
+
+       public static void filter(BasicSearch search, Filter filter) {
+               search.where((where) -> {
+                       if (filter instanceof And and) {
+                               processAnd(where, and);
+                       } else if (filter instanceof PropertyIsEqualTo propertyIsEqualTo) {
+                               processIsEqualTo(where, propertyIsEqualTo);
+                       } else {
+                               throw new IllegalArgumentException("Unsupported filter " + filter.getClass());
+                       }
+               });
+       }
+
+       private static void processAnd(AndFilter contentFilter, And filter) {
+               for (Filter child : filter.getChildren()) {
+                       if (child instanceof PropertyIsEqualTo propertyIsEqualTo) {
+                               processIsEqualTo(contentFilter, propertyIsEqualTo);
+                       }
+               }
+
+       }
+
+       private static void processIsEqualTo(ContentFilter<?> contentFilter, PropertyIsEqualTo propertyIsEqualTo) {
+               // TODO properly deal with types etc.
+               // see GeoTools org.geotools.filter.text.commons.ExpressionToText
+               PropertyName propertyName = (PropertyName) propertyIsEqualTo.getExpression1();
+               Literal value = (Literal) propertyIsEqualTo.getExpression2();
+               // String escaped = literal.toString().replaceAll("'", "''");
+               contentFilter.eq(NamespaceUtils.parsePrefixedName(propertyName.toString()), value.toString());
+       }
+
+       /** singleton */
+       private CqlUtils() {
+       }
+}
diff --git a/org.argeo.app.geo/src/org/argeo/app/internal/geo/http/GeoJsonHttpHandler.java b/org.argeo.app.geo/src/org/argeo/app/internal/geo/http/GeoJsonHttpHandler.java
new file mode 100644 (file)
index 0000000..8840a4e
--- /dev/null
@@ -0,0 +1,118 @@
+package org.argeo.app.internal.geo.http;
+
+import static org.argeo.app.geo.CqlUtils.CQL_FILTER;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+
+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.spi.ProvidedRepository;
+import org.argeo.app.api.EntityType;
+import org.argeo.app.api.WGS84PosName;
+import org.argeo.app.geo.CqlUtils;
+import org.argeo.cms.http.HttpHeader;
+import org.argeo.cms.http.server.HttpServerUtils;
+import org.geotools.data.DataUtilities;
+import org.geotools.data.geojson.GeoJSONWriter;
+import org.geotools.feature.SchemaException;
+import org.geotools.feature.simple.SimpleFeatureBuilder;
+import org.geotools.filter.text.cql2.CQL;
+import org.geotools.geometry.jts.JTSFactoryFinder;
+import org.locationtech.jts.geom.Coordinate;
+import org.locationtech.jts.geom.GeometryFactory;
+import org.locationtech.jts.geom.Point;
+import org.opengis.feature.simple.SimpleFeature;
+import org.opengis.feature.simple.SimpleFeatureType;
+import org.opengis.filter.Filter;
+
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+
+public class GeoJsonHttpHandler implements HttpHandler {
+       private ProvidedRepository contentRepository;
+
+       @Override
+       public void handle(HttpExchange exchange) throws IOException {
+               String path = HttpServerUtils.subPath(exchange);
+               ContentSession session = HttpServerUtils.getContentSession(contentRepository, exchange);
+               // Content content = session.get(path);
+
+               exchange.getResponseHeaders().set(HttpHeader.CONTENT_TYPE.getHeaderName(), "application/json; charset=utf-8");
+
+               Map<String, List<String>> parameters = HttpServerUtils.parseParameters(exchange);
+               String cql = parameters.containsKey(CQL_FILTER) ? parameters.get(CQL_FILTER).get(0) : null;
+
+               if (cql != null) {
+                       Stream<Content> res = session.search((search) -> {
+                               CqlUtils.filter(search.from(path), cql);
+                               search.getWhere().isContentClass(EntityType.local);
+                       });
+
+                       GeoJSONWriter geoJSONWriter = new GeoJSONWriter(exchange.getResponseBody());
+                       geoJSONWriter.setPrettyPrinting(true);
+
+//                     Writer writer = new OutputStreamWriter(exchange.getResponseBody());
+//                     writer.write("""
+//                                     {
+//                                       "type": "FeatureCollection",
+//                                       "features": [
+//                                     """);
+
+                       SimpleFeatureType TYPE;
+                       try {
+                               TYPE = DataUtilities.createType("Content", "the_geom:Point:srid=4326,path:String,name:String");
+                       } catch (SchemaException e) {
+                               throw new RuntimeException(e);
+                       }
+                       SimpleFeatureBuilder featureBuilder = new SimpleFeatureBuilder(TYPE);
+                       GeometryFactory geometryFactory = JTSFactoryFinder.getGeometryFactory();
+
+                       res.forEach((c) -> {
+                               if (!c.hasContentClass(EntityType.geopoint))
+                                       return;
+
+                               double latitude = c.get(WGS84PosName.lat, Double.class).get();
+                               double longitude = c.get(WGS84PosName.lng, Double.class).get();
+
+                               Coordinate coordinate = new Coordinate(longitude, latitude);
+                               Point point = geometryFactory.createPoint(coordinate);
+
+                               featureBuilder.add(point);
+                               String pth = c.getPath();
+                               featureBuilder.add(pth);
+                               featureBuilder.add(NamespaceUtils.toPrefixedName(c.getName()));
+
+                               String uuid = c.attr(LdapAttr.entryUUID);
+
+                               SimpleFeature feature = featureBuilder.buildFeature(uuid);
+//                             String json = GeoJSONWriter.toGeoJSON(feature);
+                               try {
+                                       geoJSONWriter.write(feature);
+                               } catch (IOException e) {
+                                       throw new UncheckedIOException(e);
+                               }
+                       });
+//                     writer.write("""
+//                                       ]
+//                                     }
+//                                     """);
+                       geoJSONWriter.close();
+               }
+
+       }
+
+       public void setContentRepository(ProvidedRepository contentRepository) {
+               this.contentRepository = contentRepository;
+       }
+
+       public static void main(String[] args) throws Exception {
+               Filter filter = CQL.toFilter("entity:type='apafField' AND jcr:isCheckedOut=false");
+               System.out.println(CQL.toCQL(filter));
+       }
+}
diff --git a/org.argeo.app.geo/src/org/argeo/app/internal/geo/http/GmlHttpHandler.java b/org.argeo.app.geo/src/org/argeo/app/internal/geo/http/GmlHttpHandler.java
new file mode 100644 (file)
index 0000000..7213cd2
--- /dev/null
@@ -0,0 +1,31 @@
+package org.argeo.app.internal.geo.http;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+
+import org.argeo.api.acr.Content;
+import org.argeo.api.acr.ContentSession;
+import org.argeo.api.acr.spi.ProvidedRepository;
+import org.argeo.app.geo.CqlUtils;
+import org.argeo.cms.http.server.HttpServerUtils;
+
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+
+public class GmlHttpHandler implements HttpHandler {
+       private ProvidedRepository contentRepository;
+
+       @Override
+       public void handle(HttpExchange exchange) throws IOException {
+               String path = HttpServerUtils.subPath(exchange);
+               ContentSession session = HttpServerUtils.getContentSession(contentRepository, exchange);
+//             Content content = session.get(path);
+       }
+
+       public void setContentRepository(ProvidedRepository contentRepository) {
+               this.contentRepository = contentRepository;
+       }
+
+}