Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.<x>.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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<OutputFormat> getOutputFormats() {
return null;
}

@Override
public OutputFormat getOutputFormat(String f) {
return null;
}

@Override
public Collection<FeatureType> 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());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -212,6 +214,19 @@ protected List<SimpleSource> getSources(HakunaConfigParser config) {
return sources;
}

private static void checkSridsKnown(String collectionId, FeatureType ft, List<SRIDCode> 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<SRIDCode> getKnownSrids(Properties p, HakunaConfigParser config) {
List<Integer> codes = config.getKnownSrids();
return CRSRegistryProvider.getCRSRegistry()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading