diff --git a/CHANGELOG.md b/CHANGELOG.md index b06869f2..f8728e97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Functional changes +- An unknown `crs`, `bbox-crs` or `filter-crs` SRID returns 400 instead of 500, and a collection `srid` missing from the global `srid` fails startup with a message naming both. - Fixed CSV output (`f=csv`) failing with a NullPointerException on the first geometry or floating-point value: no number formatter was ever set. CSV now uses the GeoJSON defaults: coordinates with 3 decimals for metres and 7 for degrees, double properties with up to 8 and float properties with up to 5. - New community module `hakunapi-bytes` (Java 22+): a shared byte access layer for file-backed sources, covering local files (pread, or mmap or a whole-file in-memory copy as an opt-in) and HTTP Range with a shared block cache and adaptive read-ahead. It also reads an entry of a SOZip (seek-optimized ZIP) archive with random access, locally or over HTTP, and opens a location written in GDAL's `/vsizip/` / `/vsicurl/` syntax. No source uses it yet. It is built only on a 22+ JDK, via the new `ffm` profile in `src-community/pom.xml`. - GeoJSON date and timestamp properties are written straight into the output buffer instead of via an intermediate `String`, cutting ~17% off the write time of a date-bearing feature collection. Output bytes are unchanged. @@ -24,6 +25,7 @@ For a full list of changes see: https://github.com/nlsfi/hakunapi/milestone/17 - **Oracle source removed.** Modules `hakunapi-source-oracle` and `hakunapi-oracle-webapp-jakarta` dropped. Users needing Oracle Spatial data sources should pin hakunapi ≤ 1.x or maintain the module out-of-tree. - **OpenTelemetry removed.** Modules `hakunapi-telemetry-opentelemetry` and `hakunapi-telemetry-webapp-jakarta` dropped. The `hakunapi-telemetry` JSON-log mode and NOP telemetry remain available; the `ServiceTelemetry`/`RequestTelemetry`/`TelemetryFactory` SPI in `hakunapi-core` is unchanged. - **Community modules moved and unbundled.** Modules `hakunapi-smile`, `hakunapi-esbulk`, `hakunapi-gml` and `hakunapi-proj-jhe` moved from `src/` to the new `src-community/` folder and are no longer bundled in the reference `features.war` (`hakunapi-simple-webapp-jakarta`). Maven coordinates and Java packages are unchanged, and versions are still managed by the root POM. Deployments that enable `formats=...,smile` or `formats=...,esbulk`, or select the projection factory `fi.nls.hakunapi.proj.jhe.JHeProjectionTransformerFactory` via the `proj` configuration key, must now add the corresponding dependency to their own webapp and rebuild. `hakunapi-proj-gt` remains the bundled default for coordinate transformations. See [CONTRIBUTING.md](CONTRIBUTING.md#community-modules). +- **Global `srid` must list every collection SRID.** The global `srid` property is the CRS registry, and every SRID in `collections..srid` or `default.collections.srid` must be found in it. Otherwise a request using that CRS failed with HTTP 500. Add the missing SRIDs to the global `srid` list. ### Functional changes diff --git a/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/param/CrsParam.java b/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/param/CrsParam.java index b1dcfe75..893b3ea8 100644 --- a/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/param/CrsParam.java +++ b/src/hakunapi-core/src/main/java/fi/nls/hakunapi/core/param/CrsParam.java @@ -34,6 +34,9 @@ public void modify(FeatureServiceConfig service, GetFeatureRequest request, Stri throws IllegalArgumentException { if (value != null && !value.isEmpty()) { int srid = CrsUtil.parseSRID(value, getParamName()); + if (service.getSridCode(srid).isEmpty()) { + throw new IllegalArgumentException(CrsUtil.ERR_UNSUPPORTED_CRS); + } for (GetFeatureCollection c : request.getCollections()) { HakunaPropertyGeometry geom = c.getFt().getGeom(); if (geom != null && !geom.isSRIDSupported(srid)) { diff --git a/src/hakunapi-core/src/test/java/fi/nls/hakunapi/core/param/CrsParamTest.java b/src/hakunapi-core/src/test/java/fi/nls/hakunapi/core/param/CrsParamTest.java new file mode 100644 index 00000000..087232c8 --- /dev/null +++ b/src/hakunapi-core/src/test/java/fi/nls/hakunapi/core/param/CrsParamTest.java @@ -0,0 +1,93 @@ +package fi.nls.hakunapi.core.param; + +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +import org.junit.Before; +import org.junit.Test; + +import fi.nls.hakunapi.core.FeatureProducer; +import fi.nls.hakunapi.core.FeatureServiceConfig; +import fi.nls.hakunapi.core.FeatureType; +import fi.nls.hakunapi.core.OutputFormat; +import fi.nls.hakunapi.core.SRIDCode; +import fi.nls.hakunapi.core.SimpleFeatureType; +import fi.nls.hakunapi.core.property.simple.HakunaPropertyGeometry; +import fi.nls.hakunapi.core.request.GetFeatureCollection; +import fi.nls.hakunapi.core.request.GetFeatureRequest; +import fi.nls.hakunapi.core.util.CrsUtil; + +public class CrsParamTest { + + private static final String EPSG_3067 = "http://www.opengis.net/def/crs/EPSG/0/3067"; + + private SimpleFeatureType myCollection; + private FeatureServiceConfig service; + + @Before + public void init() { + myCollection = new SimpleFeatureType() { + @Override + public FeatureProducer getFeatureProducer() { + return null; + } + }; + myCollection.setName("myCollection"); + myCollection.setProperties(Collections.emptyList()); + myCollection.setStaticFilters(Collections.emptyList()); + myCollection.setGeom(new HakunaPropertyGeometry("geometry", "table", null, false, null, new int[] { 3067 }, 84, 2, (__, ___, ____) -> {})); + + service = new FeatureServiceConfig() { + @Override + public Collection getOutputFormats() { + return null; + } + + @Override + public OutputFormat getOutputFormat(String f) { + return null; + } + + @Override + public Collection getCollections() { + return Arrays.asList(myCollection); + } + + @Override + public FeatureType getCollection(String name) { + return myCollection.getName().equals(name) ? myCollection : null; + } + }; + } + + private GetFeatureRequest request() { + GetFeatureRequest req = new GetFeatureRequest(); + req.addCollection(new GetFeatureCollection(myCollection)); + return req; + } + + @Test + public void testSridSupportedByCollectionButUnknownToService() { + service.setKnownSrids(Arrays.asList(SRIDCode.CRS84, SRIDCode.WGS84)); + try { + new CrsParam().modify(service, request(), EPSG_3067); + } catch (IllegalArgumentException e) { + assertEquals(CrsUtil.ERR_UNSUPPORTED_CRS, e.getMessage()); + return; + } + throw new AssertionError("Expected IllegalArgumentException"); + } + + @Test + public void testSridKnownToService() { + service.setKnownSrids(Arrays.asList(SRIDCode.CRS84, SRIDCode.WGS84, + new SRIDCode(3067, false, false, fi.nls.hakunapi.core.geom.HakunaGeometryDimension.XY))); + GetFeatureRequest req = request(); + new CrsParam().modify(service, req, EPSG_3067); + assertEquals(3067, req.getSRID()); + } + +} 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 337167d5..10f79dfd 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 @@ -32,6 +32,7 @@ import fi.nls.hakunapi.core.CRSRegistryProvider; import fi.nls.hakunapi.core.ConformanceClass; import fi.nls.hakunapi.core.FeatureType; +import fi.nls.hakunapi.core.property.simple.HakunaPropertyGeometry; import fi.nls.hakunapi.core.FilterParser; import fi.nls.hakunapi.core.MetadataFormat; import fi.nls.hakunapi.core.OutputFormat; @@ -106,6 +107,7 @@ public void contextInitialized(ServletContextEvent sce) { for (String collectionId : parser.readCollectionIds()) { FeatureType ft = parser.readCollection(configPath, sourcesByType, collectionId); ft = parser.applyJsonSchema(ft, schemas); + checkSridsKnown(collectionId, ft, knownSrids); collections.put(collectionId, ft); } @@ -212,6 +214,19 @@ protected List getSources(HakunaConfigParser config) { return sources; } + private static void checkSridsKnown(String collectionId, FeatureType ft, List knownSrids) { + HakunaPropertyGeometry geom = ft.getGeom(); + if (geom == null) { + return; + } + for (int srid : geom.getSrid()) { + if (knownSrids.stream().noneMatch(it -> it.getSrid() == srid)) { + throw new IllegalArgumentException("Collection '" + collectionId + "' has srid " + srid + + " which is missing from the global 'srid' property"); + } + } + } + private static List getKnownSrids(Properties p, HakunaConfigParser config) { List codes = config.getKnownSrids(); return CRSRegistryProvider.getCRSRegistry() diff --git a/webapp-jakarta/hakunapi-simple-webapp-test-jakarta/src/test/java/fi/nls/hakunapi/simple/webapp/jakarta/features/OgcApiFeaturesPart1CoreTest.java b/webapp-jakarta/hakunapi-simple-webapp-test-jakarta/src/test/java/fi/nls/hakunapi/simple/webapp/jakarta/features/OgcApiFeaturesPart1CoreTest.java index 760440a8..c1ced96f 100644 --- a/webapp-jakarta/hakunapi-simple-webapp-test-jakarta/src/test/java/fi/nls/hakunapi/simple/webapp/jakarta/features/OgcApiFeaturesPart1CoreTest.java +++ b/webapp-jakarta/hakunapi-simple-webapp-test-jakarta/src/test/java/fi/nls/hakunapi/simple/webapp/jakarta/features/OgcApiFeaturesPart1CoreTest.java @@ -246,6 +246,40 @@ public void testCollectionsAallonmurtajaItems2() { } + @Test + public void testConfiguredCrs() { + jakarta.ws.rs.core.Response r = target("/collections/test_collection/items") + .queryParam("crs", "http://www.opengis.net/def/crs/EPSG/0/3067") + .request().get(); + assertEquals(200, r.getStatus()); + } + + @Test + public void testUnconfiguredCrsIsBadRequest() { + jakarta.ws.rs.core.Response r = target("/collections/test_collection/items") + .queryParam("crs", "http://www.opengis.net/def/crs/EPSG/0/3857") + .request().get(); + assertEquals(400, r.getStatus()); + } + + @Test + public void testUnconfiguredBboxCrsIsBadRequest() { + jakarta.ws.rs.core.Response r = target("/collections/test_collection/items") + .queryParam("bbox-crs", "http://www.opengis.net/def/crs/EPSG/0/3857") + .queryParam("bbox", "0,0,1,1") + .request().get(); + assertEquals(400, r.getStatus()); + } + + @Test + public void testUnconfiguredFilterCrsIsBadRequest() { + jakarta.ws.rs.core.Response r = target("/collections/test_collection/items") + .queryParam("filter-crs", "http://www.opengis.net/def/crs/EPSG/0/3857") + .queryParam("filter", "S_INTERSECTS(geom,POINT(0 0))") + .request().get(); + assertEquals(400, r.getStatus()); + } + @Test public void testNextLinkIsFollowable() { // Force pagination with limit=1; include a crs param with reserved chars diff --git a/webapp-jakarta/hakunapi-simple-webapp-test-jakarta/src/test/java/fi/nls/hakunapi/simple/webapp/jakarta/features/UnknownSridStartupTest.java b/webapp-jakarta/hakunapi-simple-webapp-test-jakarta/src/test/java/fi/nls/hakunapi/simple/webapp/jakarta/features/UnknownSridStartupTest.java new file mode 100644 index 00000000..348a025c --- /dev/null +++ b/webapp-jakarta/hakunapi-simple-webapp-test-jakarta/src/test/java/fi/nls/hakunapi/simple/webapp/jakarta/features/UnknownSridStartupTest.java @@ -0,0 +1,52 @@ +package fi.nls.hakunapi.simple.webapp.jakarta.features; + +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; + +import jakarta.servlet.ServletContextEvent; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import fi.nls.hakunapi.simple.webapp.jakarta.HakunaContextListener; +import fi.nls.hakunapi.simple.webapp.jakarta.HakunaTestServletContext; + +public class UnknownSridStartupTest { + + @Rule + public TemporaryFolder tmp = new TemporaryFolder(); + + @Test + public void testCollectionSridMissingFromGlobalSridFailsStartup() throws Exception { + String config = String.join("\n", + "$config.version=2", + "servers=dev", + "servers.dev.url=https://localhost/hakuna", + "db.classes=fi.nls.hakunapi.source.HakunaTestSource", + "db=db", + "collections=c", + "collections.c.type=test", + "collections.c.db=db", + "collections.c.table=c", + "collections.c.srid=3067", + "collections.c.geometry.mapping=geom", + "collections.c.geometry.type=POINT", + "collections.c.testdata.1=1;POINT(1 1)"); + File dir = tmp.newFolder(); + Files.writeString(new File(dir, "hakuna.properties").toPath(), config, StandardCharsets.UTF_8); + System.setProperty("hakuna.config.path", dir.getAbsolutePath() + "/"); + + try { + new HakunaContextListener().contextInitialized(new ServletContextEvent(new HakunaTestServletContext())); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage(), e.getMessage().contains("missing from the global 'srid'")); + return; + } + throw new AssertionError("Expected IllegalArgumentException"); + } + +} diff --git a/webapp-jakarta/hakunapi-simple-webapp-test-jakarta/src/test/resources/hakuna/hakuna.properties b/webapp-jakarta/hakunapi-simple-webapp-test-jakarta/src/test/resources/hakuna/hakuna.properties index 81b8590a..e99897ad 100644 --- a/webapp-jakarta/hakunapi-simple-webapp-test-jakarta/src/test/resources/hakuna/hakuna.properties +++ b/webapp-jakarta/hakunapi-simple-webapp-test-jakarta/src/test/resources/hakuna/hakuna.properties @@ -23,6 +23,8 @@ security.apiKey.name=api-key getfeatures.limit.default=1000 getfeatures.limit.max=10000 +srid=3067,4258,3046,3047,3048,3873,3874,3875,3876,3877,3878,3879,3880,3881,3882,3883,3884,3885 + db.classes=fi.nls.hakunapi.source.HakunaTestSource db=db diff --git a/webapp-jakarta/hakunapi-simple-webapp-test-jakarta/src/test/resources/hakuna_schema/hakuna.properties b/webapp-jakarta/hakunapi-simple-webapp-test-jakarta/src/test/resources/hakuna_schema/hakuna.properties index ef1ace28..82f26df8 100644 --- a/webapp-jakarta/hakunapi-simple-webapp-test-jakarta/src/test/resources/hakuna_schema/hakuna.properties +++ b/webapp-jakarta/hakunapi-simple-webapp-test-jakarta/src/test/resources/hakuna_schema/hakuna.properties @@ -17,6 +17,7 @@ db=db schema=my_schema schema.my_schema.path=test_schema.json +srid=3067 default.collections.srid=3067 collections=building_part_area,fence