From c03aa2efbce94884b5ef26b23a2d9ee6ebcb3e37 Mon Sep 17 00:00:00 2001 From: jampukka Date: Thu, 22 Oct 2026 22:24:25 +0300 Subject: [PATCH] Add a configuration reference for the core modules and the reference webapp docs/overview/configuration.md lists every configuration key read by src/ and webapp-jakarta/ with its default, and the traps (blank values, trimming, silently skipped formats). Community modules are not covered. CONTRIBUTING.md and the changelog say a PR that changes a key is expected to update the page. Related changes found while checking the keys against the code: - servers=a, b is trimmed like every other list key (it used to fail startup with a NullPointerException) - the PostGIS source no longer reads id.alias, geometry.alias and properties.

.alias. They were a not-working and unfinished idea: properties.

.alias made startup fail with a NullPointerException - the example configs no longer set writeNulls and geometry.storageSrid, which nothing reads; addresses_view declares its storage SRID with srid.storage --- CHANGELOG.md | 3 + CONTRIBUTING.md | 5 + docker/cfg/addresses.properties | 4 +- docker/cfg/addresses_gpkg.properties | 1 - docs/overview/configuration.md | 374 ++++++++++++++++++ readme.md | 4 + .../core/config/HakunaConfigParser.java | 2 +- .../core/config/HakunaConfigParserTest.java | 14 + .../simple/postgis/PostGISSimpleSource.java | 98 +---- 9 files changed, 416 insertions(+), 89 deletions(-) create mode 100644 docs/overview/configuration.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 53a5158f..78d5f1a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -80,6 +80,9 @@ - The HTML collection page links to the collection schema and shows the spatial and temporal extent when available (#185). - HTML templates: Bootstrap 5.0.0-beta3 -> 5.3.8, Leaflet 1.7.1 -> 1.9.4, proj4 2.7.5 -> 2.22.0 and swagger-ui 3.45.1 -> 5.29.1 (still loaded from CDNs, with updated SRI hashes). The Leaflet and proj4 imports now live in `mapLibs.ftl`, shared by `featureCollection.ftl` and `feature.ftl`; a custom template directory that overrides these two templates keeps its own versions (#199). - New config key `api.collections.links.*` adds links to the top-level `links` array of the `/collections` response, in both JSON and HTML. Parallel to the existing `api.links.*` (landing page) and `default.collections.links.*` / `collections..links.*` (per-collection entries). +- New [configuration reference](docs/overview/configuration.md): every configuration key of the core modules and the reference webapp, with its default, on one page. Pull requests that change a key are expected to update it. +- `servers` is now trimmed like the other list keys: `servers=dev, prod` works instead of failing startup with a `NullPointerException`. +- PostGIS source: the keys `collections..id.alias`, `geometry.alias` and `properties.

.alias` are removed. They were a not-working and unfinished idea (`properties.

.alias` made startup fail with a `NullPointerException`); they are now ignored. ## 2.0.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8739e913..9c982e7d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -73,6 +73,11 @@ ask you to make changes before merging it. of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license. +> ### Configuration reference +> A Pull Request that adds, renames or removes a configuration key, or changes its +default, is expected to update [docs/overview/configuration.md](docs/overview/configuration.md) +in the same Pull Request. + ## Community Modules Hakunapi has a second tier of modules for contributions that are useful but not part of diff --git a/docker/cfg/addresses.properties b/docker/cfg/addresses.properties index 807935b0..9714c2ac 100644 --- a/docker/cfg/addresses.properties +++ b/docker/cfg/addresses.properties @@ -148,7 +148,6 @@ collections=addresses,addresses_view collections.addresses.casei=lower ## Should properties with null values be omitted from the response -collections.addresses.writeNulls=true ## PostgreSQL/PostGIS configuration ### This collection is backed by 'pg' (PostgreSQL/PostGIS) source/driver @@ -205,7 +204,6 @@ collections.addresses_view.title=Simple Addresses (EPSG:3903 view) collections.addresses_view.description=Addresses of Finlands Buildings using a database view transformed to EPSG:3903. Demonstrates database-level view usage and storage in a latLon-ordered projected CRS (north,east axis order). collections.addresses_view.casei=lower -collections.addresses_view.writeNulls=true collections.addresses_view.type=pg collections.addresses_view.db=db @@ -218,7 +216,7 @@ collections.addresses_view.srid=3903,10774,3067,4258 collections.addresses_view.id.mapping=id collections.addresses_view.geometry.mapping=geom -collections.addresses_view.geometry.storageSrid=3903 +collections.addresses_view.srid.storage=3903 collections.addresses_view.properties=inspireId_localId,inspireId_namespace,component_ThoroughfareName,component_PostalDescriptor,component_AdminUnitName_1,component_AdminUnitName_4,locator_designator_addressNumber,locator_level,locator_level_href,position_specification,position_specification_href,position_method,position_method_href,position_default,building,parcel diff --git a/docker/cfg/addresses_gpkg.properties b/docker/cfg/addresses_gpkg.properties index 4538371e..f98b3bf5 100644 --- a/docker/cfg/addresses_gpkg.properties +++ b/docker/cfg/addresses_gpkg.properties @@ -55,7 +55,6 @@ collections=simple_addresses collections.simple_addresses.casei=lower ## Should properties with null values be omitted from the response -collections.simple_addresses.writeNulls=true ## GeoPackage configuration ### This collection is backed by 'gpkg' (GeoPackage) source/driver diff --git a/docs/overview/configuration.md b/docs/overview/configuration.md new file mode 100644 index 00000000..d16e5620 --- /dev/null +++ b/docs/overview/configuration.md @@ -0,0 +1,374 @@ +# Configuration reference + +Cheat sheet of every configuration key the core modules (`src/`) and the reference webapp (`webapp-jakarta/`) read. The community modules (`src-community/`, for example tiles, smile and esbulk) are not covered here; see their READMEs. For the modules behind them see [modules.md](modules.md); for a working file see [docker/cfg/addresses.properties](../../docker/cfg/addresses.properties). + +**Keeping this page current:** a pull request that adds, renames, removes or changes the default of a configuration key is expected to update this page in the same PR. + +Notation: `` is a collection id, `` a name you choose. "Default: -" means the key is simply absent when unset. + +## Basics + +- The configuration is a Java `.properties` file (the webapp reads it as UTF-8, and as ISO-8859-1 if that fails). +- Lists are comma-separated and each item is trimmed: `collections=a, b`. +- A blank value counts as unset for keys read through `HakunaConfigParser.get`/`getMultiple` and `PropertyUtil.getInt` (a blank or non-numeric int falls back to the default). Keys read raw from the properties keep the blank value: `api.*` (`api.title=` gives an empty title), `servers..*` (a blank `servers..url` gives an empty URL; a missing one fails startup with a `NullPointerException`), `getfeatures.limit.*` and `srid..*` (a blank `srid..geometryDimension` fails startup; blank `wrapXMin`/`wrapXMax` count as unset, but whitespace-only fails). `srid..*` and `getfeatures.limit.*` also get no `${}` expansion. +- Booleans are read with `Boolean.parseBoolean`: anything but `true` (any case) is `false`. +- `${NAME}` in a value is replaced from the environment variable `NAME`, else from the JVM system property `NAME`. An unknown name is left as `${NAME}`. + +### Where the file is found + +Tried in this order. A property is looked up as an environment variable first (name uppercased, `.` replaced by `_`, so `hakunapi.config.path` is `HAKUNAPI_CONFIG_PATH`), then as a JVM system property. + +| Property | Meaning | +| --- | --- | +| `.hakunapi.config.path` | Path of the file for the webapp deployed at `/` | +| `.hakuna.config.path` | Same, older name | +| `hakunapi.config.path` | Path of the file, for any context | +| `hakuna.config.path` | Legacy: a directory; the file is `

/.properties` | +| `HAKUNA_APPLICATION_JSON` | Env var or system property. JSON `{"": {"key": "value"}}`; the entries are applied over the file's keys for that context | + +```properties +# -Dhakunapi.config.path=/app/config/features.properties +# or: export HAKUNAPI_CONFIG_PATH=/app/config/features.properties +``` + +## Service + +### API metadata + +| Key | Default | Description | +| --- | --- | --- | +| `api.title` | `hakunapi OGC API Features Server` | Title in OpenAPI and on the landing page | +| `api.version` | `0.0.1` | API version | +| `api.description` | `hakunapi OGC API Features Server` | Description | +| `api.termsOfService` | - | URL | +| `api.contact.name`, `api.contact.email`, `api.contact.url` | - | Contact is published if at least one is set | +| `api.license.name`, `api.license.url` | - | License is published only if `name` is set | +| `servers` | required | Comma-separated server names | +| `servers..url` | required per server | Base URL, used for every link the server generates. Trailing `/` is removed | +| `servers..description` | - | Description | + +```properties +servers=dev +servers.dev.url=http://localhost:8080/features +servers.dev.description=Development server +``` + +### Links + +Every link needs all four of `href`, `rel`, `type`, `title`. + +| Key | Description | +| --- | --- | +| `api.links` | Names of extra links on the landing page | +| `api.links..href`, `.rel`, `.type`, `.title` | The link | +| `api.collections.links` | Names of extra links in the top-level `links` of `/collections` (JSON and HTML; the HTML page lists all but `self` and `alternate`) | +| `api.collections.links..href`, `.rel`, `.type`, `.title` | The link | + +```properties +api.links=about +api.links.about.href=https://www.example.org/about +api.links.about.rel=about +api.links.about.type=text/html +api.links.about.title=About the service +``` + +### Languages (`locale`) + +| Key | Description | +| --- | --- | +| `locale` | Languages served, comma-separated; the first is the default. Enables `lang` / `Accept-Language` negotiation | +| `locale..path` | Optional UTF-8 properties catalog overlaying the main config; path relative to the config file. Only title and description keys are accepted (`api.title`, `api.description`, `api.links..title`, `collections..title`, `collections..description`, `collections..links..title`, `default.collections.links..title`) | +| `locale..lang` | Language tag; default ``, lowercased. Declaring the same language twice fails startup | + +When `locale` is set every `schema.` must resolve (through `schema..lang`, default ``) to a language listed in `locale`, or startup fails. + +### Security schemes (OpenAPI description) + +| Key | Description | +| --- | --- | +| `security` | Names of schemes. None configured means none published | +| `security..type` | `http` (published as `basic`) or `apiKey`. Anything else fails startup | +| `security..in` | For `apiKey`, required: `header`, `query` or `cookie` | +| `security..name` | For `apiKey`, required: header / query parameter / cookie name | + +This only describes the scheme in the API definition; Hakunapi does not enforce it. + +### Request handling + +| Key | Default | Description | +| --- | --- | --- | +| `getfeatures.limit.default` | `1000` | `limit` when the request has none. An unparsable value falls back to the default | +| `getfeatures.limit.max` | `10000` | Largest accepted `limit` | +| `hakuna.cors` | `true` | Register the CORS filter | +| `hakuna.gzip` | `true` | Register gzip response/request filters | +| `hakuna.content-length-buffer` | `8192` | Jersey `OUTBOUND_CONTENT_LENGTH_BUFFER`, in bytes | + +### Coordinate reference systems + +CRS:84 (`84`) and EPSG:4326 (`4326`) are always known, and `srid.84.*` / `srid.4326.*` keys are ignored. Every SRID used by a collection must be listed in `srid`, otherwise startup fails. + +| Key | Default | Description | +| --- | --- | --- | +| `srid` | `84,4326` | Extra known SRIDs, comma-separated | +| `srid..latLon` | `false` | Axis order is lat,lon (north,east) | +| `srid..degrees` | `false` | Units are degrees | +| `srid..geometryDimension` | `XY` | `XY`, `XYZ` or `XYZM` (case-sensitive) | +| `srid..wrapXMin`, `srid..wrapXMax` | - | Antimeridian wrap range; set both or neither | +| `srid..proj4` | - | proj4 definition of a projected CRS. Used by the HTML pages only: the Leaflet background map of that CRS needs it (together with `formats.html..tileUrl`). Not used for reprojection | + +```properties +srid=3067,4258 +srid.3067.latLon=false +srid.4258.latLon=true +srid.4258.degrees=true +``` + +### Output formats + +`formats` lists the enabled formats; the default is `json` only. Keys under `formats..` are passed to the format; `formats..type` defaults to ``, so the name normally is the type. + +| Type | Module | Keys | +| --- | --- | --- | +| `json` | hakunapi-geojson | `forceLonLat` (default `false`), `formatter.degrees`, `formatter.meters` | +| `jsonfg` | hakunapi-jsonfg | `formatter.degrees`, `formatter.meters` | +| `html` | hakunapi-html | `dir`, `tileUrl`, `tileAttribution`, `tileOriginX`, `tileOriginY`, `.tileUrl` (and the other three per SRID) | +| `gpkg` | hakunapi-gpkg | `dir` (required) | +| `csv` | hakunapi-csv | none | + +A format whose factory is not on the classpath, or that rejects its keys, is skipped without an error only when its factory's `canCreate` says no (a `gpkg` or `html` with a missing or unusable `dir` silently disappears). An unknown format type is also silently ignored (no log). A bad `formatter.degrees`/`formatter.meters` value (not six integers) in `json` or `jsonfg` fails startup instead. If `formats` is set, `json` is only enabled when listed; it is the default only when no format ends up enabled. The GML writer has no factory and cannot be enabled. + +| Key | Default | Description | +| --- | --- | --- | +| `formats..formatter.degrees` | `0,5,0,8,0,7` | Six integers: min and max decimals for float, double and ordinate values, for degree coordinates | +| `formats..formatter.meters` | `0,5,0,8,0,3` | Same, for metre coordinates | +| `formats.json.forceLonLat` | `false` | Write coordinates lon,lat whatever the CRS axis order | +| `formats.html.dir` | - | Directory of Freemarker templates; they override the built-in ones, which remain the fallback. Must be an existing directory or the format is skipped | +| `formats.html.tileUrl` | OpenStreetMap | Background tile URL template with `{z}/{x}/{y}` | +| `formats.html.tileAttribution` | OpenStreetMap credit | HTML attribution. The OSM defaults apply only if none of the four tile keys is set | +| `formats.html.tileOriginX`, `formats.html.tileOriginY` | - | Origin of a non-WebMercator tile grid | +| `formats.html..tileUrl` etc. | - | Background map for requests in that SRID; the same four keys per SRID | +| `formats.gpkg.dir` | required | Writable directory for the generated GeoPackage files | + +```properties +formats=json,html,gpkg,csv +formats.html.3067.tileUrl=https://example.org/wmts/{z}/{y}/{x}.png +formats.html.3067.tileOriginX=-548576 +formats.html.3067.tileOriginY=8388608 +formats.gpkg.dir=/tmp/hakunapi +formats.json.formatter.degrees=0,5,0,8,0,7 +``` + +### Telemetry + +| Key | Default | Description | +| --- | --- | --- | +| `telemetry.mode` | `NOP` | `NOP` (off) or `log-json` (hakunapi-telemetry) | +| `telemetry.logger` | - | Logger name that receives the JSON lines. Effectively required: without it nothing is logged. An unknown `telemetry.mode` (case-sensitive) silently means `NOP` (only an info log) | +| `telemetry.collections` | - | `*` for all collections, or a list of ids. Unset means no telemetry. With a list, `telemetry.collections..name` sets the logged name of that collection (default the id); with `*` that key is ignored | +| `telemetry.fields` | - | Names under which request headers are logged | +| `telemetry.fields..header` | `` | The actual request header whose value is logged as `` | + +### JSON Schema files (titles, descriptions, code lists) + +| Key | Default | Description | +| --- | --- | --- | +| `schema` | - | Names of schema files; typically the language | +| `schema..path` | required | Path, relative to the config file (an entry without one is skipped with a warning) | +| `schema..lang` | `` | Language tag, lowercased | + +```properties +schema=en,fi +schema.en.path=addresses_schema_en.json +schema.fi.path=addresses_schema_fi.json +``` + +### Sources + +| Key | Default | Description | +| --- | --- | --- | +| `db.classes` | `fi.nls.hakunapi.simple.postgis.PostGISSimpleSource` | Source classes to load. Use `fi.nls.hakunapi.source.gpkg.GpkgSimpleSource` for GeoPackage, or both comma-separated. A class that cannot be loaded is logged and skipped | +| `db` | - | Names of databases configured inline, see [Databases](#databases) | +| `joins` | - | PostGIS only, experimental: names of table joins, see [Joins](#postgis-joins-experimental) | + +## Collections + +| Key | Default | Description | +| --- | --- | --- | +| `collections` | - | Comma-separated collection ids. With none the service starts collectionless (useful with API extensions) | +| `collections..title` | from the source, else `` | Title. A `title` in a `schema` file overrides it | +| `collections..description` | from the source, else the title | Description. A `description` in a `schema` file overrides it | +| `collections..type` | `default.collections.type`, else `pg` | Source type: `pg` or `gpkg`. Only consulted when more than one source is loaded | +| `collections..srid` | `default.collections.srid` | Comma-separated SRIDs the collection can be served in. Must all be in the global `srid`. In practice required for a geometry collection: the storage SRID must be in this list | +| `collections..proj` | `fi.nls.hakunapi.proj.gt.GeoToolsProjectionTransformerFactory` | `ProjectionTransformerFactory` class. A class that fails to load is logged and ignored | +| `collections..metadata` | - | Extra collection metadata: a JSON object, or the path to a readable JSON file. A relative path is resolved against the properties file itself as if it were a directory, so only an absolute path works in practice | +| `collections..extent.spatial.crs84` | - | `minX,minY,maxX,maxY` in CRS:84. The warning about a missing spatial extent is logged only when neither this nor `extent.spatial` is set. If both are set this wins. Exactly 4 values, otherwise startup fails | +| `collections..extent.spatial` | - | Same, 4 values or 6 (with Z). Must be within lon -180..180, lat -90..90. Needs a geometry | +| `collections..extent.temporal` | - | Start and end instants, UTC with a `Z` (`2020-01-01T00:00:00Z`). Needs `time` | +| `collections..time` | - | Property names used by the `datetime` parameter | +| `collections..time.exclusive` | - | Subset of `time` whose bounds are exclusive; the others are inclusive | +| `default.collections.type` | `pg` | Default for `collections..type` | +| `default.collections.srid` | - | Default for `collections..srid` | + +```properties +collections=buildings +collections.buildings.title=Buildings +collections.buildings.srid=3067,4258 +collections.buildings.extent.spatial.crs84=19.0,59.7,31.6,70.1 +collections.buildings.time=created +collections.buildings.extent.temporal=2020-01-01T00:00:00Z,2026-01-01T00:00:00Z +``` + +### Collection links + +Each link needs `href`, `rel`, `type` and `title`. `default.collections.links` apply to every collection and come before the collection's own. + +| Key | Description | +| --- | --- | +| `default.collections.links` | Link names added to every collection | +| `default.collections.links..href`, `.rel`, `.type`, `.title` | The link | +| `collections..links` | Link names for this collection | +| `collections..links..href`, `.rel`, `.type`, `.title` | The link | + +### Pagination + +| Key | Default | Description | +| --- | --- | --- | +| `collections..pagination.strategy` | `cursor` | `cursor`, `offset` or `hybrid` | +| `collections..pagination` | the id property | Properties to order by | +| `collections..pagination.order` | `ASC` for each | `ASC` or `DESC` per entry of `pagination` | +| `collections..pagination.unordered` | `false` | With no `pagination`, do not order at all | + +### Queryables and static filters + +`parameters` makes properties filterable with `?name=value`. The properties they refer to are also the collection's queryables: they are listed in `/collections//queryables`, and a CQL2 `filter` can only use these properties (any other fails with `Non queryable property`). + +| Key | Default | Description | +| --- | --- | --- | +| `collections..parameters` | - | Names of query parameters | +| `collections..parameters.

.prop` | `

` | Property it filters. `$id` is the id, `$geometry` (or `$geom`) the geometry | +| `collections..parameters.

.name` | the property name | Query parameter name | +| `collections..parameters.

.desc` | `Filter the collection by ` | OpenAPI description | +| `collections..parameters.

.impl` | `fi.nls.hakunapi.simple.servlet.operation.param.DefaultFilterMapper` | Class that turns the value into a filter, a `FilterMapper` or `CustomizableGetFeatureParam` | +| `collections..parameters.

.impl.arg` | - | Argument handed to the class | + +Ready-made classes (package `fi.nls.hakunapi.simple.servlet.operation.param`): + +| Class | `impl.arg` | +| --- | --- | +| `DefaultFilterMapper` | none; equality | +| `NotEqualToFilterMapper` | none | +| `LikeFilterMapper` | `caseInsensitive[,wild[,single[,escape]]]`; defaults `false`, `*`, `?`, `\` | +| `DatetimeRangeMapper`, `DateRangeMapper` | an integer bit mask (1 = start inclusive, 2 = end inclusive), or two flags from `GT`, `GTE`, `LT`, `LTE` (e.g. `GTE,LT`), applied in order on top of both-inclusive. No arg leaves the mask at 0: both bounds exclusive | +| `TimestampParam` | name of the end-time property (an unknown name means no end property); the filtered property must be `timestamptz` | +| `ArrayOperationParam` | a `FilterOp` name, default `ARRAY_OVERLAPS`, which is also the only accepted value; array properties only | +| `WKTGeometryOperationParam` | required: `INTERSECTS` (the only operation; no arg fails startup); geometry properties only | +| `BboxExtraPropertyParam` | none; geometry properties only | + +Static filters are always applied to the collection: + +| Key | Description | +| --- | --- | +| `collections..filters` | Filter names | +| `collections..filters..type` | required: `=`, `IN`, `!=`, `<>`, `NOT IN`, `>`, `<`, `>=`, `<=`, `BETWEEN`, `IS NULL`, `IS NOT NULL`, `AND`, `OR` | +| `collections..filters..prop` | required except for `AND`/`OR`: property name | +| `collections..filters..value` | Comma-separated value(s): one for `>`, `<`, `>=`, `<=`; exactly two for `BETWEEN`; several for `=`/`IN` (OR) and `!=`/`<>`/`NOT IN` (AND). For `AND`/`OR`, the names of other filters | + +```properties +collections.parcels.parameters=name,kind +collections.parcels.parameters.name.impl=fi.nls.hakunapi.simple.servlet.operation.param.LikeFilterMapper +collections.parcels.parameters.name.impl.arg=true +collections.parcels.filters=active +collections.parcels.filters.active.type=NOT IN +collections.parcels.filters.active.prop=status +collections.parcels.filters.active.value=deleted,draft +``` + +### Response cache + +| Key | Default | Description | +| --- | --- | --- | +| `collections..cache` | `false` | Enable the cache | +| `collections..cache.size` | `100` | Entries | +| `collections..cache.refresh` | `60000` | Milliseconds | +| `collections..cache.expire` | `120000` | Milliseconds; must be at least `refresh`. All three must be positive | + +## Properties and columns + +These keys are read by both sources unless noted. `

` is the property name as published. + +| Key | Default | Description | +| --- | --- | --- | +| `collections..db` | required | Database name, see [Databases](#databases) | +| `collections..table` | required for `pg`; `` for `gpkg` | Table | +| `collections..schema` | `public` | `pg` only: database schema | +| `collections..id.mapping` | required for `pg`; the primary key for `gpkg` | Id column. `pg` accepts several (comma-separated) for a composite id | +| `collections..id.table` | the table | `pg`: table of each id column | +| `collections..id.transformer` | required for a composite id | `pg`: `ValueTransformer` class that joins the parts | +| `collections..id.transformer.arg` | - | `pg`: argument for it | +| `collections..geometry.mapping` | - | `pg`: geometry column. `gpkg` discovers it | +| `collections..geometry.table` | the table | `pg` | +| `collections..srid.storage` | `0` | SRID of the stored geometry, used only when the database reports none (0), as for a view or an untyped column. If the database knows the SRID it wins, and a different value here only logs a warning. If neither knows it, startup fails (`srid is unknown`). For `pg` the result must also be in the collection's `srid`; `gpkg` reads the key too | +| `collections..properties` | all columns except the id and geometry columns | Published properties. Unset or `*` discovers the columns; `*,~col` leaves one out (`~col` is honoured only after a leading `*`). Discovery always skips the geometry column. `pg`: the id column is skipped only with a single `id.mapping`, a composite id is not excluded. `gpkg`: the primary key is always skipped | +| `collections..properties.

.mapping` | `

` | Column. A single-quoted value (`'text'`) is a constant instead of a column | +| `collections..properties.

.table` | the table | `pg` only | +| `collections..properties.

.enum` | - | Allowed values; `INT`, `LONG` and `STRING` properties only. Published as an enumeration | +| `collections..properties.

.transformer` | - | `ValueTransformer` class | +| `collections..properties.

.transformer.arg` | - | Argument for it | +| `collections..properties.

.unique` | `false` | Values are unique | +| `collections..properties.

.hidden` | `false` | Not written to the output | +| `collections..casei` | `lower` | `pg` only: `lower` or `upper` (any case), the function applied for case-insensitive comparisons. Any other value fails startup with a `NoSuchElementException` (`Optional.get()` on an empty match) | +| `collections..sourceproj` | `false` | `pg` only: reproject in the database instead of in Hakunapi when the requested SRID differs from the storage SRID | + +Transformers are in `fi.nls.hakunapi.core.transformer`: `NullIf`, `IfNull`, `PrefixTransformer`, `LongToString`, `StringManipulator`, `StringJoinerTransformer`, `JSONCodeListTransformer`, `TimestampToDateTransformer`, `TimestamptzToDateTransformer`, `FixedLengthStringToIntegerTransformer`, `TransformChain`. Their `transformer.arg` formats are class specific; read the class. + +```properties +collections.buildings.db=db +collections.buildings.table=building +collections.buildings.id.mapping=id +collections.buildings.geometry.mapping=geom +collections.buildings.properties=name,use,source +collections.buildings.properties.use.mapping=building_use +collections.buildings.properties.use.enum=residential,industrial +collections.buildings.properties.source.mapping='Cadastre' +``` + +### Databases + +`collections..db` names a database. What the name means depends on the source. + +**PostGIS (`pg`)** - the name is one of: + +- `java:comp/env/`: a JNDI `DataSource` from the container. +- listed in `db=a,b`: then every `db..*` key is a [HikariCP](https://github.com/brettwooldridge/HikariCP#gear-configuration-knobs-baby) setting, with the prefix removed. +- otherwise: `.properties` next to the main config file, holding HikariCP settings. `${NAME}` placeholders work in it. + +```properties +db=main +db.main.dataSourceClassName=org.postgresql.ds.PGSimpleDataSource +db.main.dataSource.serverName=${DB_HOST} +db.main.dataSource.databaseName=gis +db.main.dataSource.user=reader +db.main.dataSource.password=${DB_PASSWORD} +``` + +**GeoPackage (`gpkg`)** - the name is first tried as a file: an absolute path, or one relative to the config file's directory. The file is opened with a pool of up to 50 connections. If no such file exists the name is handled as for `pg` (`db=` list with `db..*`, else `.properties`), with `driverClassName` defaulting to the SQLite driver. + +### PostGIS joins (Experimental) + +Joins are only weakly supported and may change. + +| Key | Description | +| --- | --- | +| `joins` | Join names | +| `joins..type` | `oneToOne`, `oneToMany`, `manyToOne` or `manyToMany` (any case) | +| `joins..leftTable`, `.leftColumn`, `.rightTable`, `.rightColumn` | The joined columns | +| `joins..joinTable`, `.joinColumnLeft`, `.joinColumnRight` | `manyToMany` only: the link table | + +## Not read by the code + +Seen in example files or older docs, but nothing in `src/`, `src-community/` or `webapp-jakarta/` reads them (a test-only data source reads `writeNulls`): + +- `collections..writeNulls` +- `collections..geometry.storageSrid` (use `collections..srid.storage`) +- `telemetry.module.`: a constant exists, nothing reads it +- `formats=geojson` in [modules.md](modules.md): the type is `json` diff --git a/readme.md b/readme.md index 4b74f4cc..6b7d2f06 100644 --- a/readme.md +++ b/readme.md @@ -44,6 +44,10 @@ To use one, add it as an explicit dependency of your own webapp. See See [CONTRIBUTING.md](CONTRIBUTING.md) +## Configuration + +All configuration keys and their defaults: [docs/overview/configuration.md](docs/overview/configuration.md) + ## Example Check out [Finnish addresses](examples/finnish_addresses) 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 79bcc63f..a4c261b5 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 @@ -137,7 +137,7 @@ public List readServers() { String[] serverNames = serverList.split(","); List servers = new ArrayList<>(); for (String serverName : serverNames) { - String prefix = "servers." + serverName; + String prefix = "servers." + serverName.trim(); String url = cfg.getProperty(prefix + ".url"); while (url.endsWith("/")) { url = url.substring(0, url.length() - 1); diff --git a/src/hakunapi-core/src/test/java/fi/nls/hakunapi/core/config/HakunaConfigParserTest.java b/src/hakunapi-core/src/test/java/fi/nls/hakunapi/core/config/HakunaConfigParserTest.java index 7a7160c4..030281f0 100644 --- a/src/hakunapi-core/src/test/java/fi/nls/hakunapi/core/config/HakunaConfigParserTest.java +++ b/src/hakunapi-core/src/test/java/fi/nls/hakunapi/core/config/HakunaConfigParserTest.java @@ -22,6 +22,20 @@ public void testGetSRIDs() { assertArrayEquals(expecteds, actuals); } + @Test + public void testServersAreTrimmed() { + Properties cfg = new Properties(); + cfg.setProperty("servers", "dev, prod"); + cfg.setProperty("servers.dev.url", "https://dev.example.org/"); + cfg.setProperty("servers.prod.url", "https://example.org"); + + List servers = new HakunaConfigParser(cfg).readServers(); + + assertEquals(2, servers.size()); + assertEquals("https://dev.example.org", servers.get(0).getUrl()); + assertEquals("https://example.org", servers.get(1).getUrl()); + } + @Test public void testExternalizedProperties() { String property = "hakunapi.test"; diff --git a/src/hakunapi-source-postgis/src/main/java/fi/nls/hakunapi/simple/postgis/PostGISSimpleSource.java b/src/hakunapi-source-postgis/src/main/java/fi/nls/hakunapi/simple/postgis/PostGISSimpleSource.java index 47158aca..1d2dd402 100644 --- a/src/hakunapi-source-postgis/src/main/java/fi/nls/hakunapi/simple/postgis/PostGISSimpleSource.java +++ b/src/hakunapi-source-postgis/src/main/java/fi/nls/hakunapi/simple/postgis/PostGISSimpleSource.java @@ -198,30 +198,27 @@ public SimpleFeatureType parse(HakunaConfigParser cfg, Path path, String collect ft.setDbSchema(dbSchema); ft.setJoins(joins); - Map> columnsByTable = new HashMap<>(); + Map> columnsByTable = new HashMap<>(); String[] idTables = cfg.getMultiple(p + "id.table"); String[] idMappings = cfg.getMultiple(p + "id.mapping"); - String[] idAliases = cfg.getMultiple(p + "id.alias"); if (idMappings.length == 0) { throw new IllegalArgumentException("Missing required property " + (p + "id.mapping")); } for (int i = 0; i < idMappings.length; i++) { String table = idTables.length > i ? idTables[i] : primaryTable; String mapping = idMappings[i]; - String alias = idAliases.length > i ? idAliases[i] : null; if (!HakunaConfigParser.isStaticMapping(mapping)) { - columnsByTable.computeIfAbsent(table, __ -> new HashSet<>()).add(new ColumnAlias(mapping, alias)); + columnsByTable.computeIfAbsent(table, __ -> new HashSet<>()).add(mapping); } } String geometryTable = cfg.get(p + "geometry.table", primaryTable); String geometryMapping = cfg.get(p + "geometry.mapping"); - String geometryAlias = cfg.get(p + "geometry.alias"); if (geometryMapping != null) { geometryMapping = geometryMapping.replace("\"", ""); } if (geometryMapping != null) { - columnsByTable.computeIfAbsent(geometryTable, __ -> new HashSet<>()).add(new ColumnAlias(geometryMapping, geometryAlias)); + columnsByTable.computeIfAbsent(geometryTable, __ -> new HashSet<>()).add(geometryMapping); } String[] properties = cfg.getMultiple(p + "properties"); @@ -244,19 +241,17 @@ public SimpleFeatureType parse(HakunaConfigParser cfg, Path path, String collect for (String property : properties) { String table = cfg.get(p + "properties." + property + ".table", primaryTable); String mapping = cfg.get(p + "properties." + property + ".mapping", property); - String alias = cfg.get(p + "properties." + property + ".alias", null); mapping = mapping.replace("\"", ""); - ColumnAlias ca = new ColumnAlias(mapping, alias); if (!HakunaConfigParser.isStaticMapping(mapping)) { - columnsByTable.computeIfAbsent(table, __ -> new HashSet<>()).add(ca); + columnsByTable.computeIfAbsent(table, __ -> new HashSet<>()).add(mapping); } } Map>> propertyTypes = new HashMap<>(); Map> propertyNullability = new HashMap<>(); - for (Map.Entry> entry : columnsByTable.entrySet()) { + for (Map.Entry> entry : columnsByTable.entrySet()) { String table = entry.getKey(); - Set columns = entry.getValue(); + Set columns = entry.getValue(); String select = getSelectSchema(dbSchema, table, columns); Map> tablePropertyTypes = getPropertyTypes(ds, select); propertyTypes.put(table, tablePropertyTypes); @@ -269,9 +264,8 @@ public SimpleFeatureType parse(HakunaConfigParser cfg, Path path, String collect // Simple String table = idTables.length > 0 ? idTables[0] : primaryTable; String mapping = idMappings[0]; - String alias = idAliases.length > 0 ? idAliases[0] : mapping; - HakunaPropertyType idColumnType = propertyTypes.get(table).get(alias).get(0); - boolean nullable = propertyNullability.get(table).get(alias); + HakunaPropertyType idColumnType = propertyTypes.get(table).get(mapping).get(0); + boolean nullable = propertyNullability.get(table).get(mapping); HakunaPropertyWriter propWriter = HakunaPropertyWriters.getIdPropertyWriter(ft, ft.getName(), "id", idColumnType); switch (idColumnType) { case INT: @@ -295,12 +289,11 @@ public SimpleFeatureType parse(HakunaConfigParser cfg, Path path, String collect for (int i = 0; i < idMappings.length; i++) { String table = idTables.length > i ? idTables[i] : primaryTable; String mapping = idMappings[i]; - String alias = idAliases.length > 0 ? idAliases[0] : mapping; - boolean nullable = propertyNullability.get(table).get(alias); + boolean nullable = propertyNullability.get(table).get(mapping); if (HakunaConfigParser.isStaticMapping(mapping)) { parts.add(HakunaPropertyStatic.create("id", "table", mapping.substring(1, mapping.length() - 1))); } else { - List typeChain = propertyTypes.get(table).get(alias); + List typeChain = propertyTypes.get(table).get(mapping); boolean unique = false; boolean hidden = true; parts.add(getDynamicProperty(cfg, HakunaConfigParser.REF_ID_PROP, table, mapping, typeChain, nullable, unique, hidden)); @@ -335,7 +328,6 @@ public SimpleFeatureType parse(HakunaConfigParser cfg, Path path, String collect boolean unique = Boolean.parseBoolean(cfg.get(propertyPrefix + "unique", "false")); boolean hidden = Boolean.parseBoolean(cfg.get(propertyPrefix + "hidden", "false")); String table = cfg.get(p + "properties." + property + ".table", primaryTable); - String alias = cfg.get(p + "properties." + property + ".alias", mapping); if (HakunaConfigParser.isStaticMapping(mapping)) { String value = mapping.substring(1, mapping.length() - 1); @@ -344,7 +336,7 @@ public SimpleFeatureType parse(HakunaConfigParser cfg, Path path, String collect } Map> tableTypes = propertyTypes.get(table); - List type = tableTypes.get(alias); + List type = tableTypes.get(mapping); if (type == null) { throw new IllegalArgumentException( "Property " + property + " with mapping " + mapping + " can't find type from table " + table); @@ -637,59 +629,6 @@ private HakunaProperty getDynamicProperty(HakunaConfigParser cfg, String name, S return cfg.getDynamicProperty(name, table, column, typeChain, nullable, unique, hidden); } - - private final class ColumnAlias { - - private final String column; - private final String alias; - private final boolean escape; - - public ColumnAlias(String column, String alias) { - this(column, alias, true); - } - - public ColumnAlias(String column, String alias, boolean escape) { - this.column = column; - this.alias = alias; - this.escape = escape; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((alias == null) ? 0 : alias.hashCode()); - result = prime * result + ((column == null) ? 0 : column.hashCode()); - result = prime * result + (escape ? 1231 : 1237); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - ColumnAlias other = (ColumnAlias) obj; - if (alias == null) { - if (other.alias != null) - return false; - } else if (!alias.equals(other.alias)) - return false; - if (column == null) { - if (other.column != null) - return false; - } else if (!column.equals(other.column)) - return false; - if (escape != other.escape) - return false; - return true; - } - - } - private Join parseJoin(HakunaConfigParser cfg, String join) { String propertyPrefix = "joins." + join + "."; String type = cfg.get(propertyPrefix + "type"); @@ -715,20 +654,11 @@ private Join parseJoin(HakunaConfigParser cfg, String join) { } } - private String getSelectSchema(String schema, String table, Set columns) { + private String getSelectSchema(String schema, String table, Set columns) { StringBuilder sb = new StringBuilder(); sb.append("SELECT "); - for (ColumnAlias col : columns) { - if (col.escape) { - sb.append('"').append(col.column).append('"'); - } else { - sb.append(col.column); - } - if (col.alias != null) { - sb.append(" AS "); - sb.append('"').append(col.alias).append('"'); - } - sb.append(','); + for (String column : columns) { + sb.append('"').append(column).append('"').append(','); } sb.setLength(sb.length() - 1); sb.append(" FROM ");