diff --git a/pom.xml b/pom.xml index 727f2764..aa7b3740 100644 --- a/pom.xml +++ b/pom.xml @@ -1,110 +1,110 @@ - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 3.5.14 - - - au.org.aodn.ogcapi - ogcapi-java - 0.0.0 - ogcapi-java - pom + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.5.14 + + + au.org.aodn.ogcapi + ogcapi-java + 0.0.0 + ogcapi-java + pom - REST API that implements OGC API - - 17 - 3.0.0 - 1.5.5.Final - 1.18.30 - 0.2.0 - 3.0.69 - 29.6 - true - true - 2.29.52 - - - - mvnrepository.com - https://repo1.maven.org/maven2/ - - - repo.osgeo.org - https://repo.osgeo.org/repository/release/ - - - codeartifact - ${env.CODEARTIFACT_REPO_URL} - - - - common - coverages - features - maps - processes - tile - records - server + REST API that implements OGC API + + 17 + 3.0.0 + 1.5.5.Final + 1.18.30 + 0.2.0 + 3.0.69 + 29.6 + true + true + 2.29.52 + + + + mvnrepository.com + https://repo1.maven.org/maven2/ + + + repo.osgeo.org + https://repo.osgeo.org/repository/release/ + + + codeartifact + ${env.CODEARTIFACT_REPO_URL} + + + + common + coverages + features + maps + processes + tile + records + server - - - - org.springdoc - springdoc-openapi-starter-webmvc-ui - 2.8.17 - - - com.github.joschi.jackson - jackson-datatype-threetenbp - 2.6.4 - - - - co.elastic.clients - elasticsearch-java - 8.19.10 - - - org.mapstruct - mapstruct - ${org.mapstruct.version} - - - org.mapstruct - mapstruct-processor - ${org.mapstruct.version} - - - org.projectlombok - lombok - ${org.projectlombok.version} - - - org.projectlombok - lombok-mapstruct-binding - ${org.projectlombok.binding} - - - - org.geotools - gt-cql - ${org.geotools.version} - - - org.geotools - gt-geojson - ${org.geotools.version} - - - org.geotools - gt-referencing - ${org.geotools.version} - + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.8.17 + + + com.github.joschi.jackson + jackson-datatype-threetenbp + 2.6.4 + + + + co.elastic.clients + elasticsearch-java + 8.19.10 + + + org.mapstruct + mapstruct + ${org.mapstruct.version} + + + org.mapstruct + mapstruct-processor + ${org.mapstruct.version} + + + org.projectlombok + lombok + ${org.projectlombok.version} + + + org.projectlombok + lombok-mapstruct-binding + ${org.projectlombok.binding} + + + + org.geotools + gt-cql + ${org.geotools.version} + + + org.geotools + gt-geojson + ${org.geotools.version} + + + org.geotools + gt-referencing + ${org.geotools.version} + org.geotools gt-wfs-ng @@ -115,20 +115,20 @@ gt-xml ${org.geotools.version} - - - org.geotools - gt-epsg-hsql - 22.2 - - - org.openapitools - jackson-databind-nullable - 0.2.6 - + + + org.geotools + gt-epsg-hsql + 22.2 + + + org.openapitools + jackson-databind-nullable + 0.2.6 + org.testcontainers testcontainers-bom @@ -153,50 +153,50 @@ cache-api 1.1.1 - - org.mock-server - mockserver-netty - 5.15.0 - - - org.mock-server - mockserver-client-java - 5.15.0 - - - org.locationtech.spatial4j - spatial4j - 0.8 - - - software.amazon.awssdk - batch - ${aws.sdk.version} - - - software.amazon.awssdk - auth - ${aws.sdk.version} - - - software.amazon.awssdk - ses - ${aws.sdk.version} - - - - org.apache.logging.log4j - log4j-layout-template-json - 2.24.3 - - - au.org.aodn - stacmodel - 0.0.56 - - - - + + org.mock-server + mockserver-netty + 5.15.0 + + + org.mock-server + mockserver-client-java + 5.15.0 + + + org.locationtech.spatial4j + spatial4j + 0.8 + + + software.amazon.awssdk + batch + ${aws.sdk.version} + + + software.amazon.awssdk + auth + ${aws.sdk.version} + + + software.amazon.awssdk + ses + ${aws.sdk.version} + + + + org.apache.logging.log4j + log4j-layout-template-json + 2.24.3 + + + au.org.aodn + stacmodel + 0.0.59 + + + + codeartifact codeartifact diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/mapper/Converter.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/mapper/Converter.java index d5d5713d..680b83df 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/mapper/Converter.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/mapper/Converter.java @@ -1,15 +1,15 @@ package au.org.aodn.ogcapi.server.core.mapper; import au.org.aodn.ogcapi.features.model.*; -import au.org.aodn.ogcapi.server.core.model.CitationModel; import au.org.aodn.ogcapi.server.core.model.ExtendedCollection; import au.org.aodn.ogcapi.server.core.model.ExtendedLink; -import au.org.aodn.ogcapi.server.core.model.StacCollectionModel; +import au.org.aodn.stac.model.StacCollectionModel; import au.org.aodn.ogcapi.server.core.model.enumeration.CQLCrsType; import au.org.aodn.ogcapi.server.core.model.enumeration.CollectionProperty; import au.org.aodn.ogcapi.server.core.parser.stac.GeometryVisitor; import au.org.aodn.ogcapi.server.core.util.ConstructUtils; import au.org.aodn.ogcapi.server.core.util.GeometryUtils; +import au.org.aodn.stac.model.CitationModel; import lombok.Builder; import lombok.Getter; import lombok.Setter; @@ -20,7 +20,10 @@ import org.slf4j.LoggerFactory; import org.springframework.http.MediaType; +import java.time.Instant; import java.util.ArrayList; +import java.util.Date; +import java.util.List; import java.util.Map; import static au.org.aodn.ogcapi.server.core.util.GeometryUtils.createCentroid; @@ -42,6 +45,36 @@ class Param { T convert(F from, Filter param); + /** + * Parse temporal interval string to Date object, the string should be in ISO 8601 format, e.g. "2020-01-01T00:00:00Z/2020-12-31T23:59:59Z" + * Structure of the input: + * - Outer list = one or more temporal intervals. + * - Inner list = a [start, end] pair. STAC encodes an unbounded endpoint as null + * (e.g. [start, null] = "from start, ongoing"). + * + * @param intervalStrings outer list of [start, end] string pairs + * @return outer list of [start, end] pairs preserving the input's null structure, + * or null if intervalStrings is null + */ + private static List> parseTemporal(List> intervalStrings) { + if (intervalStrings == null) { + return null; + } + List> intervals = new ArrayList<>(intervalStrings.size()); + for (List endpoints : intervalStrings) { + if (endpoints == null) { + intervals.add(null); + continue; + } + List parsedEndpoints = new ArrayList<>(endpoints.size()); + for (String timestamp : endpoints) { + parsedEndpoints.add(timestamp == null ? null : Date.from(Instant.parse(timestamp))); + } + intervals.add(parsedEndpoints); + } + return intervals; + } + default au.org.aodn.ogcapi.features.model.Link getSelfCollectionLink(String hostname, String id) { au.org.aodn.ogcapi.features.model.Link self = new au.org.aodn.ogcapi.features.model.Link(); @@ -100,7 +133,7 @@ default Collection getCollection(D m, Filter fil } extent.setTemporal(new ExtentTemporal()); - extent.getTemporal().interval(m.getExtent().getTemporal()); + extent.getTemporal().interval(parseTemporal(m.getExtent().getTemporal())); collection.setExtent(extent); } @@ -162,16 +195,14 @@ default Collection getCollection(D m, Filter fil // filter have values if user CQL contains BBox, hence our centroid point needs to be // the noland geometry intersect with BBox and centroid point will be within the BBox Geometry g = null; - if(filter != null) { + if (filter != null) { Object geo = filter.accept(visitor, input); if (geo instanceof PreparedGeometry) { g = ((PreparedGeometry) geo).getGeometry(); - } - else if (geo != null) { + } else if (geo != null) { g = (Geometry) geo; } - } - else { + } else { g = input.getGeometry(); } @@ -228,7 +259,7 @@ else if (geo != null) { collection.getProperties().put(CollectionProperty.aiUpdateFrequency, m.getSummaries().getAiUpdateFrequency()); } - if(m.getSummaries().getScope() != null) { + if (m.getSummaries().getScope() != null) { collection.getProperties().put(CollectionProperty.scope, m.getSummaries().getScope()); } @@ -239,6 +270,22 @@ else if (geo != null) { if (m.getSummaries().getAiParameterVocabs() != null && !m.getSummaries().getAiParameterVocabs().isEmpty()) { collection.getProperties().put(CollectionProperty.aiParameterVocabs, m.getSummaries().getAiParameterVocabs()); } + + if (m.getSummaries().getPlatformVocabs() != null && !m.getSummaries().getPlatformVocabs().isEmpty()) { + collection.getProperties().put(CollectionProperty.platformVocabs, m.getSummaries().getPlatformVocabs()); + } + + if (m.getSummaries().getOrganisationVocabs() != null && !m.getSummaries().getOrganisationVocabs().isEmpty()) { + collection.getProperties().put(CollectionProperty.organisationVocabs, m.getSummaries().getOrganisationVocabs()); + } + + if (m.getSummaries().getAiPlatformVocabs() != null && !m.getSummaries().getAiPlatformVocabs().isEmpty()) { + collection.getProperties().put(CollectionProperty.aiPlatformVocabs, m.getSummaries().getAiPlatformVocabs()); + } + + if (m.getSummaries().getDatasetProvider() != null) { + collection.getProperties().put(CollectionProperty.datasetProvider, m.getSummaries().getDatasetProvider()); + } } return collection; diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/mapper/StacToCollection.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/mapper/StacToCollection.java index e14689ee..09214172 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/mapper/StacToCollection.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/mapper/StacToCollection.java @@ -1,7 +1,7 @@ package au.org.aodn.ogcapi.server.core.mapper; import au.org.aodn.ogcapi.features.model.Collection; -import au.org.aodn.ogcapi.server.core.model.StacCollectionModel; +import au.org.aodn.stac.model.StacCollectionModel; import org.mapstruct.Mapper; import org.opengis.filter.Filter; import org.springframework.beans.factory.annotation.Value; diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/mapper/StacToCollections.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/mapper/StacToCollections.java index cb0f2b77..435b7800 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/mapper/StacToCollections.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/mapper/StacToCollections.java @@ -3,7 +3,7 @@ import au.org.aodn.ogcapi.features.model.Collection; import au.org.aodn.ogcapi.features.model.Collections; import au.org.aodn.ogcapi.server.core.model.ExtendedCollections; -import au.org.aodn.ogcapi.server.core.model.StacCollectionModel; +import au.org.aodn.stac.model.StacCollectionModel; import au.org.aodn.ogcapi.server.core.service.ElasticSearch; import org.mapstruct.Mapper; import org.opengis.filter.Filter; diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/mapper/StacToFeatureCollection.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/mapper/StacToFeatureCollection.java index ad204b1c..378c4e38 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/mapper/StacToFeatureCollection.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/mapper/StacToFeatureCollection.java @@ -3,7 +3,7 @@ import au.org.aodn.ogcapi.features.model.FeatureCollectionGeoJSON; import au.org.aodn.ogcapi.features.model.FeatureGeoJSON; import au.org.aodn.ogcapi.features.model.PointGeoJSON; -import au.org.aodn.ogcapi.server.core.model.StacItemModel; +import au.org.aodn.stac.model.StacItemModel; import au.org.aodn.ogcapi.server.core.service.ElasticSearch; import org.mapstruct.Mapper; import org.openapitools.jackson.nullable.JsonNullable; diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/mapper/StacToInlineResponse2002.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/mapper/StacToInlineResponse2002.java index af55aefc..a3f1684e 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/mapper/StacToInlineResponse2002.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/mapper/StacToInlineResponse2002.java @@ -1,6 +1,6 @@ package au.org.aodn.ogcapi.server.core.mapper; -import au.org.aodn.ogcapi.server.core.model.StacCollectionModel; +import au.org.aodn.stac.model.StacCollectionModel; import au.org.aodn.ogcapi.server.core.model.enumeration.CQLCrsType; import au.org.aodn.ogcapi.server.core.service.ElasticSearch; import au.org.aodn.ogcapi.tile.model.InlineResponse2002; diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/mapper/StacToTileSetWmWGS84Q.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/mapper/StacToTileSetWmWGS84Q.java index deeaddcb..0df6ce0f 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/mapper/StacToTileSetWmWGS84Q.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/mapper/StacToTileSetWmWGS84Q.java @@ -1,6 +1,6 @@ package au.org.aodn.ogcapi.server.core.mapper; -import au.org.aodn.ogcapi.server.core.model.StacCollectionModel; +import au.org.aodn.stac.model.StacCollectionModel; import au.org.aodn.ogcapi.server.core.model.enumeration.CQLCrsType; import au.org.aodn.ogcapi.server.core.service.ElasticSearch; import au.org.aodn.ogcapi.tile.model.TileSet; diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/AddressModel.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/AddressModel.java deleted file mode 100644 index b8cdb4d1..00000000 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/AddressModel.java +++ /dev/null @@ -1,30 +0,0 @@ -package au.org.aodn.ogcapi.server.core.model; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.List; - -//https://github.com/stac-extensions/contacts?tab=readme-ov-file#address-object -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@JsonInclude(JsonInclude.Include.NON_NULL) -public class AddressModel { - - @JsonProperty("delivery_point") - protected List deliveryPoint; - protected String city; - - @JsonProperty("administrative_area") - protected String administrativeArea; - - @JsonProperty("postal_code") - protected String postalCode; - protected String country; -} diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/AssetModel.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/AssetModel.java deleted file mode 100644 index cb99a03f..00000000 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/AssetModel.java +++ /dev/null @@ -1,53 +0,0 @@ -package au.org.aodn.ogcapi.server.core.model; - -import com.fasterxml.jackson.annotation.JsonInclude; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@JsonInclude(JsonInclude.Include.NON_NULL) -public class AssetModel { - // https://github.com/radiantearth/stac-spec/blob/master/best-practices.md#list-of-asset-roles - public enum Role { - DATA("data"), - METADATA("metadata"), - THUMBNAIL("thumbnail"), - SUMMARY("summary"), - OVERVIEW("overview"), - VISUAL("visual"), - DATE("date"), - GRAPHIC("graphic"), - DATA_MASK("data-mask"), - SNOW_ICE("snow-ice"), - LAND_WATER("land-water"), - WATER_MASK("water-mask"), - ISO_19115("iso-19115"); - - private final String role; - - Role(String role) { - this.role = role; - } - - @Override - public String toString() { - return role; - } - } - /** - * REQUIRED. URI to the asset object. Relative and absolute URI are both allowed. Trailing slashes are significant. - */ - protected String href; - protected String title; - protected String description; - protected String type; - /** - * The semantic roles of the asset, similar to the use of rel in links. - */ - protected Role role; -} diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/CitationModel.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/CitationModel.java deleted file mode 100644 index 42a85128..00000000 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/CitationModel.java +++ /dev/null @@ -1,16 +0,0 @@ -package au.org.aodn.ogcapi.server.core.model; - -import lombok.Builder; -import lombok.Data; - -import java.util.List; - -@Data -@Builder -public class CitationModel { - - protected String suggestedCitation; - protected List useLimitations; - protected List otherConstraints; - -} diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ConceptModel.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ConceptModel.java deleted file mode 100644 index 5706898f..00000000 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ConceptModel.java +++ /dev/null @@ -1,23 +0,0 @@ -package au.org.aodn.ogcapi.server.core.model; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class ConceptModel { - protected String id; - protected String url; - protected String description; - protected String title; - - @JsonInclude(JsonInclude.Include.NON_NULL) - @JsonProperty("ai:description") - protected String aiDescription; -} diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ContactModel.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ContactModel.java deleted file mode 100644 index ce1c3718..00000000 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ContactModel.java +++ /dev/null @@ -1,30 +0,0 @@ -package au.org.aodn.ogcapi.server.core.model; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.List; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class ContactModel { - - // Not include all fields according to this: https://github.com/stac-extensions/contacts - // The types of some fields are also not aligned with the spec. - // Currently only include the fields that are in use. - // May need to add more fields / modify some fields in the future. - protected String name; - protected String organization; - protected String identifier; - protected String position; - protected List emails; - protected List addresses; - protected List links; - protected List roles; - protected List phones; - -} diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/DataSearchResult.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/DataSearchResult.java index 7bd9427c..d43f1941 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/DataSearchResult.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/DataSearchResult.java @@ -4,6 +4,7 @@ import au.org.aodn.ogcapi.features.model.FeatureGeoJSON; import au.org.aodn.ogcapi.features.model.PointGeoJSON; import au.org.aodn.ogcapi.server.core.model.enumeration.FeatureProperty; +import au.org.aodn.stac.model.StacItemModel; import au.org.aodn.ogcapi.server.core.util.DatasetSummarizer; import org.openapitools.jackson.nullable.JsonNullable; diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ExtentModel.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ExtentModel.java deleted file mode 100644 index 52d04289..00000000 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ExtentModel.java +++ /dev/null @@ -1,23 +0,0 @@ -package au.org.aodn.ogcapi.server.core.model; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Builder; -import lombok.Data; - -import java.math.BigDecimal; -import java.util.Date; -import java.util.List; - -@Data -@Builder -public class ExtentModel { - protected List> bbox; - protected List> temporal; - - @JsonCreator - public ExtentModel(@JsonProperty("bbox") List> bbox, @JsonProperty("temporal") List> temporal) { - this.temporal = temporal; - this.bbox = bbox; - } -} diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/InfoModel.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/InfoModel.java deleted file mode 100644 index 919d87eb..00000000 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/InfoModel.java +++ /dev/null @@ -1,20 +0,0 @@ -package au.org.aodn.ogcapi.server.core.model; - -import com.fasterxml.jackson.annotation.JsonInclude; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.List; - -//https://github.com/stac-extensions/contacts?tab=readme-ov-file#info-object -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@JsonInclude(JsonInclude.Include.NON_NULL) -public class InfoModel { - protected String value; - protected List roles; -} diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/LicenseModel.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/LicenseModel.java deleted file mode 100644 index 91182521..00000000 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/LicenseModel.java +++ /dev/null @@ -1,14 +0,0 @@ -package au.org.aodn.ogcapi.server.core.model; - -import lombok.Builder; -import lombok.Data; - -@Data -@Builder -public class LicenseModel { - - protected String title; - protected String url; - protected String licenseGraphic; - -} diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/LinkModel.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/LinkModel.java deleted file mode 100644 index 17533bdf..00000000 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/LinkModel.java +++ /dev/null @@ -1,39 +0,0 @@ -package au.org.aodn.ogcapi.server.core.model; - -import au.org.aodn.ogcapi.server.core.util.LinkUtils; -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.List; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class LinkModel { - protected String rel; - protected String href; - protected String type; - protected String title; - - @JsonProperty("ai:group") - protected String aiGroup; - - @JsonProperty("ai:role") - protected List aiRole; - - @JsonProperty("description") - protected String description; - - public void setTitle(String title) { - String[] parsed = LinkUtils.parseLinkTitleDescription(title); - this.title = parsed[0]; - // set description if the link has successfully parsed description - if (this.description == null && parsed[1] != null) { - this.description = parsed[1]; - } - } -} diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/SearchSuggestionsModel.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/SearchSuggestionsModel.java deleted file mode 100644 index e8e44556..00000000 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/SearchSuggestionsModel.java +++ /dev/null @@ -1,37 +0,0 @@ -package au.org.aodn.ogcapi.server.core.model; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.List; -import java.util.Map; - -@Data -@Builder -@AllArgsConstructor -@NoArgsConstructor -@JsonIgnoreProperties(ignoreUnknown = true) -public class SearchSuggestionsModel { - private Map> searchSuggestions; - - @JsonProperty("abstract_phrases") - public List getAbstractPhrases() { return searchSuggestions.get("abstract_phrases"); } - - @JsonProperty("parameter_vocabs_sayt") - public List getParameterVocabs() { return searchSuggestions.get("parameter_vocabs_sayt"); } - - @JsonProperty("platform_vocabs_sayt") - public List getPlatformVocabs() { return searchSuggestions.get("platform_vocabs_sayt"); } - - @JsonProperty("organisation_vocabs_sayt") - public List getOrganisationVocabs() { return searchSuggestions.get("organisation_vocabs_sayt"); } - - @JsonProperty("search_suggestions") - private void setSearchSuggestions(Map> searchSuggestions) { - this.searchSuggestions = searchSuggestions; - } -} diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/StacCollectionModel.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/StacCollectionModel.java deleted file mode 100644 index f69fb996..00000000 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/StacCollectionModel.java +++ /dev/null @@ -1,43 +0,0 @@ -package au.org.aodn.ogcapi.server.core.model; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.*; - -import java.util.List; -import java.util.Map; - -/** - * This is used to map the json from Elastic search to object - */ -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class StacCollectionModel { - - protected String description; - protected String type; - protected ExtentModel extent; - protected SummariesModel summaries; - protected List links; - protected List contacts; - protected List themes; - protected String license; - protected Map assets; - - @JsonProperty("sci:citation") - protected String citation; - - @Getter - protected String title; - - @JsonProperty("id") - protected String uuid; - - @JsonProperty("stac_version") - protected String stacVersion; - - @JsonProperty("stac_extensions") - protected List stacExtensions; - -} diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/StacItemModel.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/StacItemModel.java deleted file mode 100644 index ebc19375..00000000 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/StacItemModel.java +++ /dev/null @@ -1,76 +0,0 @@ -package au.org.aodn.ogcapi.server.core.model; - -import au.org.aodn.ogcapi.features.model.FeatureCollectionGeoJSON; -import au.org.aodn.ogcapi.features.model.FeatureGeoJSON; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.math.BigDecimal; -import java.util.List; -import java.util.Map; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@JsonInclude(JsonInclude.Include.NON_NULL) -public class StacItemModel { - - @JsonProperty("type") - protected String type; - /** - * REQUIRED. Provider identifier. The ID should be unique within the Collection that contains the Item. - */ - @JsonProperty("id") - protected String uuid; - /** - * REQUIRED. Defines the full footprint of the asset represented by this item, formatted according to RFC 7946, - * section 3.1 if a geometry is provided or section 3.2 if no geometry is provided. - * Use to generate the vector tile, the STAC format is not optimized and hard to work with for Elastic search - */ - @JsonProperty("geometry") - protected Map geometry; - /** - * REQUIRED if geometry is not null, prohibited if geometry is null. Bounding Box of the asset represented by - * this Item, formatted according to RFC 7946, section 5. - */ - @JsonProperty("bbox") - protected List> bbox; - /** - * REQUIRED. A dictionary of additional metadata for the Item. - */ - @JsonProperty("properties") - protected Map properties; - /** - * REQUIRED. List of link objects to resources and related URLs. See the best practices for details on when the - * use self links is strongly recommended. - */ - protected List links; - - protected Map assets; - /** - * The id of the STAC Collection this Item references to. This field is required if a link with a collection relation type is present and is not allowed otherwise. - */ - @JsonProperty("collection") - protected String collection; - - @JsonProperty("stac_version") - protected String stacVersion; - - @JsonProperty("stac_extensions") - public String[] getStacExtension() { - return new String[] { - "https://stac-extensions.github.io/scientific/v1.0.0/schema.json", - "https://stac-extensions.github.io/contacts/v0.1.1/schema.json", - "https://stac-extensions.github.io/projection/v1.1.0/schema.json", - "https://stac-extensions.github.io/language/v1.0.0/schema.json", - "https://stac-extensions.github.io/themes/v1.0.0/schema.json", - "https://stac-extensions.github.io/web-map-links/v1.2.0/schema.json" - }; - } - -} diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/SummariesModel.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/SummariesModel.java deleted file mode 100644 index ca593054..00000000 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/SummariesModel.java +++ /dev/null @@ -1,57 +0,0 @@ -package au.org.aodn.ogcapi.server.core.model; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.List; -import java.util.Map; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@JsonInclude(JsonInclude.Include.NON_NULL) -public class SummariesModel { - // Do not create constructor, it will create by lombook, you can use builder() to create object. - protected int score; - protected String status; - protected List credits; - protected String creation; - protected String revision; - - @JsonProperty("proj:geometry") - protected Map geometry; - - @JsonProperty("proj:geometry_noland") - protected Map geometryNoLand; - - @JsonProperty("temporal") - protected List> temporal; - - @JsonProperty("update_frequency") - protected String updateFrequency; - - @JsonProperty("dataset_group") - protected List datasetGroup; - - @JsonProperty("ai:description") - protected String aiDescription; - - @JsonProperty("ai:update_frequency") - protected String aiUpdateFrequency; - - @JsonProperty("scope") - protected Map scope; - - protected String statement; - - @JsonProperty("parameter_vocabs") - protected List parameterVocabs; - - @JsonProperty("ai:parameter_vocabs") - protected List aiParameterVocabs; -} diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ThemeModel.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ThemeModel.java deleted file mode 100644 index 9b22ff1e..00000000 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/ThemeModel.java +++ /dev/null @@ -1,23 +0,0 @@ -package au.org.aodn.ogcapi.server.core.model; - -import au.org.aodn.stac.model.Citation; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; -import java.util.List; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class ThemeModel { - protected String scheme; - protected List concepts; - - // This is just a test to make sure the stac model can be used in this module, we can remove it when all the stac models are using the stacmodel from es-indexer - private void testStacmodelReference() { - Citation citation = Citation.builder().build(); - } - -} diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/enumeration/CollectionProperty.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/enumeration/CollectionProperty.java index 3db1dc10..b7d59487 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/model/enumeration/CollectionProperty.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/model/enumeration/CollectionProperty.java @@ -26,7 +26,11 @@ public enum CollectionProperty { aiUpdateFrequency("ai:update_frequency"), scope("scope"), parameterVocabs("parameter_vocabs"), - aiParameterVocabs("ai:parameter_vocabs"); + aiParameterVocabs("ai:parameter_vocabs"), + platformVocabs("platform_vocabs"), + organisationVocabs("organisation_vocabs"), + aiPlatformVocabs("ai:platform_vocabs"), + datasetProvider("dataset_provider"); private final String value; diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/CacheNoLandGeometry.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/CacheNoLandGeometry.java index c032c0ac..ecb9e127 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/CacheNoLandGeometry.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/CacheNoLandGeometry.java @@ -1,6 +1,6 @@ package au.org.aodn.ogcapi.server.core.service; -import au.org.aodn.ogcapi.server.core.model.StacCollectionModel; +import au.org.aodn.stac.model.StacCollectionModel; import au.org.aodn.ogcapi.server.core.model.enumeration.CQLFields; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/CacheWarm.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/CacheWarm.java index 216dda46..4e052ffa 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/CacheWarm.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/CacheWarm.java @@ -1,6 +1,6 @@ package au.org.aodn.ogcapi.server.core.service; -import au.org.aodn.ogcapi.server.core.model.StacCollectionModel; +import au.org.aodn.stac.model.StacCollectionModel; import au.org.aodn.ogcapi.server.core.service.geoserver.wfs.WfsServer; import au.org.aodn.ogcapi.server.core.service.geoserver.wms.WmsServer; import au.org.aodn.ogcapi.server.core.util.GeometryUtils; diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/ElasticSearch.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/ElasticSearch.java index a484ef3a..b3d0e3ee 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/ElasticSearch.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/ElasticSearch.java @@ -2,8 +2,8 @@ import au.org.aodn.ogcapi.features.model.FeatureGeoJSON; import au.org.aodn.ogcapi.server.core.model.EsFeatureCollectionModel; -import au.org.aodn.ogcapi.server.core.model.StacCollectionModel; -import au.org.aodn.ogcapi.server.core.model.SearchSuggestionsModel; +import au.org.aodn.stac.model.SearchSuggestionsModel; +import au.org.aodn.stac.model.StacCollectionModel; import au.org.aodn.ogcapi.server.core.model.enumeration.*; import au.org.aodn.ogcapi.server.core.parser.elastic.CQLToElasticFilterFactory; import au.org.aodn.ogcapi.server.core.parser.elastic.QueryHandler; diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/ElasticSearchBase.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/ElasticSearchBase.java index 6bbc6850..cdc11561 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/ElasticSearchBase.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/ElasticSearchBase.java @@ -1,10 +1,11 @@ package au.org.aodn.ogcapi.server.core.service; -import au.org.aodn.ogcapi.server.core.model.StacCollectionModel; +import au.org.aodn.stac.model.StacCollectionModel; import au.org.aodn.ogcapi.server.core.model.enumeration.CQLFields; import au.org.aodn.ogcapi.server.core.model.enumeration.CQLFieldsInterface; import au.org.aodn.ogcapi.server.core.model.enumeration.StacBasicField; import au.org.aodn.ogcapi.server.core.model.enumeration.StacSummeries; +import au.org.aodn.ogcapi.server.core.util.LinkUtils; import co.elastic.clients.elasticsearch.ElasticsearchClient; import co.elastic.clients.elasticsearch._types.*; import co.elastic.clients.elasticsearch._types.aggregations.*; @@ -562,7 +563,9 @@ protected StacCollectionModel formatResult(ObjectNode nodes) { try { if(nodes != null) { String json = nodes.toString(); - return mapper.readValue(json, StacCollectionModel.class); + StacCollectionModel result = mapper.readValue(json, StacCollectionModel.class); + fixupPackedLinkTitles(result); + return result; } else { log.error("Failed to serialize text to StacCollectionModel"); @@ -574,4 +577,20 @@ protected StacCollectionModel formatResult(ObjectNode nodes) { return null; } } + + private static void fixupPackedLinkTitles(StacCollectionModel collection) { + if (collection == null) { + return; + } + if (collection.getLinks() != null) { + collection.getLinks().forEach(LinkUtils::applyParsedTitle); + } + if (collection.getContacts() != null) { + collection.getContacts().forEach(contact -> { + if (contact != null && contact.getLinks() != null) { + contact.getLinks().forEach(LinkUtils::applyParsedTitle); + } + }); + } + } } diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/OGCApiService.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/OGCApiService.java index 618a694d..3d88c439 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/OGCApiService.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/OGCApiService.java @@ -2,7 +2,7 @@ import au.org.aodn.ogcapi.features.model.FeatureCollectionGeoJSON; import au.org.aodn.ogcapi.server.core.exception.CustomException; -import au.org.aodn.ogcapi.server.core.model.StacCollectionModel; +import au.org.aodn.stac.model.StacCollectionModel; import au.org.aodn.ogcapi.server.core.model.enumeration.CQLCrsType; import au.org.aodn.ogcapi.server.core.model.enumeration.FeatureId; import au.org.aodn.ogcapi.server.core.model.enumeration.OGCMediaTypeMapper; diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/Search.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/Search.java index 9420d2ad..e9f07379 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/Search.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/Search.java @@ -1,7 +1,7 @@ package au.org.aodn.ogcapi.server.core.service; import au.org.aodn.ogcapi.features.model.FeatureGeoJSON; -import au.org.aodn.ogcapi.server.core.model.StacCollectionModel; +import au.org.aodn.stac.model.StacCollectionModel; import au.org.aodn.ogcapi.server.core.model.enumeration.CQLCrsType; import au.org.aodn.ogcapi.server.core.model.ogc.FeatureRequest; import co.elastic.clients.transport.endpoints.BinaryResponse; diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/geoserver/wfs/WfsServer.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/geoserver/wfs/WfsServer.java index 841c9dce..b735ed3e 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/geoserver/wfs/WfsServer.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/geoserver/wfs/WfsServer.java @@ -2,8 +2,8 @@ import au.org.aodn.ogcapi.server.core.exception.GeoserverFieldsNotFoundException; import au.org.aodn.ogcapi.server.core.exception.GeoserverLayersNotFoundException; -import au.org.aodn.ogcapi.server.core.model.LinkModel; -import au.org.aodn.ogcapi.server.core.model.StacCollectionModel; +import au.org.aodn.stac.model.StacCollectionModel; +import au.org.aodn.stac.model.LinkModel; import au.org.aodn.ogcapi.server.core.model.ogc.FeatureRequest; import au.org.aodn.ogcapi.server.core.model.ogc.wfs.*; import au.org.aodn.ogcapi.server.core.service.ElasticSearchBase; @@ -82,6 +82,7 @@ public WfsServer(Search search, this.pretendUserEntity = entity; this.wfsDefaultParam = wfsDefaultParam; } + /** * Build CQL filter for temporal and spatial constraints */ @@ -145,6 +146,7 @@ public String buildCqlFilter(String uuid, WfsFeatureRequest featureRequest) { return cqlFilter.toString(); } + /** * Build WFS GetFeature URL */ @@ -164,17 +166,16 @@ protected String createWfsRequestUrl(String wfsUrl, String layerName, List 0) { + if (maxRecordNum > 0) { param.put("maxFeatures", String.valueOf(maxRecordNum)); } diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/geoserver/wms/WmsServer.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/geoserver/wms/WmsServer.java index e2aea30a..286374d0 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/service/geoserver/wms/WmsServer.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/service/geoserver/wms/WmsServer.java @@ -2,8 +2,8 @@ import au.org.aodn.ogcapi.server.core.exception.GeoserverFieldsNotFoundException; import au.org.aodn.ogcapi.server.core.exception.GeoserverLayersNotFoundException; -import au.org.aodn.ogcapi.server.core.model.LinkModel; -import au.org.aodn.ogcapi.server.core.model.StacCollectionModel; +import au.org.aodn.stac.model.StacCollectionModel; +import au.org.aodn.stac.model.LinkModel; import au.org.aodn.ogcapi.server.core.model.ogc.FeatureRequest; import au.org.aodn.ogcapi.server.core.model.ogc.wfs.WfsField; import au.org.aodn.ogcapi.server.core.model.ogc.wfs.WfsFields; diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/util/GeoserverUtils.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/util/GeoserverUtils.java index 2c026aac..30cac27e 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/util/GeoserverUtils.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/util/GeoserverUtils.java @@ -1,6 +1,6 @@ package au.org.aodn.ogcapi.server.core.util; -import au.org.aodn.ogcapi.server.core.model.LinkModel; +import au.org.aodn.stac.model.LinkModel; import lombok.extern.slf4j.Slf4j; import org.springframework.web.util.UriComponentsBuilder; import org.springframework.web.util.UriUtils; diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/core/util/LinkUtils.java b/server/src/main/java/au/org/aodn/ogcapi/server/core/util/LinkUtils.java index 30a8d0f1..37f6adb0 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/core/util/LinkUtils.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/core/util/LinkUtils.java @@ -1,10 +1,28 @@ package au.org.aodn.ogcapi.server.core.util; +import au.org.aodn.stac.model.LinkModel; + import static au.org.aodn.ogcapi.server.core.util.ConstructUtils.constructByJsonString; public class LinkUtils { record TitleWithDescription(String title, String description) {} + /** + * The shared {@link LinkModel} from the {@code stacmodel} artifact ships with a plain Lombok + * {@code setTitle}. Some indexed documents pack {"title": ..., "description": ...} into the + * single title field, so call this after deserialization to split them back out. + */ + public static void applyParsedTitle(LinkModel link) { + if (link == null) { + return; + } + String[] parsed = parseLinkTitleDescription(link.getTitle()); + link.setTitle(parsed[0]); + if (link.getDescription() == null && parsed[1] != null) { + link.setDescription(parsed[1]); + } + } + public static String[] parseLinkTitleDescription(String combinedTitle) { // if combinedTitle is null or empty, return null for both title and description if (combinedTitle == null || combinedTitle.trim().isEmpty()) { diff --git a/server/src/main/java/au/org/aodn/ogcapi/server/features/RestServices.java b/server/src/main/java/au/org/aodn/ogcapi/server/features/RestServices.java index c1aa04a1..6ca11c74 100644 --- a/server/src/main/java/au/org/aodn/ogcapi/server/features/RestServices.java +++ b/server/src/main/java/au/org/aodn/ogcapi/server/features/RestServices.java @@ -8,7 +8,7 @@ import au.org.aodn.ogcapi.server.core.model.ogc.wms.LayerInfo; import au.org.aodn.ogcapi.server.core.service.DasService; import au.org.aodn.ogcapi.server.core.mapper.StacToCollection; -import au.org.aodn.ogcapi.server.core.model.StacCollectionModel; +import au.org.aodn.stac.model.StacCollectionModel; import au.org.aodn.ogcapi.server.core.model.ogc.wfs.WfsFields; import au.org.aodn.ogcapi.server.core.service.ElasticSearch; import au.org.aodn.ogcapi.server.core.service.OGCApiService; diff --git a/server/src/test/java/au/org/aodn/ogcapi/server/BaseTestClass.java b/server/src/test/java/au/org/aodn/ogcapi/server/BaseTestClass.java index d4d7c07e..b7b0ea60 100644 --- a/server/src/test/java/au/org/aodn/ogcapi/server/BaseTestClass.java +++ b/server/src/test/java/au/org/aodn/ogcapi/server/BaseTestClass.java @@ -113,10 +113,12 @@ protected void clearElasticIndex() { protected void createElasticIndex() { schemas.forEach(schema -> { - try { - // TODO: This file should come from indexer jar when CodeArtifact in place - File f = ResourceUtils.getFile(String.format("classpath:%s", schema.get("mapping"))); - try (Reader reader = new FileReader(f)) { + String resourcePath = "/schema/" + schema.get("mapping"); + try (InputStream stream = getClass().getResourceAsStream(resourcePath)) { + if (stream == null) { + throw new FileNotFoundException("Schema not found on classpath: " + resourcePath); + } + try (Reader reader = new InputStreamReader(stream, StandardCharsets.UTF_8)) { CreateIndexRequest req = CreateIndexRequest.of(b -> b .index(schema.get("name")) .withJson(reader) diff --git a/server/src/test/java/au/org/aodn/ogcapi/server/core/mapper/StacToCollectionTest.java b/server/src/test/java/au/org/aodn/ogcapi/server/core/mapper/StacToCollectionTest.java index 5f258421..4038a791 100644 --- a/server/src/test/java/au/org/aodn/ogcapi/server/core/mapper/StacToCollectionTest.java +++ b/server/src/test/java/au/org/aodn/ogcapi/server/core/mapper/StacToCollectionTest.java @@ -4,6 +4,17 @@ import au.org.aodn.ogcapi.server.core.configuration.TestConfig; import au.org.aodn.ogcapi.server.core.model.*; import au.org.aodn.ogcapi.server.core.model.enumeration.CQLCrsType; +import au.org.aodn.stac.model.AssetModel; +import au.org.aodn.stac.model.CitationModel; +import au.org.aodn.stac.model.ConceptModel; +import au.org.aodn.stac.model.ContactsAddressModel; +import au.org.aodn.stac.model.ContactsModel; +import au.org.aodn.stac.model.ContactsPhoneModel; +import au.org.aodn.stac.model.ExtentModel; +import au.org.aodn.stac.model.LinkModel; +import au.org.aodn.stac.model.StacCollectionModel; +import au.org.aodn.stac.model.SummariesModel; +import au.org.aodn.stac.model.ThemesModel; import au.org.aodn.ogcapi.server.core.model.enumeration.CollectionProperty; import au.org.aodn.ogcapi.server.core.parser.stac.CQLToStacFilterFactory; import au.org.aodn.ogcapi.server.core.service.ElasticSearch; @@ -19,6 +30,7 @@ import org.springframework.boot.test.context.SpringBootTest; import java.io.IOException; +import java.time.Instant; import java.util.*; import static au.org.aodn.ogcapi.server.BaseTestClass.readResourceFile; @@ -50,12 +62,87 @@ public void verifyBBoxEmptyOrNullWorks() { stacToCollection.convert(model, null); } + @Test + public void verifyTemporalIntervalParsing() { + StacToCollection stacToCollection = new StacToCollectionImpl(); + + // Two intervals: one bounded, one with an unbounded end (ongoing). + // Mirrors STAC's [start, null] convention for "from start, ongoing". + List> temporal = Arrays.asList( + Arrays.asList("2020-01-01T00:00:00Z", "2020-12-31T23:59:59Z"), + Arrays.asList("2021-06-15T12:00:00Z", null) + ); + + StacCollectionModel model = StacCollectionModel + .builder() + .extent(new ExtentModel(Collections.singletonList(Collections.emptyList()), temporal)) + .build(); + + ExtendedCollection collection = (ExtendedCollection) stacToCollection.convert(model, null); + + List> interval = collection.getExtent().getTemporal().getInterval(); + Assertions.assertNotNull(interval); + Assertions.assertEquals(2, interval.size()); + + Assertions.assertEquals(Date.from(Instant.parse("2020-01-01T00:00:00Z")), interval.get(0).get(0)); + Assertions.assertEquals(Date.from(Instant.parse("2020-12-31T23:59:59Z")), interval.get(0).get(1)); + + Assertions.assertEquals(Date.from(Instant.parse("2021-06-15T12:00:00Z")), interval.get(1).get(0)); + Assertions.assertNull(interval.get(1).get(1), "Unbounded interval end must remain null"); + } + + @Test + public void verifyTemporalIntervalParsingPreservesNullInnerList() { + StacToCollection stacToCollection = new StacToCollectionImpl(); + + // A null inner list should be preserved as null (not flattened or skipped). + List> temporal = new ArrayList<>(); + temporal.add(null); + temporal.add(Arrays.asList("2022-03-01T00:00:00Z", "2022-03-31T23:59:59Z")); + + StacCollectionModel model = StacCollectionModel + .builder() + .extent(new ExtentModel(Collections.singletonList(Collections.emptyList()), temporal)) + .build(); + + ExtendedCollection collection = (ExtendedCollection) stacToCollection.convert(model, null); + + List> interval = collection.getExtent().getTemporal().getInterval(); + Assertions.assertNotNull(interval); + Assertions.assertEquals(2, interval.size()); + Assertions.assertNull(interval.get(0), "Null inner interval list must be preserved as null"); + Assertions.assertEquals(Date.from(Instant.parse("2022-03-01T00:00:00Z")), interval.get(1).get(0)); + Assertions.assertEquals(Date.from(Instant.parse("2022-03-31T23:59:59Z")), interval.get(1).get(1)); + } + + @Test + public void verifyTemporalIntervalParsingNormalisesNonZOffset() { + StacToCollection stacToCollection = new StacToCollectionImpl(); + + // Instant.parse accepts RFC 3339 offsets beyond 'Z' and normalises to UTC, + // so 00:00+10:00 lands at the previous day 14:00Z. + List> temporal = Collections.singletonList( + Arrays.asList("2020-01-01T00:00:00+10:00", "2020-12-31T23:59:59+10:00") + ); + + StacCollectionModel model = StacCollectionModel + .builder() + .extent(new ExtentModel(Collections.singletonList(Collections.emptyList()), temporal)) + .build(); + + ExtendedCollection collection = (ExtendedCollection) stacToCollection.convert(model, null); + List> interval = collection.getExtent().getTemporal().getInterval(); + + Assertions.assertEquals(Date.from(Instant.parse("2019-12-31T14:00:00Z")), interval.get(0).get(0)); + Assertions.assertEquals(Date.from(Instant.parse("2020-12-31T13:59:59Z")), interval.get(0).get(1)); + } + @Test public void verifyAddingPropertyWorks() { StacToCollection stacToCollection = new StacToCollectionImpl(); List credits = Arrays.asList("credit1", "credit2"); - var address = AddressModel.builder() + var address = ContactsAddressModel.builder() .city("city") .country("country") .postalCode("postalCode") @@ -63,15 +150,15 @@ public void verifyAddingPropertyWorks() { .deliveryPoint(Arrays.asList("deliveryPoint1", "deliveryPoint2")) .build(); var link = LinkModel.builder().rel("rel").href("href").type("type").title("title").build(); - var contact = ContactModel.builder() + var contact = ContactsModel.builder() .addresses(Collections.singletonList(address)) .name("name") .organization("organization") .roles(Collections.singletonList("roles")) .emails(Arrays.asList("email1", "email2")) .links(Collections.singletonList(link)) - .phones(Collections.singletonList(InfoModel.builder().value("value").build()) - ).build(); + .phones(Collections.singletonList(ContactsPhoneModel.builder().value("value").build())) + .build(); var link1 = LinkModel.builder() .rel("related") .href("https://example.com/data") @@ -89,7 +176,7 @@ public void verifyAddingPropertyWorks() { .aiGroup("ai-group") .description("description") .build(); - var theme = ThemeModel.builder() + var theme = ThemesModel.builder() .scheme("scheme") .concepts(Collections.singletonList( ConceptModel.builder().id("id").url("url").description("description").title("title").build() @@ -113,6 +200,10 @@ public void verifyAddingPropertyWorks() { scope.put("name", "IMOS publication"); List parameterVocabs = Arrays.asList("wave", "temperature"); + List platformVocabs = Arrays.asList("vessel", "satellite"); + List organisationVocabs = Arrays.asList("IMOS", "AODN"); + List aiPlatformVocabs = Arrays.asList("ai-vessel", "ai-satellite"); + String datasetProvider = "IMOS"; StacCollectionModel model = StacCollectionModel .builder() @@ -129,6 +220,10 @@ public void verifyAddingPropertyWorks() { .aiDescription(aiDescription) .scope(scope) .parameterVocabs(parameterVocabs) + .platformVocabs(platformVocabs) + .organisationVocabs(organisationVocabs) + .aiPlatformVocabs(aiPlatformVocabs) + .datasetProvider(datasetProvider) .build() ) .license("Attribution 4.0") @@ -159,6 +254,10 @@ public void verifyAddingPropertyWorks() { Assertions.assertEquals("document", ((Map) collection.getProperties().get(CollectionProperty.scope)).get("code")); Assertions.assertEquals(parameterVocabs, collection.getProperties().get(CollectionProperty.parameterVocabs)); + Assertions.assertEquals(platformVocabs, collection.getProperties().get(CollectionProperty.platformVocabs)); + Assertions.assertEquals(organisationVocabs, collection.getProperties().get(CollectionProperty.organisationVocabs)); + Assertions.assertEquals(aiPlatformVocabs, collection.getProperties().get(CollectionProperty.aiPlatformVocabs)); + Assertions.assertEquals(datasetProvider, collection.getProperties().get(CollectionProperty.datasetProvider)); Assertions.assertNotNull(collection.getLinks()); Assertions.assertEquals(3, collection.getLinks().size()); @@ -170,6 +269,31 @@ public void verifyAddingPropertyWorks() { Assertions.assertEquals(List.of("download"), convertedLink1.getAiRole()); } + @Test + public void verifyNewSummariesFieldsGuardsSkipEmptyAndNullValues() { + StacToCollection stacToCollection = new StacToCollectionImpl(); + + StacCollectionModel model = StacCollectionModel + .builder() + .summaries( + SummariesModel + .builder() + .platformVocabs(Collections.emptyList()) + .organisationVocabs(null) + .aiPlatformVocabs(Collections.emptyList()) + .datasetProvider(null) + .build() + ) + .build(); + + ExtendedCollection collection = (ExtendedCollection) stacToCollection.convert(model, null); + + Assertions.assertFalse(collection.getProperties().containsKey(CollectionProperty.platformVocabs)); + Assertions.assertFalse(collection.getProperties().containsKey(CollectionProperty.organisationVocabs)); + Assertions.assertFalse(collection.getProperties().containsKey(CollectionProperty.aiPlatformVocabs)); + Assertions.assertFalse(collection.getProperties().containsKey(CollectionProperty.datasetProvider)); + } + @Test public void verifyConvertWorks1() throws IOException, CQLException { String json = readResourceFile("classpath:databag/0c681199-06cd-435c-9468-be6998799b1f.json"); diff --git a/server/src/test/java/au/org/aodn/ogcapi/server/core/parser/stac/ParserTest.java b/server/src/test/java/au/org/aodn/ogcapi/server/core/parser/stac/ParserTest.java index 47812048..a1967aa9 100644 --- a/server/src/test/java/au/org/aodn/ogcapi/server/core/parser/stac/ParserTest.java +++ b/server/src/test/java/au/org/aodn/ogcapi/server/core/parser/stac/ParserTest.java @@ -2,7 +2,7 @@ import au.org.aodn.ogcapi.server.BaseTestClass; import au.org.aodn.ogcapi.server.core.mapper.Converter; -import au.org.aodn.ogcapi.server.core.model.StacCollectionModel; +import au.org.aodn.stac.model.StacCollectionModel; import au.org.aodn.ogcapi.server.core.model.enumeration.CQLCrsType; import au.org.aodn.ogcapi.server.core.util.GeometryUtils; import com.fasterxml.jackson.annotation.JsonInclude; diff --git a/server/src/test/java/au/org/aodn/ogcapi/server/core/service/geoserver/wfs/WfsServerTest.java b/server/src/test/java/au/org/aodn/ogcapi/server/core/service/geoserver/wfs/WfsServerTest.java index 87f4aa15..88f67cd3 100644 --- a/server/src/test/java/au/org/aodn/ogcapi/server/core/service/geoserver/wfs/WfsServerTest.java +++ b/server/src/test/java/au/org/aodn/ogcapi/server/core/service/geoserver/wfs/WfsServerTest.java @@ -1,8 +1,8 @@ package au.org.aodn.ogcapi.server.core.service.geoserver.wfs; import au.org.aodn.ogcapi.server.core.exception.GeoserverFieldsNotFoundException; -import au.org.aodn.ogcapi.server.core.model.LinkModel; -import au.org.aodn.ogcapi.server.core.model.StacCollectionModel; +import au.org.aodn.stac.model.StacCollectionModel; +import au.org.aodn.stac.model.LinkModel; import au.org.aodn.ogcapi.server.core.model.ogc.FeatureRequest; import au.org.aodn.ogcapi.server.core.model.ogc.wfs.FeatureTypeInfo; import au.org.aodn.ogcapi.server.core.model.ogc.wfs.WfsField; diff --git a/server/src/test/java/au/org/aodn/ogcapi/server/core/service/geoserver/wms/WmsServerTest.java b/server/src/test/java/au/org/aodn/ogcapi/server/core/service/geoserver/wms/WmsServerTest.java index f3d4c8ec..f12abc51 100644 --- a/server/src/test/java/au/org/aodn/ogcapi/server/core/service/geoserver/wms/WmsServerTest.java +++ b/server/src/test/java/au/org/aodn/ogcapi/server/core/service/geoserver/wms/WmsServerTest.java @@ -5,8 +5,8 @@ import au.org.aodn.ogcapi.server.core.configuration.TestConfig; import au.org.aodn.ogcapi.server.core.configuration.GeoServerConfig; import au.org.aodn.ogcapi.server.core.exception.GeoserverFieldsNotFoundException; -import au.org.aodn.ogcapi.server.core.model.LinkModel; -import au.org.aodn.ogcapi.server.core.model.StacCollectionModel; +import au.org.aodn.stac.model.StacCollectionModel; +import au.org.aodn.stac.model.LinkModel; import au.org.aodn.ogcapi.server.core.model.ogc.FeatureRequest; import au.org.aodn.ogcapi.server.core.model.ogc.wms.DescribeLayerResponse; import au.org.aodn.ogcapi.server.core.service.ElasticSearchBase; diff --git a/server/src/test/java/au/org/aodn/ogcapi/server/core/util/LinkUtilsTest.java b/server/src/test/java/au/org/aodn/ogcapi/server/core/util/LinkUtilsTest.java index 4b07fe4d..8a33e9b4 100644 --- a/server/src/test/java/au/org/aodn/ogcapi/server/core/util/LinkUtilsTest.java +++ b/server/src/test/java/au/org/aodn/ogcapi/server/core/util/LinkUtilsTest.java @@ -1,5 +1,6 @@ package au.org.aodn.ogcapi.server.core.util; +import au.org.aodn.stac.model.LinkModel; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; @@ -67,4 +68,43 @@ void testParseLinkTitleDescription_titleWithAbstract() { assertEquals(combinedTitle, result[0]); assertNull(result[1]); } + + @Test + void testApplyParsedTitle_splitsPackedTitle() { + LinkModel link = new LinkModel(); + link.setTitle("{\"title\":\"foo\",\"description\":\"bar\"}"); + + LinkUtils.applyParsedTitle(link); + + assertEquals("foo", link.getTitle()); + assertEquals("bar", link.getDescription()); + } + + @Test + void testApplyParsedTitle_preservesExistingDescription() { + LinkModel link = new LinkModel(); + link.setDescription("existing"); + link.setTitle("{\"title\":\"foo\",\"description\":\"bar\"}"); + + LinkUtils.applyParsedTitle(link); + + assertEquals("foo", link.getTitle()); + assertEquals("existing", link.getDescription()); + } + + @Test + void testApplyParsedTitle_plainTitlePassesThrough() { + LinkModel link = new LinkModel(); + link.setTitle("Just a title"); + + LinkUtils.applyParsedTitle(link); + + assertEquals("Just a title", link.getTitle()); + assertNull(link.getDescription()); + } + + @Test + void testApplyParsedTitle_nullLinkIsNoop() { + assertDoesNotThrow(() -> LinkUtils.applyParsedTitle(null)); + } } diff --git a/server/src/test/resources/data_index_schema.json b/server/src/test/resources/data_index_schema.json deleted file mode 100644 index c7c73430..00000000 --- a/server/src/test/resources/data_index_schema.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "mappings": { - "properties": { - "type": { - "type": "keyword" - }, - "features": { - "type": "nested", - "properties": { - "type": { - "type": "keyword" - }, - "geometry": { - "type": "geo_shape" - }, - "properties": { - "type": "object", - "properties": { - "date": { - "type": "date" - }, - "count": { - "type": "double" - }, - "collection": { - "type": "keyword" - }, - "key": { - "type": "keyword" - } - } - } - } - } - } - } -} diff --git a/server/src/test/resources/portal_records_index_schema.json b/server/src/test/resources/portal_records_index_schema.json deleted file mode 100644 index b0d9dec7..00000000 --- a/server/src/test/resources/portal_records_index_schema.json +++ /dev/null @@ -1,373 +0,0 @@ -{ - "settings": { - "analysis": { - "analyzer": { - "custom_analyser": { - "type": "custom", - "tokenizer": "standard", - "filter": [ - "lowercase", - "english_stop" - ] - }, - "shingle_analyser": { - "type": "custom", - "tokenizer": "standard", - "char_filter": [ - "html_strip" - ], - "filter": [ - "lowercase", - "asciifolding", - "remove_numbers", - "uuid_filter", - "non_standard_pattern_filter", - "et_al_stop", - "english_stop", - "length_filter", - "token_limit", - "shingle_filter", - "unique" - ] - } - }, - "filter": { - "english_stop": { - "type": "stop", - "stopwords": "_english_" - }, - "shingle_filter": { - "type": "shingle", - "min_shingle_size": 2, - "max_shingle_size": 4, - "output_unigrams": true - }, - "uuid_filter": { - "type": "pattern_replace", - "pattern": "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}", - "replacement": "" - }, - "non_standard_pattern_filter": { - "type": "pattern_replace", - "pattern": ".*[^a-zA-Z- ].*", - "replacement": "" - }, - "remove_numbers": { - "type": "pattern_replace", - "pattern": "\\b\\d+\\b", - "replacement": "" - }, - "token_limit" : { - "type": "limit", - "max_token_count": 350 - }, - "length_filter": { - "type": "length", - "min": 2 - }, - "et_al_stop": { - "type": "stop", - "stopwords": ["et", "al", "et al", "et.", "al."] - } - } - } - }, - "mappings": { - "dynamic": true, - "properties": { - "id": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword" - } - }, - "analyzer": "keyword" - }, - "stac_version": { - "type": "keyword", - "index": false - }, - "type": { - "type": "keyword", - "index": false - }, - "title": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword" - } - } - }, - "search_suggestions": { - "type": "nested", - "properties": { - "abstract_phrases": { - "type": "search_as_you_type", - "analyzer": "custom_analyser" - }, - "parameter_vocabs_sayt": { - "type": "search_as_you_type", - "analyzer": "custom_analyser" - }, - "platform_vocabs_sayt": { - "type": "search_as_you_type", - "analyzer": "custom_analyser" - }, - "organisation_vocabs_sayt": { - "type": "search_as_you_type", - "analyzer": "custom_analyser" - } - } - }, - "parameter_vocabs": { - "type": "keyword" - }, - "platform_vocabs": { - "type": "keyword" - }, - "organisation_vocabs": { - "type": "keyword" - }, - "keywords": { - "type": "nested", - "properties": { - "keyword": { - "type": "text" - } - } - }, - "extent": { - "type": "nested", - "properties": { - "bbox": { - "type": "double" - }, - "temporal": { - "type": "date" - } - } - }, - "description": { - "type": "text" - }, - "license": { - "type": "keyword", - "index": false - }, - "links": { - "type": "nested", - "properties": { - "link": { - "type": "nested", - "properties": { - "href": { - "type": "keyword", - "index": false - }, - "rel": { - "type": "keyword", - "index": false - }, - "type": { - "type": "keyword", - "index": false - }, - "title": { - "type": "keyword" - }, - "description": { - "type": "keyword" - }, - "ai:group": { - "type": "keyword" - }, - "ai:role": { - "type": "keyword" - } - } - } - } - }, - "assets": { - "type": "flattened" - }, - "sci:citation": { - "type": "keyword", - "index": false - }, - "summaries": { - "properties": { - "ai:description": { - "type": "keyword", - "index": false - }, - "ai:update_frequency": { - "type": "keyword" - }, - "ai:parameter_vocabs": { - "type": "keyword" - }, - "ai:platform_vocabs": { - "type": "keyword" - }, - "score": { - "type": "long" - }, - "status": { - "type": "keyword" - }, - "credits": { - "type": "text" - }, - "scope": { - "type": "nested", - "properties": { - "code": { - "type": "keyword" - }, - "name": { - "type": "keyword" - } - } - }, - "dataset_provider": { - "type": "text" - }, - "dataset_group": { - "type": "keyword" - }, - "creation": { - "type": "date" - }, - "revision": { - "type": "date" - }, - "proj:geometry_noland": { - "type": "geo_shape" - }, - "proj:geometry": { - "type": "geo_shape" - }, - "temporal": { - "type": "nested", - "properties": { - "start": { - "type": "date" - }, - "end": { - "type": "date" - } - } - }, - "statement": { - "type": "keyword", - "index": false - } - } - }, - "contacts": { - "type": "nested", - "properties": { - "contact": { - "type": "nested", - "properties": { - "name": { - "type": "keyword", - "index": false - }, - "organization": { - "type": "keyword" - }, - "position": { - "type": "keyword", - "index": false - }, - "phones": { - "type": "nested", - "properties": { - "value": { - "type": "keyword", - "index": false - }, - "roles": { - "type": "keyword", - "index": false - } - } - }, - "emails": { - "type": "nested", - "properties": { - "value": { - "type": "keyword", - "index": false - }, - "roles": { - "type": "keyword", - "index": false - } - } - }, - "addresses": { - "type": "nested", - "properties": { - "delivery_point": { - "type": "keyword", - "index": false - }, - "city": { - "type": "keyword", - "index": false - }, - "administrative_area": { - "type": "keyword", - "index": false - }, - "postal_code": { - "type": "keyword", - "index": false - }, - "country": { - "type": "keyword", - "index": false - } - } - }, - "links": { - "type": "nested", - "properties": { - "link": { - "type": "nested", - "properties": { - "href": { - "type": "keyword" - }, - "rel": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "title": { - "type": "keyword" - }, - "description": { - "type": "keyword" - } - } - } - } - }, - "roles": { - "type": "keyword", - "index": false - } - } - } - } - } - } - } -} diff --git a/server/src/test/resources/vocabs_index_schema.json b/server/src/test/resources/vocabs_index_schema.json deleted file mode 100644 index 8055f597..00000000 --- a/server/src/test/resources/vocabs_index_schema.json +++ /dev/null @@ -1,112 +0,0 @@ -{ - "mappings": { - "dynamic": true, - "properties": { - "parameter_vocab": { - "properties": { - "label": { - "type": "text" - }, - "definition": { - "type": "text" - }, - "about": { - "type": "keyword" - }, - "narrower": { - "type": "nested", - "properties": { - "label": { - "type": "text" - }, - "about": { - "type": "keyword" - }, - "narrower": { - "type": "nested", - "properties": { - "label": { - "type": "text" - }, - "about": { - "type": "keyword" - } - } - } - } - } - } - }, - "platform_vocab": { - "properties": { - "label": { - "type": "text" - }, - "definition": { - "type": "text" - }, - "about": { - "type": "keyword" - }, - "narrower": { - "type": "nested", - "properties": { - "label": { - "type": "text" - }, - "about": { - "type": "keyword" - }, - "narrower": { - "type": "nested", - "properties": { - "label": { - "type": "text" - }, - "about": { - "type": "keyword" - } - } - } - } - } - } - }, - "organisation_vocab": { - "properties": { - "label": { - "type": "text" - }, - "definition": { - "type": "text" - }, - "about": { - "type": "keyword" - }, - "narrower": { - "type": "nested", - "properties": { - "label": { - "type": "text" - }, - "about": { - "type": "keyword" - }, - "narrower": { - "type": "nested", - "properties": { - "label": { - "type": "text" - }, - "about": { - "type": "keyword" - } - } - } - } - } - } - } - } - } -}