From f7d957d6a6be08e7890bfcd6fb208b98196082c2 Mon Sep 17 00:00:00 2001 From: jampukka Date: Tue, 28 Oct 2025 16:20:54 +0200 Subject: [PATCH 1/3] Add Optional FeatureType#getSrid(int srid) --- .../fi/nls/hakunapi/config/test/ConfigTest.java | 3 ++- .../java/fi/nls/hakunapi/core/FeatureType.java | 3 +++ .../fi/nls/hakunapi/core/SimpleFeatureType.java | 16 +++++++++++++++- .../hakunapi/core/config/HakunaConfigParser.java | 4 +++- .../source/gpkg/GpkgSimpleSourceTest.java | 5 +++-- .../fi/nls/hakunapi/simple/sdo/SdoQueryMain.java | 3 ++- .../TestTracingFeatureServiceTelemetry.java | 7 +++++++ .../TestLoggingFeatureServiceTelemetry.java | 7 +++++++ .../webapp/jakarta/HakunaContextListener.java | 2 +- .../webapp/javax/HakunaContextListener.java | 2 +- 10 files changed, 44 insertions(+), 8 deletions(-) diff --git a/src/hakunapi-config-test/src/main/java/fi/nls/hakunapi/config/test/ConfigTest.java b/src/hakunapi-config-test/src/main/java/fi/nls/hakunapi/config/test/ConfigTest.java index 0e055347..7f259c0d 100644 --- a/src/hakunapi-config-test/src/main/java/fi/nls/hakunapi/config/test/ConfigTest.java +++ b/src/hakunapi-config-test/src/main/java/fi/nls/hakunapi/config/test/ConfigTest.java @@ -7,6 +7,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -111,7 +112,7 @@ public static void main(String[] args) { if (verbose) { System.out.println("Handling collection " + id); } - parser.readCollection(path, sourcesByType, id); + parser.readCollection(path, sourcesByType, id, Collections.emptyList()); } catch (Exception e) { System.err.println("ERROR: " + e.getMessage()); } diff --git a/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/FeatureType.java b/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/FeatureType.java index 174e7043..478bb696 100644 --- a/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/FeatureType.java +++ b/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/FeatureType.java @@ -4,6 +4,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Optional; import fi.nls.hakunapi.core.filter.Filter; import fi.nls.hakunapi.core.param.GetFeatureParam; @@ -57,4 +58,6 @@ public default CacheSettings getCacheSettings() { return null; } + public Optional getSrid(int srid); + } diff --git a/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/SimpleFeatureType.java b/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/SimpleFeatureType.java index 3b9f3880..e8f672ae 100644 --- a/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/SimpleFeatureType.java +++ b/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/SimpleFeatureType.java @@ -4,9 +4,9 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Optional; import fi.nls.hakunapi.core.filter.Filter; -import fi.nls.hakunapi.core.geom.HakunaGeometryDimension; import fi.nls.hakunapi.core.param.GetFeatureParam; import fi.nls.hakunapi.core.projection.ProjectionTransformerFactory; import fi.nls.hakunapi.core.property.HakunaProperty; @@ -32,6 +32,7 @@ public abstract class SimpleFeatureType implements FeatureType { private List staticFilters; private ProjectionTransformerFactory transformerFactory; private Map metadata; + private List knownSrids; public abstract FeatureProducer getFeatureProducer(); @@ -201,5 +202,18 @@ public void setMetadata(Map metadata) { public Map getMetadata() { return metadata; } + + public Optional getSrid(int srid) { + if (knownSrids == null) { + return Optional.empty(); + } + return knownSrids.stream() + .filter(x -> x.getSrid() == srid) + .findAny(); + } + + public void setKnownSrids(List knownSrids) { + this.knownSrids = knownSrids; + } } diff --git a/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/config/HakunaConfigParser.java b/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/config/HakunaConfigParser.java index 6e103655..1d8e6558 100644 --- a/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/config/HakunaConfigParser.java +++ b/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/config/HakunaConfigParser.java @@ -34,6 +34,7 @@ import fi.nls.hakunapi.core.PaginationStrategyCursor; import fi.nls.hakunapi.core.PaginationStrategyHybrid; import fi.nls.hakunapi.core.PaginationStrategyOffset; +import fi.nls.hakunapi.core.SRIDCode; import fi.nls.hakunapi.core.SimpleFeatureType; import fi.nls.hakunapi.core.SimpleSource; import fi.nls.hakunapi.core.filter.Filter; @@ -276,7 +277,7 @@ public String[] readCollectionIds() { return collectionsIds; } - public FeatureType readCollection(Path path, Map sourcesByType, String collectionId) throws Exception { + public FeatureType readCollection(Path path, Map sourcesByType, String collectionId, List knownSrids) throws Exception { LOG.info("collection "+collectionId); // Current prefix for properties String p = "collections." + collectionId + "."; @@ -304,6 +305,7 @@ public FeatureType readCollection(Path path, Map sourcesBy ft.setMetadata(parseMetadata(p, path)); ft.setProjectionTransformerFactory(getProjection(p)); ft.setCacheSettings(parseCacheConfig(collectionId)); + ft.setKnownSrids(knownSrids); if (ft.getPaginationStrategy() == null) { ft.setPaginationStrategy(getPaginationStrategy(p, ft)); diff --git a/src/hakunapi-source-gpkg/src/test/java/fi/nls/hakunapi/source/gpkg/GpkgSimpleSourceTest.java b/src/hakunapi-source-gpkg/src/test/java/fi/nls/hakunapi/source/gpkg/GpkgSimpleSourceTest.java index 2d0ecd9b..577b891b 100644 --- a/src/hakunapi-source-gpkg/src/test/java/fi/nls/hakunapi/source/gpkg/GpkgSimpleSourceTest.java +++ b/src/hakunapi-source-gpkg/src/test/java/fi/nls/hakunapi/source/gpkg/GpkgSimpleSourceTest.java @@ -9,6 +9,7 @@ import java.nio.charset.StandardCharsets; import java.time.Instant; import java.time.OffsetDateTime; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Properties; @@ -52,7 +53,7 @@ public static void setup() throws IOException { @Test public void testParsingFeatureType() throws Exception { - FeatureType ft = cfg.readCollection(null, Map.of(source.getType(), source), "sample_feature_table"); + FeatureType ft = cfg.readCollection(null, Map.of(source.getType(), source), "sample_feature_table", Collections.emptyList()); assertEquals("sample_feature_table", ft.getTitle()); assertEquals("", ft.getDescription()); assertEquals(5, ft.getProperties().size()); @@ -70,7 +71,7 @@ public void testParsingFeatureType() throws Exception { @Test public void testReadingFeatures() throws Exception { - FeatureType ft = cfg.readCollection(null, Map.of(source.getType(), source), "sample_feature_table"); + FeatureType ft = cfg.readCollection(null, Map.of(source.getType(), source), "sample_feature_table", Collections.emptyList()); GetFeatureRequest request = new GetFeatureRequest(); request.setSRID(3067); request.setLimit(1); diff --git a/src/hakunapi-source-oracle/src/test/java/fi/nls/hakunapi/simple/sdo/SdoQueryMain.java b/src/hakunapi-source-oracle/src/test/java/fi/nls/hakunapi/simple/sdo/SdoQueryMain.java index 4e836b76..67f799a6 100644 --- a/src/hakunapi-source-oracle/src/test/java/fi/nls/hakunapi/simple/sdo/SdoQueryMain.java +++ b/src/hakunapi-source-oracle/src/test/java/fi/nls/hakunapi/simple/sdo/SdoQueryMain.java @@ -8,6 +8,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -109,7 +110,7 @@ public Map init(String[] args) throws Exception { Map collections = new HashMap<>(); for (String collectionId : parser.readCollectionIds()) { - FeatureType ft = parser.readCollection(configPath, sourcesByType, collectionId); + FeatureType ft = parser.readCollection(configPath, sourcesByType, collectionId, Collections.emptyList()); collections.put(collectionId, ft); } diff --git a/src/hakunapi-telemetry-opentelemetry/src/test/java/fi/nls/hakunapi/telemetry/TestTracingFeatureServiceTelemetry.java b/src/hakunapi-telemetry-opentelemetry/src/test/java/fi/nls/hakunapi/telemetry/TestTracingFeatureServiceTelemetry.java index e40bb36e..9d38a400 100644 --- a/src/hakunapi-telemetry-opentelemetry/src/test/java/fi/nls/hakunapi/telemetry/TestTracingFeatureServiceTelemetry.java +++ b/src/hakunapi-telemetry-opentelemetry/src/test/java/fi/nls/hakunapi/telemetry/TestTracingFeatureServiceTelemetry.java @@ -6,6 +6,7 @@ import java.time.Instant; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Properties; import org.junit.Test; @@ -15,6 +16,7 @@ import fi.nls.hakunapi.core.FeatureType; import fi.nls.hakunapi.core.PaginationStrategy; import fi.nls.hakunapi.core.PaginationStrategyOffset; +import fi.nls.hakunapi.core.SRIDCode; import fi.nls.hakunapi.core.config.HakunaConfigParser; import fi.nls.hakunapi.core.filter.Filter; import fi.nls.hakunapi.core.param.GetFeatureParam; @@ -171,5 +173,10 @@ public FeatureProducer getFeatureProducer() { return null; } + @Override + public Optional getSrid(int srid) { + return Optional.empty(); + } + }; } diff --git a/src/hakunapi-telemetry/src/test/java/fi/nls/hakunapi/telemetry/TestLoggingFeatureServiceTelemetry.java b/src/hakunapi-telemetry/src/test/java/fi/nls/hakunapi/telemetry/TestLoggingFeatureServiceTelemetry.java index 76c8d2fb..f894f5b1 100644 --- a/src/hakunapi-telemetry/src/test/java/fi/nls/hakunapi/telemetry/TestLoggingFeatureServiceTelemetry.java +++ b/src/hakunapi-telemetry/src/test/java/fi/nls/hakunapi/telemetry/TestLoggingFeatureServiceTelemetry.java @@ -6,6 +6,7 @@ import java.time.Instant; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Properties; import org.junit.Test; @@ -15,6 +16,7 @@ import fi.nls.hakunapi.core.FeatureType; import fi.nls.hakunapi.core.PaginationStrategy; import fi.nls.hakunapi.core.PaginationStrategyOffset; +import fi.nls.hakunapi.core.SRIDCode; import fi.nls.hakunapi.core.config.HakunaConfigParser; import fi.nls.hakunapi.core.filter.Filter; import fi.nls.hakunapi.core.param.GetFeatureParam; @@ -170,5 +172,10 @@ public FeatureProducer getFeatureProducer() { return null; } + @Override + public Optional getSrid(int srid) { + return Optional.empty(); + } + }; } diff --git a/webapp-jakarta/hakunapi-simple-webapp-jakarta/src/main/java/fi/nls/hakunapi/simple/webapp/jakarta/HakunaContextListener.java b/webapp-jakarta/hakunapi-simple-webapp-jakarta/src/main/java/fi/nls/hakunapi/simple/webapp/jakarta/HakunaContextListener.java index 7bbee7d3..ea0de65e 100644 --- a/webapp-jakarta/hakunapi-simple-webapp-jakarta/src/main/java/fi/nls/hakunapi/simple/webapp/jakarta/HakunaContextListener.java +++ b/webapp-jakarta/hakunapi-simple-webapp-jakarta/src/main/java/fi/nls/hakunapi/simple/webapp/jakarta/HakunaContextListener.java @@ -101,7 +101,7 @@ public void contextInitialized(ServletContextEvent sce) { Map collections = new LinkedHashMap<>(); for (String collectionId : parser.readCollectionIds()) { - FeatureType ft = parser.readCollection(configPath, sourcesByType, collectionId); + FeatureType ft = parser.readCollection(configPath, sourcesByType, collectionId, knownSrids); collections.put(collectionId, ft); } diff --git a/webapp-javax/hakunapi-simple-webapp-javax/src/main/java/fi/nls/hakunapi/simple/webapp/javax/HakunaContextListener.java b/webapp-javax/hakunapi-simple-webapp-javax/src/main/java/fi/nls/hakunapi/simple/webapp/javax/HakunaContextListener.java index da284a74..297ec923 100644 --- a/webapp-javax/hakunapi-simple-webapp-javax/src/main/java/fi/nls/hakunapi/simple/webapp/javax/HakunaContextListener.java +++ b/webapp-javax/hakunapi-simple-webapp-javax/src/main/java/fi/nls/hakunapi/simple/webapp/javax/HakunaContextListener.java @@ -101,7 +101,7 @@ public void contextInitialized(ServletContextEvent sce) { Map collections = new LinkedHashMap<>(); for (String collectionId : parser.readCollectionIds()) { - FeatureType ft = parser.readCollection(configPath, sourcesByType, collectionId); + FeatureType ft = parser.readCollection(configPath, sourcesByType, collectionId, knownSrids); collections.put(collectionId, ft); } From 8e9748b65487db78e5fa568f90ce5dd34eefd716 Mon Sep 17 00:00:00 2001 From: jampukka Date: Tue, 28 Oct 2025 22:52:42 +0200 Subject: [PATCH 2/3] Add context object to ExpressionVisitor.visit --- .../cql2/function/geometry/Buffer.java | 16 ++- .../cql2/function/geometry/ST_Buffer.java | 11 +-- .../TestGeometryFunctionsFactory.java | 20 +--- .../hakunapi/cql2/function/CQL2Functions.java | 6 +- .../nls/hakunapi/cql2/function/Function.java | 27 +++-- .../nls/hakunapi/cql2/model/Expression.java | 4 +- .../cql2/model/ExpressionToHakunaFilter.java | 99 +++++++++++-------- .../cql2/model/ExpressionVisitor.java | 66 ++++++------- .../hakunapi/cql2/model/FilterContext.java | 26 +++++ .../fi/nls/hakunapi/cql2/text/CQL2Text.java | 4 +- .../cql2/function/FunctionTableTest.java | 23 ++--- .../text/ExpressionToHakunaFilterTest.java | 78 +++++++++++++++ .../cql2/text/ExpressionToString.java | 54 +++++----- .../cql2/text/TextParserVisitorTest.java | 62 ++++++------ 14 files changed, 306 insertions(+), 190 deletions(-) create mode 100644 src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/model/FilterContext.java create mode 100644 src/hakunapi-cql2/src/test/java/fi/nls/hakunapi/cql2/text/ExpressionToHakunaFilterTest.java diff --git a/src/hakunapi-cql2-functions/src/main/java/fi/nls/hakunapi/cql2/function/geometry/Buffer.java b/src/hakunapi-cql2-functions/src/main/java/fi/nls/hakunapi/cql2/function/geometry/Buffer.java index 346f7108..4a6a5cf0 100644 --- a/src/hakunapi-cql2-functions/src/main/java/fi/nls/hakunapi/cql2/function/geometry/Buffer.java +++ b/src/hakunapi-cql2-functions/src/main/java/fi/nls/hakunapi/cql2/function/geometry/Buffer.java @@ -1,11 +1,13 @@ package fi.nls.hakunapi.cql2.function.geometry; +import java.util.List; + import org.locationtech.jts.geom.Geometry; import fi.nls.hakunapi.core.schemas.FunctionArgumentInfo.FunctionArgumentType; import fi.nls.hakunapi.core.schemas.FunctionReturnsInfo.FunctionReturnsType; import fi.nls.hakunapi.cql2.function.Function; -import fi.nls.hakunapi.cql2.model.function.FunctionCall; +import fi.nls.hakunapi.cql2.model.FilterContext; public class Buffer extends Function { @@ -18,9 +20,15 @@ public Buffer() { } @Override - public Object visit(FunctionCall functionCall) { - Geometry geom = toGeometry(functionCall, "geom"); - double radius_of_buffer = toNumber(functionCall, "radius_of_buffer").doubleValue(); + public Object invoke(List args, Object context) { + Geometry geom = getGeometryArg(args, "geom"); + double radius_of_buffer = getNumberArg(args, "radius_of_buffer").doubleValue(); + if (context != null && context instanceof FilterContext) { + FilterContext fContext = (FilterContext) context; + if (fContext.filterSrid.isDegrees()) { + // TODO: Handle differently? + } + } return geom.buffer(radius_of_buffer); } diff --git a/src/hakunapi-cql2-functions/src/main/java/fi/nls/hakunapi/cql2/function/geometry/ST_Buffer.java b/src/hakunapi-cql2-functions/src/main/java/fi/nls/hakunapi/cql2/function/geometry/ST_Buffer.java index 0c7bd3d5..6958c863 100644 --- a/src/hakunapi-cql2-functions/src/main/java/fi/nls/hakunapi/cql2/function/geometry/ST_Buffer.java +++ b/src/hakunapi-cql2-functions/src/main/java/fi/nls/hakunapi/cql2/function/geometry/ST_Buffer.java @@ -1,5 +1,6 @@ package fi.nls.hakunapi.cql2.function.geometry; +import java.util.List; import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -10,7 +11,6 @@ import fi.nls.hakunapi.core.schemas.FunctionArgumentInfo.FunctionArgumentType; import fi.nls.hakunapi.core.schemas.FunctionReturnsInfo.FunctionReturnsType; import fi.nls.hakunapi.cql2.function.Function; -import fi.nls.hakunapi.cql2.model.function.FunctionCall; public class ST_Buffer extends Function { @@ -31,11 +31,10 @@ public ST_Buffer() { } @Override - public Object visit(FunctionCall functionCall) { - - Geometry geom = toGeometry(functionCall, "geom"); - double radius_of_buffer = toNumber(functionCall, "radius_of_buffer").doubleValue(); - String buffer_style_parameters = toString(functionCall, "buffer_style_parameters"); + public Object invoke(List args, Object context) { + Geometry geom = getGeometryArg(args, "geom"); + double radius_of_buffer = getNumberArg(args, "radius_of_buffer").doubleValue(); + String buffer_style_parameters = getStringArg(args, "buffer_style_parameters"); String[] parts = buffer_style_parameters.split(" "); final Map kv = Stream.of(parts).filter(v -> !v.isEmpty()).map(elem -> elem.split("=")) .filter(v -> v.length != 0).collect(Collectors.toMap(e -> e[0], e -> e[1])); diff --git a/src/hakunapi-cql2-functions/src/test/java/fi/nls/hakunapi/cql2/function/geometry/TestGeometryFunctionsFactory.java b/src/hakunapi-cql2-functions/src/test/java/fi/nls/hakunapi/cql2/function/geometry/TestGeometryFunctionsFactory.java index 7109404f..5ca2aab1 100644 --- a/src/hakunapi-cql2-functions/src/test/java/fi/nls/hakunapi/cql2/function/geometry/TestGeometryFunctionsFactory.java +++ b/src/hakunapi-cql2-functions/src/test/java/fi/nls/hakunapi/cql2/function/geometry/TestGeometryFunctionsFactory.java @@ -4,22 +4,16 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; -import java.util.Arrays; import java.util.List; import org.junit.Test; import org.locationtech.jts.geom.Coordinate; -import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.geom.LineString; import org.locationtech.jts.geom.Polygon; import fi.nls.hakunapi.cql2.function.Function; import fi.nls.hakunapi.cql2.function.FunctionTable; -import fi.nls.hakunapi.cql2.model.function.FunctionCall; -import fi.nls.hakunapi.cql2.model.literal.NumberLiteral; -import fi.nls.hakunapi.cql2.model.literal.StringLiteral; -import fi.nls.hakunapi.cql2.model.spatial.SpatialLiteral; public class TestGeometryFunctionsFactory { @@ -37,9 +31,7 @@ public void testGeometryFunctionsBuffer() { LineString geomFrom = geomFactory .createLineString(new Coordinate[] { new Coordinate(0, 0), new Coordinate(1, 1) }); - FunctionCall functionCall = new FunctionCall("Buffer", - Arrays.asList(new SpatialLiteral(geomFrom), new NumberLiteral(1))); - Object rv = buffer.visit(functionCall); + Object rv = buffer.invoke(List.of(geomFrom, 1), null); assertNotNull(rv); assertTrue(rv instanceof Polygon); @@ -59,9 +51,7 @@ public void testGeometryFunctionsSTBuffer() { LineString geomFrom = geomFactory .createLineString(new Coordinate[] { new Coordinate(0, 0), new Coordinate(1, 1) }); - FunctionCall functionCall = new FunctionCall("ST_Buffer", - Arrays.asList(new SpatialLiteral(geomFrom), new NumberLiteral(1), new StringLiteral(""))); - Object rv = st_buffer.visit(functionCall); + Object rv = st_buffer.invoke(List.of(geomFrom, 1, ""), null); assertNotNull(rv); assertTrue(rv instanceof Polygon); @@ -81,11 +71,7 @@ public void testGeometryFunctionsSTBufferMiter() { LineString geomFrom = geomFactory .createLineString(new Coordinate[] { new Coordinate(0, 0), new Coordinate(1, 1) }); - FunctionCall functionCall = new FunctionCall("ST_Buffer", - Arrays.asList(new SpatialLiteral(geomFrom), new NumberLiteral(1), - // - new StringLiteral("join=miter"))); - Object rv = st_buffer.visit(functionCall); + Object rv = st_buffer.invoke(List.of(geomFrom, 1, "join=miter"), null); assertNotNull(rv); assertTrue(rv instanceof Polygon); diff --git a/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/function/CQL2Functions.java b/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/function/CQL2Functions.java index b5ef6b11..2d39f044 100644 --- a/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/function/CQL2Functions.java +++ b/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/function/CQL2Functions.java @@ -17,7 +17,11 @@ public class CQL2Functions { Map FUNCTION_TABLES = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); public void register() { - FunctionTableProvider.getFunctionTables().forEach(ft -> { + init(FunctionTableProvider.getFunctionTables()); + } + + public void init(List functionTables) { + functionTables.forEach(ft -> { FUNCTION_TABLES.put(ft.getPackageName(), ft); }); } diff --git a/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/function/Function.java b/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/function/Function.java index d403ffaa..13b5e3ed 100644 --- a/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/function/Function.java +++ b/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/function/Function.java @@ -11,16 +11,12 @@ import fi.nls.hakunapi.core.schemas.FunctionArgumentInfo.FunctionArgumentType; import fi.nls.hakunapi.core.schemas.FunctionReturnsInfo; import fi.nls.hakunapi.core.schemas.FunctionReturnsInfo.FunctionReturnsType; -import fi.nls.hakunapi.cql2.model.function.FunctionCall; -import fi.nls.hakunapi.cql2.model.literal.NumberLiteral; -import fi.nls.hakunapi.cql2.model.literal.StringLiteral; -import fi.nls.hakunapi.cql2.model.spatial.SpatialLiteral; public class Function { @FunctionalInterface public interface FunctionImplementation { - Object visit(Function func, FunctionCall call); + Object invoke(Function func, List args, Object context); } protected String name; @@ -74,7 +70,7 @@ public FunctionReturnsInfo getReturns() { return returns; } - public Object visit(FunctionCall functionCall) { + public Object invoke(List args, Object context) { throw new RuntimeException("Missing function implementation"); } @@ -97,22 +93,25 @@ public static Function of(String name, String description, String metadataUrl) { public static Function of(String name, final FunctionImplementation impl) { return new Function(name, null, null) { - public Object visit(FunctionCall call) { - return impl.visit(this, call); + public Object invoke(List args, Object context) { + return impl.invoke(this, args, context); } }; } - protected Geometry toGeometry(FunctionCall call, String name) { - return ((SpatialLiteral) call.getArgs().get(argumentsPosName.get(name))).getGeometry(); + protected Geometry getGeometryArg(List args, String name) { + int i = argumentsPosName.get(name); + return (Geometry) args.get(i); } - protected Number toNumber(FunctionCall call, String name) { - return ((NumberLiteral) call.getArgs().get(argumentsPosName.get(name))).getValue(); + protected Number getNumberArg(List args, String name) { + int i = argumentsPosName.get(name); + return (Number) args.get(i); } - protected String toString(FunctionCall call, String name) { - return ((StringLiteral) call.getArgs().get(argumentsPosName.get(name))).getValue(); + protected String getStringArg(List args, String name) { + int i = argumentsPosName.get(name); + return args.get(i).toString(); } } diff --git a/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/model/Expression.java b/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/model/Expression.java index 843f8781..90c44013 100644 --- a/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/model/Expression.java +++ b/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/model/Expression.java @@ -2,8 +2,8 @@ public interface Expression { - default public Object accept(ExpressionVisitor visitor) { - return visitor.visit(this); + default public Object accept(ExpressionVisitor visitor, Object context) { + return visitor.visit(this, context); } default public boolean isCasei() { diff --git a/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/model/ExpressionToHakunaFilter.java b/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/model/ExpressionToHakunaFilter.java index 9891cb47..98cb8da0 100644 --- a/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/model/ExpressionToHakunaFilter.java +++ b/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/model/ExpressionToHakunaFilter.java @@ -1,15 +1,17 @@ package fi.nls.hakunapi.cql2.model; +import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.stream.Collectors; +import java.util.stream.IntStream; import org.locationtech.jts.geom.Geometry; -import fi.nls.hakunapi.core.FeatureType; import fi.nls.hakunapi.core.filter.Filter; import fi.nls.hakunapi.core.property.HakunaProperty; import fi.nls.hakunapi.core.property.simple.HakunaPropertyGeometry; +import fi.nls.hakunapi.core.schemas.FunctionArgumentInfo; +import fi.nls.hakunapi.core.schemas.FunctionArgumentInfo.FunctionArgumentType; import fi.nls.hakunapi.cql2.function.CQL2Functions; import fi.nls.hakunapi.cql2.function.Function; import fi.nls.hakunapi.cql2.model.function.FunctionCall; @@ -27,36 +29,29 @@ public class ExpressionToHakunaFilter implements ExpressionVisitor { - private final Map queryables; - - public ExpressionToHakunaFilter(FeatureType ft) { - this.queryables = ft.getQueryableProperties().stream() - .collect(Collectors.toMap(HakunaProperty::getName, it -> it)); - } - @Override - public Object visit(And and) { + public Object visit(And and, Object context) { return Filter - .and(and.getChildren().stream().map(this::visit).map(Filter.class::cast).collect(Collectors.toList())); + .and(and.getChildren().stream().map(child -> visit(child, context)).map(Filter.class::cast).collect(Collectors.toList())); } @Override - public Object visit(Or or) { + public Object visit(Or or, Object context) { return Filter - .or(or.getChildren().stream().map(this::visit).map(Filter.class::cast).collect(Collectors.toList())); + .or(or.getChildren().stream().map(child -> visit(child, context)).map(Filter.class::cast).collect(Collectors.toList())); } @Override - public Object visit(Not not) { - return ((Filter) visit(not.getExpression())).negate(); + public Object visit(Not not, Object context) { + return ((Filter) visit(not.getExpression(), context)).negate(); } @Override - public Object visit(BinaryComparisonPredicate p) { + public Object visit(BinaryComparisonPredicate p, Object context) { PropertyName propertyName = p.getProp(); Expression e = p.getValue(); - HakunaProperty prop = visit(propertyName); - String value = (String) visit(e); + HakunaProperty prop = visit(propertyName, context); + String value = (String) visit(e, context); boolean caseInsensitive = propertyName.isCasei() || e.isCasei(); switch (p.getOp()) { @@ -78,23 +73,23 @@ public Object visit(BinaryComparisonPredicate p) { } @Override - public Object visit(LikePredicate p) { + public Object visit(LikePredicate p, Object context) { PropertyName propertyName = p.getProperty(); StringLiteral pattern = p.getPattern(); - HakunaProperty prop = visit(propertyName); + HakunaProperty prop = visit(propertyName, context); String value = pattern.getValue(); boolean caseInsensitive = propertyName.isCasei() || pattern.isCasei(); return Filter.like(prop, value, '%', '_', '\\', caseInsensitive); } @Override - public Object visit(IsNullPredicate p) { - return Filter.isNull(visit(p.getProperty())); + public Object visit(IsNullPredicate p, Object context) { + return Filter.isNull(visit(p.getProperty(), context)); } @Override - public HakunaProperty visit(PropertyName p) { - HakunaProperty prop = queryables.get(p.getValue()); + public HakunaProperty visit(PropertyName p, Object context) { + HakunaProperty prop = ((FilterContext) context).queryables.get(p.getValue()); if (prop == null) { throw new IllegalArgumentException("Non queryable property " + p.getValue()); } @@ -102,40 +97,40 @@ public HakunaProperty visit(PropertyName p) { } @Override - public Object visit(BooleanLiteral p) { + public Object visit(BooleanLiteral p, Object context) { return p.getValue() ? "true" : "false"; } @Override - public Object visit(NumberLiteral p) { + public Object visit(NumberLiteral p, Object context) { return Double.toString(p.getValue()); } @Override - public Object visit(StringLiteral p) { + public Object visit(StringLiteral p, Object context) { return p.getValue(); } @Override - public Object visit(DateLiteral p) { + public Object visit(DateLiteral p, Object context) { return p.getDate().toString(); } @Override - public Object visit(TimestampLiteral p) { + public Object visit(TimestampLiteral p, Object context) { return p.getTimestamp().toString(); } @Override - public Object visit(SpatialPredicate p) { - HakunaProperty property = visit(p.getProp()); + public Object visit(SpatialPredicate p, Object context) { + HakunaProperty property = visit(p.getProp(), context); if (!(property instanceof HakunaPropertyGeometry)) { throw new IllegalArgumentException( "Unexpected property type, " + p.getProp().getValue() + " is not a geometry property"); } HakunaPropertyGeometry prop = (HakunaPropertyGeometry) property; - Object geometry = visit(p.getValue()); + Object geometry = visit(p.getValue(), context); if (!(geometry instanceof Geometry)) { throw new IllegalArgumentException("Expected geometry"); } @@ -164,23 +159,49 @@ public Object visit(SpatialPredicate p) { } @Override - public Object visit(SpatialLiteral p) { + public Object visit(SpatialLiteral p, Object context) { return p.getGeometry(); } @Override - public Object visit(FunctionCall fn) { - Optional function = CQL2Functions.INSTANCE.getAnyFunction(fn.getName()); + public Object visit(FunctionCall functionCall, Object context) { + List args = functionCall.getArgs().stream() + .map(arg -> visit(arg, context)) + .collect(Collectors.toList()); + return CQL2Functions.INSTANCE.getAnyFunction(functionCall.getName()) + .map(f -> invokeFunction(f, args, context)) + .orElseThrow(() -> new RuntimeException("Unmapped Function")); + } + + private static final Map> BACK_FROM_STRING = Map.of( + FunctionArgumentType.number.name(), ExpressionToHakunaFilter::parseNumber + ); - if (function.isPresent()) { - return function.get().visit(fn); + private static final Number parseNumber(Object o) { + try { + return Double.parseDouble(o.toString()); + } catch (Exception e) { + throw new RuntimeException(e); } - throw new RuntimeException("Unmapped Function"); + } + private Object invokeFunction(Function f, List visitedArgs, Object context) { + // visit() transformed all literal values (except Geometries) to strings for Filter + // => Convert them back as the Functions expect objects that match the arg type + List fArgs = f.getArguments(); + int n = Math.min(fArgs.size(), visitedArgs.size()); + List args = IntStream.range(0, n) + .mapToObj(i -> { + FunctionArgumentInfo fArg = fArgs.get(i); + Object arg = visitedArgs.get(i); + return BACK_FROM_STRING.getOrDefault(fArg.getType().get(0), o -> o).apply(arg); + }) + .collect(Collectors.toList()); + return f.invoke(args, context); } @Override - public Object visit(EmptyExpression ee) { + public Object visit(EmptyExpression ee, Object context) { return Filter.PASS; } diff --git a/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/model/ExpressionVisitor.java b/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/model/ExpressionVisitor.java index 507405e5..797dcc0c 100644 --- a/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/model/ExpressionVisitor.java +++ b/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/model/ExpressionVisitor.java @@ -15,64 +15,64 @@ public interface ExpressionVisitor { - default public Object visit(Expression e) { + default public Object visit(Expression e, Object context) { if (e instanceof BinaryComparisonPredicate) { - return visit((BinaryComparisonPredicate) e); + return visit((BinaryComparisonPredicate) e, context); } else if (e instanceof LikePredicate) { - return visit((LikePredicate) e); + return visit((LikePredicate) e, context); } else if (e instanceof IsNullPredicate) { - return visit((IsNullPredicate) e); + return visit((IsNullPredicate) e, context); } else if (e instanceof BooleanLiteral) { - return visit((BooleanLiteral) e); + return visit((BooleanLiteral) e, context); } else if (e instanceof NumberLiteral) { - return visit((NumberLiteral) e); + return visit((NumberLiteral) e, context); } else if (e instanceof StringLiteral) { - return visit((StringLiteral) e); + return visit((StringLiteral) e, context); } else if (e instanceof DateLiteral) { - return visit((DateLiteral) e); + return visit((DateLiteral) e, context); } else if (e instanceof TimestampLiteral) { - return visit((TimestampLiteral) e); + return visit((TimestampLiteral) e, context); } else if (e instanceof PropertyName) { - return visit((PropertyName) e); + return visit((PropertyName) e, context); } else if (e instanceof And) { - return visit((And) e); + return visit((And) e, context); } else if (e instanceof Or) { - return visit((Or) e); + return visit((Or) e, context); } else if (e instanceof Not) { - return visit((Not) e); + return visit((Not) e, context); } else if (e instanceof SpatialPredicate) { - return visit((SpatialPredicate) e); + return visit((SpatialPredicate) e, context); } else if (e instanceof SpatialLiteral) { - return visit((SpatialLiteral) e); + return visit((SpatialLiteral) e, context); } else if (e instanceof FunctionCall) { - return visit((FunctionCall) e); + return visit((FunctionCall) e, context); } else if (e instanceof EmptyExpression) { - return visit((EmptyExpression) e); + return visit((EmptyExpression) e, context); } throw new IllegalStateException("Unknown Expression " + e.getClass()); } - public Object visit(And and); - public Object visit(Or or); - public Object visit(Not not); + public Object visit(And and, Object context); + public Object visit(Or or, Object context); + public Object visit(Not not, Object context); - public Object visit(SpatialPredicate p); - public Object visit(SpatialLiteral p); + public Object visit(SpatialPredicate p, Object context); + public Object visit(SpatialLiteral p, Object context); - public Object visit(BinaryComparisonPredicate p); - public Object visit(LikePredicate p); - public Object visit(IsNullPredicate p); + public Object visit(BinaryComparisonPredicate p, Object context); + public Object visit(LikePredicate p, Object context); + public Object visit(IsNullPredicate p, Object context); - public Object visit(PropertyName p); + public Object visit(PropertyName p, Object context); - public Object visit(BooleanLiteral p); - public Object visit(NumberLiteral p); - public Object visit(StringLiteral p); - public Object visit(DateLiteral p); - public Object visit(TimestampLiteral p); + public Object visit(BooleanLiteral p, Object context); + public Object visit(NumberLiteral p, Object context); + public Object visit(StringLiteral p, Object context); + public Object visit(DateLiteral p, Object context); + public Object visit(TimestampLiteral p, Object context); - public Object visit(FunctionCall fn); - public Object visit(EmptyExpression ee); + public Object visit(FunctionCall fn, Object context); + public Object visit(EmptyExpression ee, Object context); } diff --git a/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/model/FilterContext.java b/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/model/FilterContext.java new file mode 100644 index 00000000..af29ab3a --- /dev/null +++ b/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/model/FilterContext.java @@ -0,0 +1,26 @@ +package fi.nls.hakunapi.cql2.model; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import fi.nls.hakunapi.core.FeatureType; +import fi.nls.hakunapi.core.SRIDCode; +import fi.nls.hakunapi.core.property.HakunaProperty; + +public class FilterContext { + + public final Map queryables; + public final SRIDCode filterSrid; + + public FilterContext(FeatureType ft, SRIDCode filterSrid) { + this(ft.getQueryableProperties(), filterSrid); + } + + public FilterContext(List queryables, SRIDCode filterSrid) { + this.queryables = queryables.stream() + .collect(Collectors.toMap(HakunaProperty::getName, it -> it)); + this.filterSrid = filterSrid; + } + +} diff --git a/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/text/CQL2Text.java b/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/text/CQL2Text.java index ae31a570..76afe598 100644 --- a/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/text/CQL2Text.java +++ b/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/text/CQL2Text.java @@ -15,6 +15,7 @@ import fi.nls.hakunapi.cql2.model.EmptyExpression; import fi.nls.hakunapi.cql2.model.Expression; import fi.nls.hakunapi.cql2.model.ExpressionToHakunaFilter; +import fi.nls.hakunapi.cql2.model.FilterContext; public class CQL2Text implements FilterParser { @@ -31,7 +32,8 @@ public String getCode() { public Filter parse(FeatureType ft, String filter, int filterSrid) throws IllegalArgumentException { try { Expression expression = parse(filter, new GeometryFactory(new PrecisionModel(), filterSrid)); - return (Filter) new ExpressionToHakunaFilter(ft).visit(expression); + FilterContext context = new FilterContext(ft, ft.getSrid(filterSrid).get()); + return (Filter) new ExpressionToHakunaFilter().visit(expression, context); } catch (Exception e) { if (e instanceof IllegalArgumentException) { throw e; diff --git a/src/hakunapi-cql2/src/test/java/fi/nls/hakunapi/cql2/function/FunctionTableTest.java b/src/hakunapi-cql2/src/test/java/fi/nls/hakunapi/cql2/function/FunctionTableTest.java index 44783def..47111613 100644 --- a/src/hakunapi-cql2/src/test/java/fi/nls/hakunapi/cql2/function/FunctionTableTest.java +++ b/src/hakunapi-cql2/src/test/java/fi/nls/hakunapi/cql2/function/FunctionTableTest.java @@ -2,13 +2,11 @@ import static org.junit.Assert.assertTrue; -import java.util.Arrays; +import java.util.List; import org.junit.Test; import fi.nls.hakunapi.core.schemas.FunctionArgumentInfo.FunctionArgumentType; -import fi.nls.hakunapi.cql2.model.function.FunctionCall; -import fi.nls.hakunapi.cql2.model.literal.StringLiteral; public class FunctionTableTest { @@ -17,23 +15,18 @@ public void testFunction() { FunctionTableImpl functions = FunctionTable.of("poc", // - Function.of("A", (func, call) -> { - return func.toString(call, "param"); + Function.of("echo", (func, args, ctx) -> { + return func.getStringArg(args, "param"); }).argument("param", FunctionArgumentType.string), // - Function.of("B", (func, call) -> { - return func.toString(call, "param"); - }).argument("param", FunctionArgumentType.string) + Function.of("numToString", (func, args, ctx) -> { + return func.getNumberArg(args, "arg1").toString(); + }).argument("arg1", FunctionArgumentType.number) ); - FunctionCall callA = new FunctionCall("A", Arrays.asList(new StringLiteral("A"))); - - assertTrue("A".equals(functions.getFunction("A").visit(callA))); - - FunctionCall callB = new FunctionCall("B", Arrays.asList(new StringLiteral("B"))); - - assertTrue("B".equals(functions.getFunction("B").visit(callB))); + assertTrue("A".equals(functions.getFunction("echo").invoke(List.of("A"), null))); + assertTrue("1337.0".equals(functions.getFunction("numToString").invoke(List.of(1337.0), null))); } } diff --git a/src/hakunapi-cql2/src/test/java/fi/nls/hakunapi/cql2/text/ExpressionToHakunaFilterTest.java b/src/hakunapi-cql2/src/test/java/fi/nls/hakunapi/cql2/text/ExpressionToHakunaFilterTest.java new file mode 100644 index 00000000..85a037af --- /dev/null +++ b/src/hakunapi-cql2/src/test/java/fi/nls/hakunapi/cql2/text/ExpressionToHakunaFilterTest.java @@ -0,0 +1,78 @@ +package fi.nls.hakunapi.cql2.text; + +import static org.junit.Assert.assertEquals; + +import org.junit.BeforeClass; +import org.junit.Test; +import org.locationtech.jts.algorithm.Centroid; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Geometry; + +import java.util.List; + +import fi.nls.hakunapi.core.SRIDCode; +import fi.nls.hakunapi.core.filter.Filter; +import fi.nls.hakunapi.core.filter.FilterOp; +import fi.nls.hakunapi.core.geom.HakunaGeometryDimension; +import fi.nls.hakunapi.core.geom.HakunaGeometryType; +import fi.nls.hakunapi.core.property.HakunaProperty; +import fi.nls.hakunapi.core.property.HakunaPropertyWriters; +import fi.nls.hakunapi.core.property.simple.HakunaPropertyGeometry; +import fi.nls.hakunapi.core.schemas.FunctionArgumentInfo.FunctionArgumentType; +import fi.nls.hakunapi.core.schemas.FunctionReturnsInfo.FunctionReturnsType; +import fi.nls.hakunapi.cql2.function.CQL2Functions; +import fi.nls.hakunapi.cql2.function.Function; +import fi.nls.hakunapi.cql2.function.FunctionTable; +import fi.nls.hakunapi.cql2.model.Expression; +import fi.nls.hakunapi.cql2.model.ExpressionToHakunaFilter; +import fi.nls.hakunapi.cql2.model.FilterContext; + +public class ExpressionToHakunaFilterTest { + + @BeforeClass + public static void init() { + Function buffer = Function.of("buffer", (fn, args, ctx) -> { + Geometry g = (Geometry) args.get(0); + double distance = ((Number) args.get(1)).doubleValue(); + return g.buffer(distance); + }) + .argument("geom", FunctionArgumentType.geometry) + .argument("distance", FunctionArgumentType.number) + .returns(FunctionReturnsType.geometry); + + Function centroid = Function.of("centroid", (fn, args, ctx) -> { + Geometry g = (Geometry) args.get(0); + return g.getFactory().createPoint(Centroid.getCentroid(g)); + }) + .argument("geom", FunctionArgumentType.geometry) + .returns(FunctionReturnsType.geometry); + + CQL2Functions.INSTANCE.init(List.of(FunctionTable.of("geometryTests", buffer, centroid))); + } + + @Test + public void testNestedFunctionCall() { + int[] srid = new int[] { 4326 }; + int storageSrid = srid[0]; + HakunaPropertyGeometry footprint = new HakunaPropertyGeometry("footprint", "any", "any", true, + HakunaGeometryType.POLYGON, srid, storageSrid, 2, HakunaPropertyWriters.HIDDEN); + List queryables = List.of(footprint); + + String filter = "S_Intersects(footprint, BUFFER(cenTroid( lineString (0 0, 100 100)), 10))"; + SRIDCode filterSrid = new SRIDCode(4326, true, true, HakunaGeometryDimension.XY); + + FilterContext ctx = new FilterContext(queryables, filterSrid); + + Expression expr = CQL2Text.parse(filter); + Filter f = (Filter) new ExpressionToHakunaFilter().visit(expr, ctx); + + assertEquals("footprint", f.getProp().getName()); + assertEquals(FilterOp.INTERSECTS, f.getOp()); + + Geometry bufferedCentroid = (Geometry) f.getValue(); + Envelope actual = bufferedCentroid.getEnvelopeInternal(); + Envelope expected = new Envelope(40, 60, 40, 60); + assertEquals(expected, actual); + } + +} diff --git a/src/hakunapi-cql2/src/test/java/fi/nls/hakunapi/cql2/text/ExpressionToString.java b/src/hakunapi-cql2/src/test/java/fi/nls/hakunapi/cql2/text/ExpressionToString.java index 66ee451a..b0f0759e 100644 --- a/src/hakunapi-cql2/src/test/java/fi/nls/hakunapi/cql2/text/ExpressionToString.java +++ b/src/hakunapi-cql2/src/test/java/fi/nls/hakunapi/cql2/text/ExpressionToString.java @@ -37,7 +37,7 @@ public String finish() { } @Override - public Object visit(And and) { + public Object visit(And and, Object context) { boolean first = true; for (Expression e : and.getChildren()) { if (!first) { @@ -46,7 +46,7 @@ public Object visit(And and) { if (e instanceof LogicalExpression) { sb.append("("); } - visit(e); + visit(e, context); if (e instanceof LogicalExpression) { sb.append(")"); } @@ -56,7 +56,7 @@ public Object visit(And and) { } @Override - public Object visit(Or or) { + public Object visit(Or or, Object context) { boolean first = true; for (Expression e : or.getChildren()) { if (!first) { @@ -65,7 +65,7 @@ public Object visit(Or or) { if (e instanceof LogicalExpression) { sb.append("("); } - visit(e); + visit(e, context); if (e instanceof LogicalExpression) { sb.append(")"); } @@ -75,103 +75,103 @@ public Object visit(Or or) { } @Override - public Object visit(Not not) { + public Object visit(Not not, Object context) { sb.append("NOT ("); - this.visit(not.getExpression()); + this.visit(not.getExpression(), context); sb.append(")"); return null; } @Override - public Object visit(BinaryComparisonPredicate expression) { - visit(expression.getProp()); + public Object visit(BinaryComparisonPredicate expression, Object context) { + visit(expression.getProp(), context); sb.append(' '); if (expression.getProp().isCasei() || expression.getValue().isCasei()) { sb.append(CASEI_PREFIX); } sb.append(expression.getOp().op); sb.append(' '); - visit(expression.getValue()); + visit(expression.getValue(), context); return null; } @Override - public Object visit(LikePredicate p) { - visit(p.getProperty()); + public Object visit(LikePredicate p, Object context) { + visit(p.getProperty(), context); sb.append(' '); if (p.getProperty().isCasei() || p.getPattern().isCasei()) { sb.append('I'); } sb.append("LIKE"); sb.append(' '); - visit(p.getPattern()); + visit(p.getPattern(), context); return null; } @Override - public Object visit(IsNullPredicate p) { - visit(p.getProperty()); + public Object visit(IsNullPredicate p, Object context) { + visit(p.getProperty(), context); sb.append(" IS NULL"); return null; } @Override - public Object visit(PropertyName expression) { + public Object visit(PropertyName expression, Object context) { sb.append(expression.getValue()); return null; } @Override - public Object visit(NumberLiteral number) { + public Object visit(NumberLiteral number, Object context) { sb.append(number.getValue()); return null; } @Override - public Object visit(BooleanLiteral bool) { + public Object visit(BooleanLiteral bool, Object context) { sb.append(bool.getValue()); return null; } @Override - public Object visit(StringLiteral string) { + public Object visit(StringLiteral string, Object context) { sb.append(string.getValue()); return null; } @Override - public Object visit(DateLiteral p) { + public Object visit(DateLiteral p, Object context) { sb.append(p.getDate().toString()); return null; } @Override - public Object visit(TimestampLiteral p) { + public Object visit(TimestampLiteral p, Object context) { sb.append(p.getTimestamp().toString()); return null; } @Override - public Object visit(SpatialPredicate p) { + public Object visit(SpatialPredicate p, Object context) { sb.append(p.getOp().name()); sb.append('('); - visit(p.getProp()); + visit(p.getProp(), context); sb.append(','); sb.append(' '); - visit(p.getValue()); + visit(p.getValue(), context); sb.append(')'); return null; } @Override - public Object visit(SpatialLiteral p) { + public Object visit(SpatialLiteral p, Object context) { sb.append(new WKTWriter().write(p.getGeometry())); return null; } @Override - public Object visit(FunctionCall fn) { + public Object visit(FunctionCall fn, Object context) { sb.append(fn.getName()); sb.append('('); boolean f = true; @@ -179,7 +179,7 @@ public Object visit(FunctionCall fn) { if (!f) { sb.append(", "); } - visit(arg); + visit(arg, context); f = false; } sb.append(')'); @@ -187,7 +187,7 @@ public Object visit(FunctionCall fn) { } @Override - public Object visit(EmptyExpression ee) { + public Object visit(EmptyExpression ee, Object context) { return null; } diff --git a/src/hakunapi-cql2/src/test/java/fi/nls/hakunapi/cql2/text/TextParserVisitorTest.java b/src/hakunapi-cql2/src/test/java/fi/nls/hakunapi/cql2/text/TextParserVisitorTest.java index c345ec04..e3c7d667 100644 --- a/src/hakunapi-cql2/src/test/java/fi/nls/hakunapi/cql2/text/TextParserVisitorTest.java +++ b/src/hakunapi-cql2/src/test/java/fi/nls/hakunapi/cql2/text/TextParserVisitorTest.java @@ -20,7 +20,7 @@ public void init() { @Test public void testCombiningLogicalExpressions() { String input = "foo < +12. AND myprop IS Null OR dog = 'baf' AND ns1:bar.z_2 < 'dog <> bark' AnD (bAz = true oR qux = false) ANd foo1 <> -.5 AND baz IN ('dog', 'pants') AND foo2 < -2 AND street = 'Tarkk''ampujankatu'"; - parse(input).accept(toString); + parse(input).accept(toString, null); String expected = "(foo < 12.0 AND myprop IS NULL) OR (dog = baf AND ns1:bar.z_2 < dog <> bark AND (bAz = true OR qux = false) AND foo1 <> -0.5 AND (baz = dog OR baz = pants) AND foo2 < -2.0 AND street = Tarkk'ampujankatu)"; String actual = toString.finish(); assertEquals(expected, actual); @@ -29,7 +29,7 @@ public void testCombiningLogicalExpressions() { @Test public void testPrecedence() { String input = "foo < 0 OR (bar > 0 AND (baz > 0 OR qux < 0))"; - parse(input).accept(toString); + parse(input).accept(toString, null); String expected = "foo < 0.0 OR (bar > 0.0 AND (baz > 0.0 OR qux < 0.0))"; String actual = toString.finish(); assertEquals(expected, actual); @@ -38,7 +38,7 @@ public void testPrecedence() { @Test public void testEscapedQuoteWithDoubleSingleQuote() { String input = "name = 'Tarkk''ampujankatu'"; - parse(input).accept(toString); + parse(input).accept(toString, null); String expected = "name = Tarkk'ampujankatu"; String actual = toString.finish(); assertEquals(expected, actual); @@ -47,7 +47,7 @@ public void testEscapedQuoteWithDoubleSingleQuote() { @Test public void testNoSpaceBetweenOperatorAndOperands() { String input = "component_AdminUnitName_4='MyTest'"; - parse(input).accept(toString); + parse(input).accept(toString, null); String expected = "component_AdminUnitName_4 = MyTest"; String actual = toString.finish(); assertEquals(expected, actual); @@ -56,7 +56,7 @@ public void testNoSpaceBetweenOperatorAndOperands() { @Test public void testEscapedQuoteWithBackslash() { String input = "name = 'Tarkk\\'ampujankatu'"; - parse(input).accept(toString); + parse(input).accept(toString, null); String expected = "name = Tarkk'ampujankatu"; String actual = toString.finish(); assertEquals(expected, actual); @@ -65,7 +65,7 @@ public void testEscapedQuoteWithBackslash() { @Test public void testEscapedQuoteWithAdditionalBackslash() { String input = "name = 'Tarkk\\\\'ampujankatu'"; - parse(input).accept(toString); + parse(input).accept(toString, null); String expected = "name = Tarkk\\'ampujankatu"; String actual = toString.finish(); assertEquals(expected, actual); @@ -80,7 +80,7 @@ public void testEscapedQuoteWithTripleSingleQuote() { @Test public void testEscapedQuoteStartsWith() { String input = "greeting = '''ello there mate!'"; - parse(input).accept(toString); + parse(input).accept(toString, null); String expected = "greeting = 'ello there mate!"; String actual = toString.finish(); assertEquals(expected, actual); @@ -89,7 +89,7 @@ public void testEscapedQuoteStartsWith() { @Test public void testEscapedQuoteEndsWith() { String input = "possessive_apostrophe = 'yes, fairies'''"; - parse(input).accept(toString); + parse(input).accept(toString, null); String expected = "possessive_apostrophe = yes, fairies'"; String actual = toString.finish(); assertEquals(expected, actual); @@ -98,7 +98,7 @@ public void testEscapedQuoteEndsWith() { @Test public void testMultipleAnd() { String input = "foo < 0 AND bar > 0 AND baz > 0 AND qux < 0"; - parse(input).accept(toString); + parse(input).accept(toString, null); String expected = "foo < 0.0 AND bar > 0.0 AND baz > 0.0 AND qux < 0.0"; String actual = toString.finish(); assertEquals(expected, actual); @@ -107,7 +107,7 @@ public void testMultipleAnd() { @Test public void testTimestampMicros() { String timestamp = "foo < TIMESTAMP('2011-01-01T05:00:00.123456789Z')"; - parse(timestamp).accept(toString); + parse(timestamp).accept(toString, null); String expected = "foo < 2011-01-01T05:00:00.123456789Z"; String actual = toString.finish(); assertEquals(expected, actual); @@ -116,7 +116,7 @@ public void testTimestampMicros() { @Test public void testTimestamp() { String timestamp = "foo < TIMESTAMP('2011-01-01T05:00:00Z')"; - parse(timestamp).accept(toString); + parse(timestamp).accept(toString, null); String expected = "foo < 2011-01-01T05:00:00Z"; String actual = toString.finish(); assertEquals(expected, actual); @@ -125,7 +125,7 @@ public void testTimestamp() { @Test public void testDate() { String date = "foo < dAtE('2011-01-01')"; - parse(date).accept(toString); + parse(date).accept(toString, null); String expected = "foo < 2011-01-01"; String actual = toString.finish(); assertEquals(expected, actual); @@ -134,7 +134,7 @@ public void testDate() { @Test public void testBetween() { String between = "foo BETWEEN 1 AND 3"; - parse(between).accept(toString); + parse(between).accept(toString, null); String expected = "foo >= 1.0 AND foo <= 3.0"; String actual = toString.finish(); assertEquals(expected, actual); @@ -143,7 +143,7 @@ public void testBetween() { @Test public void testNotBetween() { String notBetween = "foo NOT BETWEEN 1 AND 3"; - parse(notBetween).accept(toString); + parse(notBetween).accept(toString, null); String expected = "foo < 1.0 OR foo > 3.0"; String actual = toString.finish(); assertEquals(expected, actual); @@ -152,7 +152,7 @@ public void testNotBetween() { @Test public void testLike() { String input = "baz LIKE 'my_pattern%'"; - parse(input).accept(toString); + parse(input).accept(toString, null); String expected = "baz LIKE my_pattern%"; String actual = toString.finish(); assertEquals(expected, actual); @@ -161,7 +161,7 @@ public void testLike() { @Test public void testLikeCaseiValue() { String input = "baz LIKE casei('my_pattern%')"; - parse(input).accept(toString); + parse(input).accept(toString, null); String expected = "baz ILIKE my_pattern%"; String actual = toString.finish(); assertEquals(expected, actual); @@ -170,7 +170,7 @@ public void testLikeCaseiValue() { @Test public void testLikeCaseiProperty() { String input = "casei(baz) LIKE casei('my_pattern%')"; - parse(input).accept(toString); + parse(input).accept(toString, null); String expected = "baz ILIKE my_pattern%"; String actual = toString.finish(); assertEquals(expected, actual); @@ -179,7 +179,7 @@ public void testLikeCaseiProperty() { @Test public void testLikeCaseiBoth() { String input = "casei(baz) LIKE casei('my_pattern%')"; - parse(input).accept(toString); + parse(input).accept(toString, null); String expected = "baz ILIKE my_pattern%"; String actual = toString.finish(); assertEquals(expected, actual); @@ -188,7 +188,7 @@ public void testLikeCaseiBoth() { @Test public void testIn() { String in = "baz in ('dog', 'pants')"; - parse(in).accept(toString); + parse(in).accept(toString, null); String expected = "baz = dog OR baz = pants"; String actual = toString.finish(); assertEquals(expected, actual); @@ -197,7 +197,7 @@ public void testIn() { @Test public void testNotIn() { String notIn = "baz NOT in ('dog', 'pants')"; - parse(notIn).accept(toString); + parse(notIn).accept(toString, null); String expected = "baz <> dog AND baz <> pants"; String actual = toString.finish(); assertEquals(expected, actual); @@ -206,7 +206,7 @@ public void testNotIn() { @Test public void testCaseiIn() { String input = "CASEI(road_class) IN (CASEI('Οδος'),CASEI('Straße'))"; - parse(input).accept(toString); + parse(input).accept(toString, null); String expected = "road_class @= Οδος OR road_class @= Straße"; String actual = toString.finish(); assertEquals(expected, actual); @@ -215,7 +215,7 @@ public void testCaseiIn() { @Test public void testSIntersectsPoint() { String intersects_point = "s_intersects(footprint, point (43.5845 -79.5442))"; - parse(intersects_point).accept(toString); + parse(intersects_point).accept(toString, null); String expected = "S_INTERSECTS(footprint, POINT (43.5845 -79.5442))"; String actual = toString.finish(); assertEquals(expected, actual); @@ -224,7 +224,7 @@ public void testSIntersectsPoint() { @Test public void testSIntersectsMultiPoint() { String input = "s_intersects(footprint, MULTIPOINT ((10 40), (40 30), (20 20), (30 10)))"; - parse(input).accept(toString); + parse(input).accept(toString, null); String expected = "S_INTERSECTS(footprint, MULTIPOINT ((10 40), (40 30), (20 20), (30 10)))"; String actual = toString.finish(); assertEquals(expected, actual); @@ -233,7 +233,7 @@ public void testSIntersectsMultiPoint() { @Test public void testSIntersectsLinestring() { String intersects_line = "s_intersects(footprint, lineString (43.5845 -79.5442, 43.6079 -79.4893))"; - parse(intersects_line).accept(toString); + parse(intersects_line).accept(toString, null); String expected = "S_INTERSECTS(footprint, LINESTRING (43.5845 -79.5442, 43.6079 -79.4893))"; String actual = toString.finish(); assertEquals(expected, actual); @@ -242,7 +242,7 @@ public void testSIntersectsLinestring() { @Test public void testSIntersectsPolygon() { String intersects_polygon = "s_INTERSECts(footprint, PolyGon ((43.5845 -79.5442, 43.6079 -79.4893, 43.5677 -79.4632, 43.6129 -79.3925, 43.6223 -79.3238, 43.6576 -79.3163, 43.7945 -79.1178, 43.8144 -79.1542, 43.8555 -79.1714, 43.7509 -79.6390, 43.5845 -79.5442)))"; - parse(intersects_polygon).accept(toString); + parse(intersects_polygon).accept(toString, null); String expected = "S_INTERSECTS(footprint, POLYGON ((43.5845 -79.5442, 43.6079 -79.4893, 43.5677 -79.4632, 43.6129 -79.3925, 43.6223 -79.3238, 43.6576 -79.3163, 43.7945 -79.1178, 43.8144 -79.1542, 43.8555 -79.1714, 43.7509 -79.639, 43.5845 -79.5442)))"; String actual = toString.finish(); assertEquals(expected, actual); @@ -251,7 +251,7 @@ public void testSIntersectsPolygon() { @Test public void testCrossesPolygon() { String crosses_polygon = "S_CROSSES(footprint, PolyGon ((43.5845 -79.5442, 43.6079 -79.4893, 43.5677 -79.4632, 43.6129 -79.3925, 43.6223 -79.3238, 43.6576 -79.3163, 43.7945 -79.1178, 43.8144 -79.1542, 43.8555 -79.1714, 43.7509 -79.6390, 43.5845 -79.5442)))"; - parse(crosses_polygon).accept(toString); + parse(crosses_polygon).accept(toString, null); String expected = "S_CROSSES(footprint, POLYGON ((43.5845 -79.5442, 43.6079 -79.4893, 43.5677 -79.4632, 43.6129 -79.3925, 43.6223 -79.3238, 43.6576 -79.3163, 43.7945 -79.1178, 43.8144 -79.1542, 43.8555 -79.1714, 43.7509 -79.639, 43.5845 -79.5442)))"; String actual = toString.finish(); assertEquals(expected, actual); @@ -260,7 +260,7 @@ public void testCrossesPolygon() { @Test public void testSIntersectsBbox() { String intersects_envelope = "S_Intersects(footprint, bbox (43.5845, -79.5442, 43.6079, -79.7893))"; - parse(intersects_envelope).accept(toString); + parse(intersects_envelope).accept(toString, null); String expected = "S_INTERSECTS(footprint, POLYGON ((43.5845 -79.5442, 43.6079 -79.5442, 43.6079 -79.7893, 43.5845 -79.7893, 43.5845 -79.5442)))"; String actual = toString.finish(); assertEquals(expected, actual); @@ -269,7 +269,7 @@ public void testSIntersectsBbox() { @Test public void testSIntersectsGeometryCollection() { String intersects_line = "s_intersects(footprint, GeometryCollection (lineString (43.5845 -79.5442, 43.6079 -79.4893), point (43.5845 -79.5442)))"; - parse(intersects_line).accept(toString); + parse(intersects_line).accept(toString, null); String expected = "S_INTERSECTS(footprint, GEOMETRYCOLLECTION (LINESTRING (43.5845 -79.5442, 43.6079 -79.4893), POINT (43.5845 -79.5442)))"; String actual = toString.finish(); assertEquals(expected, actual); @@ -278,7 +278,7 @@ public void testSIntersectsGeometryCollection() { @Test public void testIntersectsBufferLine() { String intersects_buffer_line = "S_Intersects(footprint, BUFFER( lineString (43.5845 -79.5442, 43.6079 -79.4893), 10))"; - parse(intersects_buffer_line).accept(toString); + parse(intersects_buffer_line).accept(toString, null); String expected = "S_INTERSECTS(footprint, BUFFER(LINESTRING (43.5845 -79.5442, 43.6079 -79.4893), 10.0))"; String actual = toString.finish(); assertEquals(expected, actual); @@ -287,7 +287,7 @@ public void testIntersectsBufferLine() { @Test public void testRandomFunctionCalls() { String function_call = "foo > foo(bar(1), 2, 3)"; - parse(function_call).accept(toString); + parse(function_call).accept(toString, null); String expected = "foo > foo(bar(1.0), 2.0, 3.0)"; String actual = toString.finish(); assertEquals(expected, actual); @@ -296,7 +296,7 @@ public void testRandomFunctionCalls() { @Test public void testEmptyString() { String function_call = " "; - parse(function_call).accept(toString); + parse(function_call).accept(toString, null); String expected = ""; String actual = toString.finish(); assertEquals(expected, actual); From 49cc8d6a5ae82e97081060710dd7608d69e06799 Mon Sep 17 00:00:00 2001 From: jampukka Date: Wed, 29 Oct 2025 21:54:39 +0200 Subject: [PATCH 3/3] Play with generics --- .../cql2/function/geometry/Buffer.java | 8 ++-- .../cql2/function/geometry/ST_Buffer.java | 5 ++- .../TestGeometryFunctionsFactory.java | 6 +-- .../hakunapi/cql2/function/CQL2Functions.java | 2 +- .../nls/hakunapi/cql2/function/Function.java | 26 +++++------ .../cql2/model/ExpressionToHakunaFilter.java | 43 +++++++++---------- .../cql2/model/ExpressionVisitor.java | 36 ++++++++-------- .../hakunapi/cql2/model/FilterContext.java | 24 +++-------- .../cql2/model/SimpleFilterContext.java | 41 ++++++++++++++++++ .../fi/nls/hakunapi/cql2/text/CQL2Text.java | 3 +- .../text/ExpressionToHakunaFilterTest.java | 28 ++++++++++-- 11 files changed, 137 insertions(+), 85 deletions(-) create mode 100644 src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/model/SimpleFilterContext.java diff --git a/src/hakunapi-cql2-functions/src/main/java/fi/nls/hakunapi/cql2/function/geometry/Buffer.java b/src/hakunapi-cql2-functions/src/main/java/fi/nls/hakunapi/cql2/function/geometry/Buffer.java index 4a6a5cf0..8d046e4f 100644 --- a/src/hakunapi-cql2-functions/src/main/java/fi/nls/hakunapi/cql2/function/geometry/Buffer.java +++ b/src/hakunapi-cql2-functions/src/main/java/fi/nls/hakunapi/cql2/function/geometry/Buffer.java @@ -9,7 +9,7 @@ import fi.nls.hakunapi.cql2.function.Function; import fi.nls.hakunapi.cql2.model.FilterContext; -public class Buffer extends Function { +public class Buffer extends Function { public Buffer() { super("Buffer", null, null); @@ -20,12 +20,12 @@ public Buffer() { } @Override - public Object invoke(List args, Object context) { + public Object invoke(List args, FilterContext context) { Geometry geom = getGeometryArg(args, "geom"); double radius_of_buffer = getNumberArg(args, "radius_of_buffer").doubleValue(); - if (context != null && context instanceof FilterContext) { + if (context != null) { FilterContext fContext = (FilterContext) context; - if (fContext.filterSrid.isDegrees()) { + if (fContext.filterSrid().isDegrees()) { // TODO: Handle differently? } } diff --git a/src/hakunapi-cql2-functions/src/main/java/fi/nls/hakunapi/cql2/function/geometry/ST_Buffer.java b/src/hakunapi-cql2-functions/src/main/java/fi/nls/hakunapi/cql2/function/geometry/ST_Buffer.java index 6958c863..dd3ffcbb 100644 --- a/src/hakunapi-cql2-functions/src/main/java/fi/nls/hakunapi/cql2/function/geometry/ST_Buffer.java +++ b/src/hakunapi-cql2-functions/src/main/java/fi/nls/hakunapi/cql2/function/geometry/ST_Buffer.java @@ -11,8 +11,9 @@ import fi.nls.hakunapi.core.schemas.FunctionArgumentInfo.FunctionArgumentType; import fi.nls.hakunapi.core.schemas.FunctionReturnsInfo.FunctionReturnsType; import fi.nls.hakunapi.cql2.function.Function; +import fi.nls.hakunapi.cql2.model.FilterContext; -public class ST_Buffer extends Function { +public class ST_Buffer extends Function { /* * reference: http://postgis.net/docs/manual-3.2/ST_Buffer.html @@ -31,7 +32,7 @@ public ST_Buffer() { } @Override - public Object invoke(List args, Object context) { + public Object invoke(List args, FilterContext context) { Geometry geom = getGeometryArg(args, "geom"); double radius_of_buffer = getNumberArg(args, "radius_of_buffer").doubleValue(); String buffer_style_parameters = getStringArg(args, "buffer_style_parameters"); diff --git a/src/hakunapi-cql2-functions/src/test/java/fi/nls/hakunapi/cql2/function/geometry/TestGeometryFunctionsFactory.java b/src/hakunapi-cql2-functions/src/test/java/fi/nls/hakunapi/cql2/function/geometry/TestGeometryFunctionsFactory.java index 5ca2aab1..91d8ebc2 100644 --- a/src/hakunapi-cql2-functions/src/test/java/fi/nls/hakunapi/cql2/function/geometry/TestGeometryFunctionsFactory.java +++ b/src/hakunapi-cql2-functions/src/test/java/fi/nls/hakunapi/cql2/function/geometry/TestGeometryFunctionsFactory.java @@ -25,7 +25,7 @@ public void testGeometryFunctionsBuffer() { List functionTables = factory.createFunctionTables(); assertEquals(functionTables.size(), 1); - Function buffer = functionTables.get(0).getFunction("Buffer"); + Function buffer = functionTables.get(0).getFunction("Buffer"); assertNotNull(buffer); LineString geomFrom = geomFactory @@ -45,7 +45,7 @@ public void testGeometryFunctionsSTBuffer() { List functionTables = factory.createFunctionTables(); assertEquals(functionTables.size(), 1); - Function st_buffer = functionTables.get(0).getFunction("ST_Buffer"); + Function st_buffer = functionTables.get(0).getFunction("ST_Buffer"); assertNotNull(st_buffer); LineString geomFrom = geomFactory @@ -65,7 +65,7 @@ public void testGeometryFunctionsSTBufferMiter() { List functionTables = factory.createFunctionTables(); assertEquals(functionTables.size(), 1); - Function st_buffer = functionTables.get(0).getFunction("ST_Buffer"); + Function st_buffer = functionTables.get(0).getFunction("ST_Buffer"); assertNotNull(st_buffer); LineString geomFrom = geomFactory diff --git a/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/function/CQL2Functions.java b/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/function/CQL2Functions.java index 2d39f044..db072a5e 100644 --- a/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/function/CQL2Functions.java +++ b/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/function/CQL2Functions.java @@ -35,7 +35,7 @@ public Optional getAnyFunction(String functionName) { .filter(Objects::nonNull).findFirst(); } - public static FunctionInfo fromFunc(Function func) { + public static FunctionInfo fromFunc(Function func) { final FunctionInfo funcInfo = new FunctionInfo(func.getName(), func.getDescription(), func.getMetadataUrl()); funcInfo.setArguments(func.getArguments()); funcInfo.setReturns(func.getReturns()); diff --git a/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/function/Function.java b/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/function/Function.java index 13b5e3ed..526f7390 100644 --- a/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/function/Function.java +++ b/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/function/Function.java @@ -12,11 +12,11 @@ import fi.nls.hakunapi.core.schemas.FunctionReturnsInfo; import fi.nls.hakunapi.core.schemas.FunctionReturnsInfo.FunctionReturnsType; -public class Function { +public class Function { @FunctionalInterface - public interface FunctionImplementation { - Object invoke(Function func, List args, Object context); + public interface FunctionImplementation { + Object invoke(Function func, List args, TContext context); } protected String name; @@ -50,14 +50,14 @@ public void setMetadataUrl(String metadataUrl) { this.metadataUrl = metadataUrl; } - public Function argument(String name, FunctionArgumentType type) { + public Function argument(String name, FunctionArgumentType type) { FunctionArgumentInfo funcArgInfo = new FunctionArgumentInfo(name, null, type); argumentsPosName.put(name, arguments.size()); arguments.add(funcArgInfo); return this; } - public Function returns(FunctionReturnsType type) { + public Function returns(FunctionReturnsType type) { returns = new FunctionReturnsInfo(type); return this; } @@ -70,7 +70,7 @@ public FunctionReturnsInfo getReturns() { return returns; } - public Object invoke(List args, Object context) { + public Object invoke(List args, TContext context) { throw new RuntimeException("Missing function implementation"); } @@ -81,19 +81,19 @@ protected Function(String name, String description, String metadataUrl) { this.metadataUrl = metadataUrl; } - public static Function of(String name) { - return new Function(name, null, null); + public static Function of(String name) { + return new Function<>(name, null, null); } - public static Function of(String name, String description, String metadataUrl) { + public static Function of(String name, String description, String metadataUrl) { - return new Function(name, description, metadataUrl); + return new Function<>(name, description, metadataUrl); } - public static Function of(String name, final FunctionImplementation impl) { + public static Function of(String name, final FunctionImplementation impl) { - return new Function(name, null, null) { - public Object invoke(List args, Object context) { + return new Function<>(name, null, null) { + public Object invoke(List args, TContext context) { return impl.invoke(this, args, context); } }; diff --git a/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/model/ExpressionToHakunaFilter.java b/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/model/ExpressionToHakunaFilter.java index 98cb8da0..b5a9c774 100644 --- a/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/model/ExpressionToHakunaFilter.java +++ b/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/model/ExpressionToHakunaFilter.java @@ -27,27 +27,27 @@ import fi.nls.hakunapi.cql2.model.spatial.SpatialLiteral; import fi.nls.hakunapi.cql2.model.spatial.SpatialPredicate; -public class ExpressionToHakunaFilter implements ExpressionVisitor { +public class ExpressionToHakunaFilter implements ExpressionVisitor { @Override - public Object visit(And and, Object context) { + public Object visit(And and, FilterContext context) { return Filter .and(and.getChildren().stream().map(child -> visit(child, context)).map(Filter.class::cast).collect(Collectors.toList())); } @Override - public Object visit(Or or, Object context) { + public Object visit(Or or, FilterContext context) { return Filter .or(or.getChildren().stream().map(child -> visit(child, context)).map(Filter.class::cast).collect(Collectors.toList())); } @Override - public Object visit(Not not, Object context) { + public Object visit(Not not, FilterContext context) { return ((Filter) visit(not.getExpression(), context)).negate(); } @Override - public Object visit(BinaryComparisonPredicate p, Object context) { + public Object visit(BinaryComparisonPredicate p, FilterContext context) { PropertyName propertyName = p.getProp(); Expression e = p.getValue(); HakunaProperty prop = visit(propertyName, context); @@ -73,7 +73,7 @@ public Object visit(BinaryComparisonPredicate p, Object context) { } @Override - public Object visit(LikePredicate p, Object context) { + public Object visit(LikePredicate p, FilterContext context) { PropertyName propertyName = p.getProperty(); StringLiteral pattern = p.getPattern(); HakunaProperty prop = visit(propertyName, context); @@ -83,46 +83,43 @@ public Object visit(LikePredicate p, Object context) { } @Override - public Object visit(IsNullPredicate p, Object context) { + public Object visit(IsNullPredicate p, FilterContext context) { return Filter.isNull(visit(p.getProperty(), context)); } @Override - public HakunaProperty visit(PropertyName p, Object context) { - HakunaProperty prop = ((FilterContext) context).queryables.get(p.getValue()); - if (prop == null) { - throw new IllegalArgumentException("Non queryable property " + p.getValue()); - } - return prop; + public HakunaProperty visit(PropertyName p, FilterContext context) { + return context.queryable(p.getValue()) + .orElseThrow(() -> new IllegalArgumentException("Non queryable property " + p.getValue())); } @Override - public Object visit(BooleanLiteral p, Object context) { + public Object visit(BooleanLiteral p, FilterContext context) { return p.getValue() ? "true" : "false"; } @Override - public Object visit(NumberLiteral p, Object context) { + public Object visit(NumberLiteral p, FilterContext context) { return Double.toString(p.getValue()); } @Override - public Object visit(StringLiteral p, Object context) { + public Object visit(StringLiteral p, FilterContext context) { return p.getValue(); } @Override - public Object visit(DateLiteral p, Object context) { + public Object visit(DateLiteral p, FilterContext context) { return p.getDate().toString(); } @Override - public Object visit(TimestampLiteral p, Object context) { + public Object visit(TimestampLiteral p, FilterContext context) { return p.getTimestamp().toString(); } @Override - public Object visit(SpatialPredicate p, Object context) { + public Object visit(SpatialPredicate p, FilterContext context) { HakunaProperty property = visit(p.getProp(), context); if (!(property instanceof HakunaPropertyGeometry)) { throw new IllegalArgumentException( @@ -159,12 +156,12 @@ public Object visit(SpatialPredicate p, Object context) { } @Override - public Object visit(SpatialLiteral p, Object context) { + public Object visit(SpatialLiteral p, FilterContext context) { return p.getGeometry(); } @Override - public Object visit(FunctionCall functionCall, Object context) { + public Object visit(FunctionCall functionCall, FilterContext context) { List args = functionCall.getArgs().stream() .map(arg -> visit(arg, context)) .collect(Collectors.toList()); @@ -185,7 +182,7 @@ private static final Number parseNumber(Object o) { } } - private Object invokeFunction(Function f, List visitedArgs, Object context) { + private Object invokeFunction(Function f, List visitedArgs, FilterContext context) { // visit() transformed all literal values (except Geometries) to strings for Filter // => Convert them back as the Functions expect objects that match the arg type List fArgs = f.getArguments(); @@ -201,7 +198,7 @@ private Object invokeFunction(Function f, List visitedArgs, Object conte } @Override - public Object visit(EmptyExpression ee, Object context) { + public Object visit(EmptyExpression ee, FilterContext context) { return Filter.PASS; } diff --git a/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/model/ExpressionVisitor.java b/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/model/ExpressionVisitor.java index 797dcc0c..ba574a8b 100644 --- a/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/model/ExpressionVisitor.java +++ b/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/model/ExpressionVisitor.java @@ -13,9 +13,9 @@ import fi.nls.hakunapi.cql2.model.spatial.SpatialLiteral; import fi.nls.hakunapi.cql2.model.spatial.SpatialPredicate; -public interface ExpressionVisitor { +public interface ExpressionVisitor { - default public Object visit(Expression e, Object context) { + default public Object visit(Expression e, T context) { if (e instanceof BinaryComparisonPredicate) { return visit((BinaryComparisonPredicate) e, context); } else if (e instanceof LikePredicate) { @@ -53,26 +53,26 @@ default public Object visit(Expression e, Object context) { throw new IllegalStateException("Unknown Expression " + e.getClass()); } - public Object visit(And and, Object context); - public Object visit(Or or, Object context); - public Object visit(Not not, Object context); + public Object visit(And and, T context); + public Object visit(Or or, T context); + public Object visit(Not not, T context); - public Object visit(SpatialPredicate p, Object context); - public Object visit(SpatialLiteral p, Object context); + public Object visit(SpatialPredicate p, T context); + public Object visit(SpatialLiteral p, T context); - public Object visit(BinaryComparisonPredicate p, Object context); - public Object visit(LikePredicate p, Object context); - public Object visit(IsNullPredicate p, Object context); + public Object visit(BinaryComparisonPredicate p, T context); + public Object visit(LikePredicate p, T context); + public Object visit(IsNullPredicate p, T context); - public Object visit(PropertyName p, Object context); + public Object visit(PropertyName p, T context); - public Object visit(BooleanLiteral p, Object context); - public Object visit(NumberLiteral p, Object context); - public Object visit(StringLiteral p, Object context); - public Object visit(DateLiteral p, Object context); - public Object visit(TimestampLiteral p, Object context); + public Object visit(BooleanLiteral p, T context); + public Object visit(NumberLiteral p, T context); + public Object visit(StringLiteral p, T context); + public Object visit(DateLiteral p, T context); + public Object visit(TimestampLiteral p, T context); - public Object visit(FunctionCall fn, Object context); - public Object visit(EmptyExpression ee, Object context); + public Object visit(FunctionCall fn, T context); + public Object visit(EmptyExpression ee, T context); } diff --git a/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/model/FilterContext.java b/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/model/FilterContext.java index af29ab3a..ce381a88 100644 --- a/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/model/FilterContext.java +++ b/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/model/FilterContext.java @@ -1,26 +1,16 @@ package fi.nls.hakunapi.cql2.model; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; +import java.util.Optional; -import fi.nls.hakunapi.core.FeatureType; import fi.nls.hakunapi.core.SRIDCode; +import fi.nls.hakunapi.core.projection.ProjectionTransformerFactory; import fi.nls.hakunapi.core.property.HakunaProperty; -public class FilterContext { +public interface FilterContext { - public final Map queryables; - public final SRIDCode filterSrid; - - public FilterContext(FeatureType ft, SRIDCode filterSrid) { - this(ft.getQueryableProperties(), filterSrid); - } - - public FilterContext(List queryables, SRIDCode filterSrid) { - this.queryables = queryables.stream() - .collect(Collectors.toMap(HakunaProperty::getName, it -> it)); - this.filterSrid = filterSrid; - } + public Optional queryable(String name); + public Optional storageSrid(); + public Optional projectionTransformer(); + public SRIDCode filterSrid(); } diff --git a/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/model/SimpleFilterContext.java b/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/model/SimpleFilterContext.java new file mode 100644 index 00000000..48d92492 --- /dev/null +++ b/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/model/SimpleFilterContext.java @@ -0,0 +1,41 @@ +package fi.nls.hakunapi.cql2.model; + +import java.util.Optional; + +import fi.nls.hakunapi.core.FeatureType; +import fi.nls.hakunapi.core.SRIDCode; +import fi.nls.hakunapi.core.projection.ProjectionTransformerFactory; +import fi.nls.hakunapi.core.property.HakunaProperty; + +public class SimpleFilterContext implements FilterContext { + + private final FeatureType ft; + private final SRIDCode filterSrid; + + public SimpleFilterContext(FeatureType ft, SRIDCode filterSrid) { + this.ft = ft; + this.filterSrid = filterSrid; + } + + @Override + public Optional queryable(String name) { + return ft.getQueryableProperties().stream() + .filter(prop -> prop.getName().equals(name)) + .findAny(); + } + + @Override + public Optional storageSrid() { + return ft.getGeom() == null ? Optional.empty() : ft.getSrid(ft.getGeom().getStorageSRID()); + } + + @Override + public Optional projectionTransformer() { + return Optional.ofNullable(ft.getProjectionTransformerFactory()); + } + @Override + public SRIDCode filterSrid() { + return filterSrid; + } + +} diff --git a/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/text/CQL2Text.java b/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/text/CQL2Text.java index 76afe598..4e09a89d 100644 --- a/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/text/CQL2Text.java +++ b/src/hakunapi-cql2/src/main/java/fi/nls/hakunapi/cql2/text/CQL2Text.java @@ -16,6 +16,7 @@ import fi.nls.hakunapi.cql2.model.Expression; import fi.nls.hakunapi.cql2.model.ExpressionToHakunaFilter; import fi.nls.hakunapi.cql2.model.FilterContext; +import fi.nls.hakunapi.cql2.model.SimpleFilterContext; public class CQL2Text implements FilterParser { @@ -32,7 +33,7 @@ public String getCode() { public Filter parse(FeatureType ft, String filter, int filterSrid) throws IllegalArgumentException { try { Expression expression = parse(filter, new GeometryFactory(new PrecisionModel(), filterSrid)); - FilterContext context = new FilterContext(ft, ft.getSrid(filterSrid).get()); + FilterContext context = new SimpleFilterContext(ft, ft.getSrid(filterSrid).get()); return (Filter) new ExpressionToHakunaFilter().visit(expression, context); } catch (Exception e) { if (e instanceof IllegalArgumentException) { diff --git a/src/hakunapi-cql2/src/test/java/fi/nls/hakunapi/cql2/text/ExpressionToHakunaFilterTest.java b/src/hakunapi-cql2/src/test/java/fi/nls/hakunapi/cql2/text/ExpressionToHakunaFilterTest.java index 85a037af..0590d4e1 100644 --- a/src/hakunapi-cql2/src/test/java/fi/nls/hakunapi/cql2/text/ExpressionToHakunaFilterTest.java +++ b/src/hakunapi-cql2/src/test/java/fi/nls/hakunapi/cql2/text/ExpressionToHakunaFilterTest.java @@ -9,12 +9,14 @@ import org.locationtech.jts.geom.Geometry; import java.util.List; +import java.util.Optional; import fi.nls.hakunapi.core.SRIDCode; import fi.nls.hakunapi.core.filter.Filter; import fi.nls.hakunapi.core.filter.FilterOp; import fi.nls.hakunapi.core.geom.HakunaGeometryDimension; import fi.nls.hakunapi.core.geom.HakunaGeometryType; +import fi.nls.hakunapi.core.projection.ProjectionTransformerFactory; import fi.nls.hakunapi.core.property.HakunaProperty; import fi.nls.hakunapi.core.property.HakunaPropertyWriters; import fi.nls.hakunapi.core.property.simple.HakunaPropertyGeometry; @@ -31,7 +33,7 @@ public class ExpressionToHakunaFilterTest { @BeforeClass public static void init() { - Function buffer = Function.of("buffer", (fn, args, ctx) -> { + Function buffer = Function.of("buffer", (fn, args, ctx) -> { Geometry g = (Geometry) args.get(0); double distance = ((Number) args.get(1)).doubleValue(); return g.buffer(distance); @@ -40,7 +42,7 @@ public static void init() { .argument("distance", FunctionArgumentType.number) .returns(FunctionReturnsType.geometry); - Function centroid = Function.of("centroid", (fn, args, ctx) -> { + Function centroid = Function.of("centroid", (fn, args, ctx) -> { Geometry g = (Geometry) args.get(0); return g.getFactory().createPoint(Centroid.getCentroid(g)); }) @@ -61,7 +63,27 @@ public void testNestedFunctionCall() { String filter = "S_Intersects(footprint, BUFFER(cenTroid( lineString (0 0, 100 100)), 10))"; SRIDCode filterSrid = new SRIDCode(4326, true, true, HakunaGeometryDimension.XY); - FilterContext ctx = new FilterContext(queryables, filterSrid); + FilterContext ctx = new FilterContext() { + @Override + public Optional queryable(String name) { + return queryables.stream().filter(x -> x.getName().equals(name)).findAny(); + } + + @Override + public Optional storageSrid() { + return Optional.empty(); + } + + @Override + public Optional projectionTransformer() { + return Optional.empty(); + } + + @Override + public SRIDCode filterSrid() { + return filterSrid; + } + }; Expression expr = CQL2Text.parse(filter); Filter f = (Filter) new ExpressionToHakunaFilter().visit(expr, ctx);