diff --git a/modules/library/common-index-model/src/main/java/org/fao/geonet/index/converter/DcatConverter.java b/modules/library/common-index-model/src/main/java/org/fao/geonet/index/converter/DcatConverter.java index 22fc5ace..a4888402 100644 --- a/modules/library/common-index-model/src/main/java/org/fao/geonet/index/converter/DcatConverter.java +++ b/modules/library/common-index-model/src/main/java/org/fao/geonet/index/converter/DcatConverter.java @@ -316,7 +316,7 @@ private Dataset getDataset(IndexRecord record, String uri, String resourceIdenti } // - datasetBuilder.spatial(record.getGeometries().stream().map(g -> DctSpatial.builder() + datasetBuilder.spatial(record.getGeometriesAsJsonString().stream().map(g -> DctSpatial.builder() .location(DctLocation.builder().geometry(g).build()).build()).collect( Collectors.toList())); @@ -470,7 +470,8 @@ private DataService getDataService(IndexRecord record, String uri, String resour .getProperties().get(CommonField.key)); } - dataServiceBuilder.spatial(record.getGeometries().stream().map(g -> DctSpatial.builder() + dataServiceBuilder.spatial(record.getGeometriesAsJsonString().stream() + .map(g -> DctSpatial.builder() .location(DctLocation.builder().geometry(g).build()).build()).collect( Collectors.toList())); diff --git a/modules/library/common-index-model/src/main/java/org/fao/geonet/index/converter/GeoJsonConverter.java b/modules/library/common-index-model/src/main/java/org/fao/geonet/index/converter/GeoJsonConverter.java index 9f2c1ea6..464877b0 100644 --- a/modules/library/common-index-model/src/main/java/org/fao/geonet/index/converter/GeoJsonConverter.java +++ b/modules/library/common-index-model/src/main/java/org/fao/geonet/index/converter/GeoJsonConverter.java @@ -29,7 +29,7 @@ * Index document to GeoJSON mapping. */ @Component -public class GeoJsonConverter { +public class GeoJsonConverter implements IGeoJsonConverter { @Autowired FormatterConfiguration formatterConfiguration; @@ -50,7 +50,7 @@ public Record convert(IndexRecord record) { ObjectMapper objectMapper = JsonUtils.getObjectMapper(); recordBuilder.geometry(objectMapper.readValue( - record.getGeometries().get(0), + record.getGeometriesAsJsonString().get(0), Geometry.class)); } catch (Exception ex) { // TODO: Process the exception diff --git a/modules/library/common-index-model/src/main/java/org/fao/geonet/index/converter/IGeoJsonConverter.java b/modules/library/common-index-model/src/main/java/org/fao/geonet/index/converter/IGeoJsonConverter.java new file mode 100644 index 00000000..2ce7ed93 --- /dev/null +++ b/modules/library/common-index-model/src/main/java/org/fao/geonet/index/converter/IGeoJsonConverter.java @@ -0,0 +1,7 @@ +package org.fao.geonet.index.converter; + +import org.fao.geonet.index.model.gn.IndexRecord; + +public interface IGeoJsonConverter { + Object convert(IndexRecord record); +} diff --git a/modules/library/common-index-model/src/main/java/org/fao/geonet/index/converter/SchemaOrgConverter.java b/modules/library/common-index-model/src/main/java/org/fao/geonet/index/converter/SchemaOrgConverter.java index a589ec4d..ea654453 100644 --- a/modules/library/common-index-model/src/main/java/org/fao/geonet/index/converter/SchemaOrgConverter.java +++ b/modules/library/common-index-model/src/main/java/org/fao/geonet/index/converter/SchemaOrgConverter.java @@ -14,6 +14,7 @@ import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.StringUtils; import org.fao.geonet.index.model.gn.Contact; import org.fao.geonet.index.model.gn.IndexRecord; @@ -35,6 +36,7 @@ * *

TODO: Add support to translation https://bib.schema.org/workTranslation */ +@Slf4j(topic = "org.fao.geonet.ogcapi.records") public class SchemaOrgConverter { public static Map dateMapping = Map.ofEntries( @@ -246,7 +248,7 @@ public static ObjectNode convert(IndexRecord record) { if (record.getGeometries().size() > 0) { ObjectNode spatialCoverage = root.putObject("spatialCoverage"); ArrayNode geo = spatialCoverage.putArray("geo"); - record.getGeometries().forEach(g -> { + record.getGeometriesAsJsonString().forEach(g -> { GeoJsonReader geoJsonReader = new GeoJsonReader(); try { ObjectNode shape = createThing(null, Types.GeoShape, root); @@ -259,7 +261,7 @@ public static ObjectNode convert(IndexRecord record) { envelope.getMaxY(), envelope.getMaxX())); geo.add(shape); } catch (ParseException e) { - e.printStackTrace(); + log.debug(e.getMessage(),e); } }); } diff --git a/modules/library/common-index-model/src/main/java/org/fao/geonet/index/model/gn/IndexRecord.java b/modules/library/common-index-model/src/main/java/org/fao/geonet/index/model/gn/IndexRecord.java index 1f09d24d..ca667766 100644 --- a/modules/library/common-index-model/src/main/java/org/fao/geonet/index/model/gn/IndexRecord.java +++ b/modules/library/common-index-model/src/main/java/org/fao/geonet/index/model/gn/IndexRecord.java @@ -9,8 +9,11 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import java.sql.Array; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -151,14 +154,14 @@ public class IndexRecord extends IndexDocument { // TODO XML @XmlTransient @JsonProperty(IndexRecordFieldNames.geom) - @JsonDeserialize(using = NodeTreeAsStringDeserializer.class) - private List geometries = new ArrayList<>(); + //@JsonDeserialize(using = NodeTreeAsStringDeserializer.class) + private List geometries = new ArrayList<>(); @JsonProperty(IndexRecordFieldNames.specificationConformance) private List specificationConformance = new ArrayList(); // @JsonAnyGetter - private Map> otherProperties = new HashMap<>(); + private Map otherProperties = new HashMap<>(); private Map> codelists = new HashMap<>(); @@ -202,6 +205,28 @@ public IndexRecord(AbstractMetadata r) { public IndexRecord() { } + /** + * get the #geometries as a JSON string. + */ + public List getGeometriesAsJsonString() { + if (geometries == null || geometries.isEmpty()) { + return null; + } + List result = new ArrayList<>(); + + for (var g:geometries) { + String s = null; + try { + s = (new ObjectMapper()).writeValueAsString(g); + } catch (JsonProcessingException e) { + continue; + } + result.add(s); + } + return result; + + } + /** * Collect all other properties in a map. */ @@ -222,13 +247,18 @@ public void ignored(String name, Object value) { new Codelist(c)).collect(Collectors.toList())); } } else { - ArrayList s = otherProperties.get(name); + var s = otherProperties.get(name); if (s == null) { - s = new ArrayList<>(1); - s.add(value.toString()); - otherProperties.put(name, s); + otherProperties.put(name, value); } else { - s.add(value.toString()); + if (s instanceof List) { + ((List) s).add(value.toString()); + } else { + var list = new ArrayList(); + list.add(s); + list.add(value); + otherProperties.put(name, list); + } } } } diff --git a/modules/library/common-index-model/src/main/java/org/fao/geonet/index/model/gn/LocationSerializer.java b/modules/library/common-index-model/src/main/java/org/fao/geonet/index/model/gn/LocationSerializer.java index bd3fb8c3..1886646c 100644 --- a/modules/library/common-index-model/src/main/java/org/fao/geonet/index/model/gn/LocationSerializer.java +++ b/modules/library/common-index-model/src/main/java/org/fao/geonet/index/model/gn/LocationSerializer.java @@ -20,6 +20,7 @@ public void serialize( JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeStartObject(); gen.writeArrayFieldStart(IndexRecordFieldNames.location); coordinate.forEach(c -> { try { diff --git a/modules/library/common-index-model/src/test/java/org/fao/geonet/index/model/gn/IndexRecordTest.java b/modules/library/common-index-model/src/test/java/org/fao/geonet/index/model/gn/IndexRecordTest.java index f13e14df..3234c3e1 100644 --- a/modules/library/common-index-model/src/test/java/org/fao/geonet/index/model/gn/IndexRecordTest.java +++ b/modules/library/common-index-model/src/test/java/org/fao/geonet/index/model/gn/IndexRecordTest.java @@ -99,7 +99,7 @@ public void testJsonToPojo() throws IOException { Assert.assertEquals( "{\"type\":\"Polygon\",\"coordinates\":[[[-31.2684,27.6375],[-13.4198,27.6375],[-13.4198,66.5662],[-31.2684,66.5662],[-31.2684,27.6375]]]}", - record.getGeometries().get(0)); + record.getGeometriesAsJsonString().get(0)); } catch (JsonProcessingException e) { e.printStackTrace(); Assert.fail(); diff --git a/modules/library/common-search/src/main/java/org/fao/geonet/common/search/processor/impl/GeoJsonResponseProcessorImpl.java b/modules/library/common-search/src/main/java/org/fao/geonet/common/search/processor/impl/GeoJsonResponseProcessorImpl.java index 53ad4542..f271bedb 100644 --- a/modules/library/common-search/src/main/java/org/fao/geonet/common/search/processor/impl/GeoJsonResponseProcessorImpl.java +++ b/modules/library/common-search/src/main/java/org/fao/geonet/common/search/processor/impl/GeoJsonResponseProcessorImpl.java @@ -6,13 +6,14 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import java.io.InputStream; import java.io.OutputStream; +import java.time.Instant; +import java.util.concurrent.atomic.AtomicInteger; import javax.servlet.http.HttpSession; import lombok.extern.slf4j.Slf4j; import org.fao.geonet.common.search.domain.ReservedOperation; import org.fao.geonet.common.search.domain.UserInfo; import org.fao.geonet.index.JsonUtils; -import org.fao.geonet.index.converter.GeoJsonConverter; -import org.fao.geonet.index.model.geojson.Record; +import org.fao.geonet.index.converter.IGeoJsonConverter; import org.fao.geonet.index.model.gn.IndexRecord; import org.fao.geonet.index.model.gn.IndexRecordFieldNames; import org.springframework.beans.factory.annotation.Autowired; @@ -25,7 +26,7 @@ public class GeoJsonResponseProcessorImpl extends JsonUserAndSelectionAwareResponseProcessorImpl { @Autowired - GeoJsonConverter geoJsonConverter; + IGeoJsonConverter geoJsonConverter; @Override public void processResponse(HttpSession httpSession, @@ -37,13 +38,14 @@ public void processResponse(HttpSession httpSession, JsonGenerator generator = ResponseParser.jsonFactory.createGenerator(streamToClient); try { - ResponseParser responseParser = new ResponseParser(); + ResponseParser elasticJsonResponseParser = new ResponseParser(); generator.writeStartObject(); generator.writeStringField("type", "FeatureCollection"); generator.writeArrayFieldStart("features"); + AtomicInteger numbFeatures = new AtomicInteger(0); { - responseParser.matchHits(parser, generator, doc -> { + elasticJsonResponseParser.matchHits(parser, generator, doc -> { // Remove fields with privileges info if (doc.has(IndexRecordFieldNames.source)) { @@ -57,8 +59,9 @@ public void processResponse(HttpSession httpSession, doc.get(IndexRecordFieldNames.source).toPrettyString(), IndexRecord.class); try { - Record geojsonRecord = geoJsonConverter.convert(indexRecord); + Object geojsonRecord = geoJsonConverter.convert(indexRecord); generator.writeRawValue(objectMapper.writeValueAsString(geojsonRecord)); + numbFeatures.incrementAndGet(); } catch (Exception ex) { log.error(String.format( "GeoJSON conversion returned null result for uuid %s. Check http://localhost:9901/collections/main/items/%s?f=geojson", @@ -68,7 +71,15 @@ public void processResponse(HttpSession httpSession, }, false); } generator.writeEndArray(); - generator.writeNumberField("size", responseParser.total); + generator.writeNumberField("numberMatched", elasticJsonResponseParser.total); + generator.writeNumberField("numberReturned", numbFeatures.intValue()); + + generator.writeStringField("timeStamp", Instant.now().toString()); + + generator.writeArrayFieldStart("links"); + //TO DO ADD LINKS + generator.writeEndArray(); + generator.writeEndObject(); generator.flush(); } finally { diff --git a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/OgcApiRecordApp.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/OgcApiRecordApp.java index 564c1172..39cd0229 100644 --- a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/OgcApiRecordApp.java +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/OgcApiRecordApp.java @@ -5,6 +5,7 @@ package org.fao.geonet.ogcapi.records; +import org.fao.geonet.index.converter.GeoJsonConverter; import org.fao.geonet.ogcapi.records.controller.CapabilitiesApiController; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @@ -13,6 +14,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.FilterType; import org.springframework.context.annotation.Import; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -21,7 +23,10 @@ @SpringBootApplication @RefreshScope @Import({CapabilitiesApiController.class}) -@ComponentScan({"org.fao.geonet", "org.fao.geonet.domain"}) +@ComponentScan( + value = {"org.fao.geonet", "org.fao.geonet.domain"}, excludeFilters = + {@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = GeoJsonConverter.class)} +) @Configuration @EnableCaching public class OgcApiRecordApp { @@ -30,9 +35,9 @@ public static void main(String[] args) { SpringApplication.run(OgcApiRecordApp.class, args); } - /** * Configure CORS to allow all connections. + * * @return CORS configuration. */ @Bean diff --git a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/OgcApiRecordWebApp.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/OgcApiRecordWebApp.java index c39cf79d..bac069b1 100644 --- a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/OgcApiRecordWebApp.java +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/OgcApiRecordWebApp.java @@ -5,18 +5,33 @@ package org.fao.geonet.ogcapi.records; +import org.fao.geonet.index.converter.GeoJsonConverter; import org.fao.geonet.ogcapi.records.controller.CapabilitiesApiController; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.FilterType; import org.springframework.context.annotation.Import; + +/** + * We remove the "GeoJsonConverter" from the scanned components so this will app will use the + * "OgcApiGeoJsonConverter". + */ @SpringBootApplication @Import({CapabilitiesApiController.class}) -@ComponentScan({"org.fao.geonet", "org.fao.geonet.domain"}) +@ComponentScan( + value = {"org.fao.geonet", "org.fao.geonet.domain"}, excludeFilters = + {@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = GeoJsonConverter.class)} +) +//@ComponentScan( +// value = {"org.fao.geonet", "org.fao.geonet.domain"} +//) @Configuration @EnableCaching public class OgcApiRecordWebApp extends SpringBootServletInitializer { + + } \ No newline at end of file diff --git a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/controller/ItemApiController.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/controller/ItemApiController.java index 26f26c59..ae27f967 100644 --- a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/controller/ItemApiController.java +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/controller/ItemApiController.java @@ -288,7 +288,11 @@ public ResponseEntity collectionsCollectionIdItemsGet( boolean allSourceFields = mediaType.equals(GnMediaType.APPLICATION_DCAT2_XML) - || mediaType.equals(GnMediaType.APPLICATION_RDF_XML); + || mediaType.equals(GnMediaType.APPLICATION_RDF_XML) + || mediaType.equals(GnMediaType.APPLICATION_GEOJSON) + || mediaType.equals(GnMediaType.APPLICATION_JSON_LD) + || mediaType.equals(MediaType.APPLICATION_JSON) + || mediaType.equals(GnMediaType.APPLICATION_RDF_XML); return collectionsCollectionIdItemsGetInternal( query, @@ -496,7 +500,8 @@ private List setDefaultRssSortBy(List sortby, HttpServletRequest private String search( Query requestQuery, - HttpServletRequest request, boolean allSourceFields) { + HttpServletRequest request, + boolean allSourceFields) { Source source = collectionService.retrieveSourceForCollection(requestQuery.getCollectionId()); @@ -634,4 +639,4 @@ private String getResponseContentType(HttpServletRequest request) { return mediaType; } -} \ No newline at end of file +} diff --git a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/converter/OgcApiGeoJsonConverter.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/converter/OgcApiGeoJsonConverter.java new file mode 100644 index 00000000..f947c0ce --- /dev/null +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/converter/OgcApiGeoJsonConverter.java @@ -0,0 +1,99 @@ +/** + * (c) 2024 Open Source Geospatial Foundation - all rights reserved This code is licensed under the + * GPL 2.0 license, available at the root application directory. + */ + +package org.fao.geonet.ogcapi.records.converter; + +import java.util.ArrayList; +import org.fao.geonet.domain.Metadata; +import org.fao.geonet.index.converter.IGeoJsonConverter; +import org.fao.geonet.index.model.gn.IndexRecord; +import org.fao.geonet.ogcapi.records.controller.model.CollectionInfo; +import org.fao.geonet.ogcapi.records.model.GeoJsonPolygon; +import org.fao.geonet.ogcapi.records.model.OgcApiTime; +import org.fao.geonet.ogcapi.records.util.ElasticIndexJson2CollectionInfo; +import org.fao.geonet.repository.MetadataRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Converts an elastic JSON Index Record into an OGCAPI compliant GeoJSON record. + */ +@Component +public class OgcApiGeoJsonConverter implements IGeoJsonConverter { + + @Autowired + ElasticIndexJson2CollectionInfo elasticIndexJson2CollectionInfo; + + @Autowired + MetadataRepository metadataRepository; + + /** + * converts an IndexRecord (elastic Index Json Record) to a OgcApiGeoJsonRecord. + * @param elasticIndexJsonRecord IndexRecord from Elastic + * @return converted to OgcApiGeoJsonRecord. + */ + public OgcApiGeoJsonRecord convert(IndexRecord elasticIndexJsonRecord) { + var result = new OgcApiGeoJsonRecord(); + + injectCollectionInfoProperties(result, elasticIndexJsonRecord); + + result.setConformsTo(new ArrayList<>()); + result.getConformsTo() + .add("http://www.opengis.net/spec/ogcapi-records-1/1.0/req/record-core"); + + result.setProperty("gn-elastic-index-record", elasticIndexJsonRecord); + + Metadata metadataRecord = metadataRepository.findOneByUuid( + elasticIndexJsonRecord.getMetadataIdentifier()); + + result.setProperty("gn-record-xml", metadataRecord.getData()); + + if (elasticIndexJsonRecord.getResourceType() != null + && !elasticIndexJsonRecord.getResourceType().isEmpty()) { + result.setType(elasticIndexJsonRecord.getResourceType().get(0)); + } + return result; + } + + private void injectCollectionInfoProperties(OgcApiGeoJsonRecord result, + IndexRecord elasticIndexJsonRecord) { + var collectionInfo = new CollectionInfo(); + elasticIndexJson2CollectionInfo.injectLinkedServiceRecordInfo(collectionInfo, + elasticIndexJsonRecord); + + result.setId(collectionInfo.getId()); + result.setProperty("title", collectionInfo.getTitle()); + result.setProperty("description", collectionInfo.getDescription()); + result.setProperty("language", collectionInfo.getLanguage()); + result.setProperty("languages", collectionInfo.getLanguages()); + result.setProperty("created", collectionInfo.getCreated()); + result.setProperty("updated", collectionInfo.getUpdated()); + result.setProperty("type", collectionInfo.getType()); + result.setProperty("keywords", collectionInfo.getKeywords()); + result.setProperty("themes", collectionInfo.getThemes()); + result.setProperty("resourceLanguages", collectionInfo.getRecordLanguages()); + result.setProperty("contacts", collectionInfo.getContacts()); + result.setProperty("license", collectionInfo.getLicense()); + result.setProperty("rights", collectionInfo.getRights()); + + if (collectionInfo.getExtent() != null + && collectionInfo.getExtent().getSpatial() != null + && collectionInfo.getExtent().getSpatial().getBbox() != null + && !collectionInfo.getExtent().getSpatial().getBbox().isEmpty()) { + result.setGeometry( + GeoJsonPolygon.fromBBox(collectionInfo.getExtent().getSpatial().getBbox())); + } + + if (collectionInfo.getExtent() != null + && collectionInfo.getExtent().getTemporal() != null + && collectionInfo.getExtent().getTemporal().getInterval() != null + && !collectionInfo.getExtent().getTemporal().getInterval().isEmpty()) { + var time = new OgcApiTime(); + time.setInterval(collectionInfo.getExtent().getTemporal().getInterval()); + result.setTime(time); + } + } + +} diff --git a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/converter/OgcApiGeoJsonRecord.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/converter/OgcApiGeoJsonRecord.java new file mode 100644 index 00000000..1be34dc0 --- /dev/null +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/converter/OgcApiGeoJsonRecord.java @@ -0,0 +1,132 @@ +/** + * (c) 2024 Open Source Geospatial Foundation - all rights reserved This code is licensed under the + * GPL 2.0 license, available at the root application directory. + */ + +package org.fao.geonet.ogcapi.records.converter; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.experimental.SuperBuilder; +import org.fao.geonet.ogcapi.records.model.GeoJsonPolygon; +import org.fao.geonet.ogcapi.records.model.OgcApiLink; +import org.fao.geonet.ogcapi.records.model.OgcApiLinkTemplate; +import org.fao.geonet.ogcapi.records.model.OgcApiTime; + +/** + * See OGCAPI spec - Table 8. (8.2.2. Core properties) Core properties related to the catalog + * record. + */ +@Data +@SuperBuilder +@AllArgsConstructor +public class OgcApiGeoJsonRecord { + + /** + * A unique record identifier assigned by the server. + */ + private String id; + + /** + * A spatial extent associated with the resource described by this record. Can be null if there is + * no associated spatial extent. + */ + @JsonInclude(Include.NON_EMPTY) + private GeoJsonPolygon geometry; + + /** + * The extensions/conformance classes used in this record. + */ + @JsonInclude(Include.NON_EMPTY) + private List conformsTo; + + /** + * A temporal extent associated with the resource described by this record. Can be null if there + * is no associated temporal extent. + */ + @JsonInclude(Include.NON_EMPTY) + private OgcApiTime time; + + /** + * A list of links related to this record. + */ + @JsonProperty(value = "links") + @JsonInclude(Include.NON_EMPTY) + private List links; + + /** + * A list of link templates related to this record. + */ + @JsonProperty(value = "linkTemplates") + @JsonInclude(Include.NON_EMPTY) + private List linkTemplates; + + private String type = "Feature"; + + + /** + * Extra properties. Can have any properties you want. However, OGCAPI-Records defines; + * + *

properties.created - The date this record was created in the server. + * + *

properties.updated - The most recent date on which the record was changed. + * + *

properties.language - The language used for + * textual values (i.e. titles, descriptions, etc.) of this record. + * + *

properties.languages - The list of other languages in which this record is available. + * + *

properties.type - The nature or genre of the resource described by this record. + * + *

properties.title - A human-readable name given to the resource described by this record. + * + *

properties.description - A free-text description of the resource described by this record. + * + *

properties.keywords - A list of free-form keywords or tags associated with the resource + * described by this record. + * + *

properties.themes - A knowledge organization system used to classify the resource + * described by this resource. + * + *

properties.resourceLanguages - The list of languages in which the resource described + * by this record can be retrieved. + * + *

properties.externalIds - One or more identifiers, assigned by an + * external entity, for the resource described by this record. + * + *

properties.formats - A list of + * available distributions for the resource described by this record. + * + *

properties.contacts - A list of contacts qualified by their role(s) in association to + * the record or the resource described by this record. + * + *

properties.license - The legal provisions under which the resource + * described by this record is made available. NOTE: special format (see spec) + * + *

properties.rights - A statement that concerns all rights not addressed by the license + * such as a copyright statement. + */ + @JsonProperty(value = "properties") + private Map properties; + + public OgcApiGeoJsonRecord() { + properties = new LinkedHashMap<>(); + } + + /** + * sets a named property in the #properties object. + * @param propertyName which property to set? + * @param value value of the property. + */ + public void setProperty(String propertyName, Object value) { + if (value != null) { + properties.put(propertyName, value); + } + } +} diff --git a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/GeoJsonPolygon.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/GeoJsonPolygon.java new file mode 100644 index 00000000..84836c99 --- /dev/null +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/GeoJsonPolygon.java @@ -0,0 +1,53 @@ +/** + * (c) 2024 Open Source Geospatial Foundation - all rights reserved This code is licensed under the + * GPL 2.0 license, available at the root application directory. + */ + +package org.fao.geonet.ogcapi.records.model; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * represents a GeoJSON polygon. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class GeoJsonPolygon { + + public String type = "Polygon"; + + public List>> coordinates = new ArrayList<>(); + + /** + * builder to construct a polygon from a bounding box. + * @param bbox bounding box - xmin, ymin, xmax, ymax + * @return GeoJsonPolygon with a rectangular polygon representing the bounding box. + */ + public static GeoJsonPolygon fromBBox(List bbox) { + var result = new GeoJsonPolygon(); + result.addCoord(bbox.get(0), bbox.get(1)); //xmin, ymin + result.addCoord(bbox.get(2), bbox.get(1)); //xmax, ymin + result.addCoord(bbox.get(2), bbox.get(3)); //xmax, ymax + result.addCoord(bbox.get(0), bbox.get(3)); //xmin, ymax + result.addCoord(bbox.get(0), bbox.get(1)); //xmin, ymin + return result; + } + + /** + * helper function to add a point to the polygon. + * @param c1 coordinate 1 (x) + * @param c2 coordinate 2 (y) + */ + public void addCoord(Double c1, Double c2) { + if (coordinates.isEmpty()) { + coordinates.add(new ArrayList<>()); + } + coordinates.get(0).add(Arrays.asList(c1, c2)); + } +} diff --git a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiLinkTemplate.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiLinkTemplate.java new file mode 100644 index 00000000..03351eff --- /dev/null +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiLinkTemplate.java @@ -0,0 +1,39 @@ +/** + * (c) 2024 Open Source Geospatial Foundation - all rights reserved This code is licensed under the + * GPL 2.0 license, available at the root application directory. + */ + +package org.fao.geonet.ogcapi.records.model; + +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * OGCAPI link Template. https://github.com/opengeospatial/ogcapi-records/blob/master/core/openapi/schemas/linkTemplate.yaml + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OgcApiLinkTemplate extends OgcApiLink { + + /** + * Supplies a resolvable URI to a remote resource (or resource fragment). example: + * http://data.example.com/buildings/(building-id} + */ + private String uriTemplate; + + /** + * The base URI to which the variable name can be appended to retrieve the definition of the + * variable as a JSON Schema fragment. format: uri + */ + private String varBase; + + /** + * This object contains one key per substitution variable in the templated URL. Each key defines + * the schema of one substitution variable using a JSON Schema fragment and can thus include + * things like the data type of the variable, enumerations, minimum values, maximum values, etc. + */ + private Map variables; +} diff --git a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiTime.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiTime.java new file mode 100644 index 00000000..40852560 --- /dev/null +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiTime.java @@ -0,0 +1,72 @@ +package org.fao.geonet.ogcapi.records.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import java.util.ArrayList; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +/** + * OGCAPI Time. https://github.com/opengeospatial/ogcapi-records/blob/master/core/openapi/schemas/time.yaml + * + *

see 8.2.7. Temporal information (ogcapi records spec). + */ +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +public class OgcApiTime { + + /** + * type: string. pattern: "^\\d{4}-\\d{2}-\\d{2}$" + */ + @JsonInclude(Include.NON_EMPTY) + private String date; + + /** + * type: string. pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?Z$" + */ + @JsonInclude(Include.NON_EMPTY) + private String timestamp; + + /** + * Interval. minItems: 2 maxItems: 2 items: oneOf: - type: string pattern: + * "^\\d{4}-\\d{2}-\\d{2}$" - type: string + * pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?Z$" + * - type: string enum: - ".." + */ + @JsonInclude(Include.NON_EMPTY) + private List interval; + + /** + * Minimum time period resolvable in the dataset, as an ISO 8601 duration. example: - 'P1D' + */ + @JsonInclude(Include.NON_EMPTY) + private String resolution; + + /** + * Set the interval given a set of interval datetimes (1st=start,2nd=end). + * If the intervals are null, use "..". See ogcapi spec for intervals. + * @param interval list of datetime objects. Should be exactly 2, can be nulls. + */ + public void setInterval(List interval) { + if (interval.size() != 2) { + return; + } + this.interval = new ArrayList<>(); + + if (interval.get(0) != null) { + this.interval.add(interval.get(0)); + } else { + this.interval.add(".."); + } + if (interval.get(1) != null) { + this.interval.add(interval.get(1)); + } else { + this.interval.add(".."); + } + } +} diff --git a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/service/RecordService.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/service/RecordService.java index 4231816e..b56fe89c 100644 --- a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/service/RecordService.java +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/service/RecordService.java @@ -161,7 +161,7 @@ public JsonNode getRecordAsJson( JsonNode totalValue = "json".equals(type) ? actualObj.get("hits").get("total").get("value") - : actualObj.get("size"); + : actualObj.get("numberMatched"); if ((totalValue == null) || (totalValue.intValue() == 0)) { throw new ResponseStatusException(HttpStatus.NOT_FOUND, diff --git a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/util/ElasticIndexJson2CollectionInfo.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/util/ElasticIndexJson2CollectionInfo.java index 13bfc117..68a8ba88 100644 --- a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/util/ElasticIndexJson2CollectionInfo.java +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/util/ElasticIndexJson2CollectionInfo.java @@ -69,6 +69,10 @@ public void injectLinkedServiceRecordInfo(CollectionInfo collectionInfo, return; } + if (collectionInfo.getId() == null) { + set(getId(collectionInfo, indexRecord), collectionInfo, "id"); + } + set(getTitle(collectionInfo, indexRecord), collectionInfo, "title"); set(getDescription(collectionInfo, indexRecord), collectionInfo, "description"); @@ -174,6 +178,14 @@ private ArrayList getOtherLangs(CollectionInfo collectionInfo, return null; } + //process id + private String getId(CollectionInfo collectionInfo, + IndexRecord indexRecord) { + var id = indexRecord.getMetadataIdentifier(); + return id; + } + + //process main language private OgcApiLanguage getLanguage(CollectionInfo collectionInfo, IndexRecord indexRecord) { @@ -245,10 +257,10 @@ private void handleExtent(CollectionInfo collectionInfo, OgcApiSpatialExtent spa private OgcApiTemporalExtent getTemporalExtent(IndexRecord indexRecord) { OgcApiTemporalExtent temporalExtent = null; var myTemporalExtent = indexRecord.getResourceTemporalExtentDateRange(); - if (myTemporalExtent != null || myTemporalExtent.isEmpty()) { + if (myTemporalExtent == null || myTemporalExtent.isEmpty()) { myTemporalExtent = indexRecord.getResourceTemporalDateRange(); } - if (myTemporalExtent != null || myTemporalExtent.isEmpty()) { + if (myTemporalExtent != null && !myTemporalExtent.isEmpty()) { temporalExtent = OgcApiTemporalExtent.fromGnIndexRecord(myTemporalExtent.get(0)); } return temporalExtent; @@ -259,14 +271,8 @@ private OgcApiSpatialExtent getSpatialExtent(IndexRecord indexRecord) { OgcApiSpatialExtent spatialExtent = null; var mySpatialExtent = indexRecord.getGeometries(); if (mySpatialExtent != null && !mySpatialExtent.isEmpty()) { - Map map = null; - try { - map = new org.codehaus.jackson.map.ObjectMapper().readValue(mySpatialExtent.get(0), - TypeFactory.mapType(HashMap.class, String.class, Object.class)); - } catch (IOException e) { - return null; - } - spatialExtent = OgcApiSpatialExtent.fromGnIndexRecord(map); + spatialExtent = OgcApiSpatialExtent.fromGnIndexRecord( + (Map) mySpatialExtent.get(0)); } return spatialExtent; } diff --git a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/util/RecordsEsQueryBuilder.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/util/RecordsEsQueryBuilder.java index 6ec63872..91e92942 100644 --- a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/util/RecordsEsQueryBuilder.java +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/util/RecordsEsQueryBuilder.java @@ -32,6 +32,7 @@ @Slf4j(topic = "org.fao.geonet.ogcapi") public class RecordsEsQueryBuilder { + private static final String SORT_BY_SEPARATOR = ","; // TODO: Sources depends on output type private static final List defaultSources = Arrays.asList( @@ -42,13 +43,16 @@ public class RecordsEsQueryBuilder { "contact", "contactForResource", "cl_status", "edit", "tag", "changeDate", - "createDate", "mainLanguage", "geom", "formats", "resourceTemporalDateRange"); + "createDate", "mainLanguage", "geom", "formats", + "resourceTemporalDateRange","resourceTemporalExtentDateRange"); + private static final String defaultSpatialOperation = "intersects"; @Autowired QueryToElastic queryToElastic; @Autowired private SearchConfiguration configuration; + public RecordsEsQueryBuilder(SearchConfiguration configuration) { this.configuration = configuration; }