From 51fbde4f860e214002d5585b477c5047fff933b1 Mon Sep 17 00:00:00 2001 From: "david.blasby" Date: Tue, 1 Oct 2024 10:36:07 -0700 Subject: [PATCH] initial queryables implementation --- .../controller/QueryableApiController.java | 63 +++++++ .../geonet/ogcapi/records/model/JsonItem.java | 30 ++++ .../ogcapi/records/model/JsonProperty.java | 155 ++++++++++++++++++ .../ogcapi/records/model/JsonSchema.java | 116 +++++++++++++ .../records/service/QueryablesService.java | 124 ++++++++++++++ 5 files changed, 488 insertions(+) create mode 100644 modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/controller/QueryableApiController.java create mode 100644 modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/JsonItem.java create mode 100644 modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/JsonProperty.java create mode 100644 modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/JsonSchema.java create mode 100644 modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/service/QueryablesService.java diff --git a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/controller/QueryableApiController.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/controller/QueryableApiController.java new file mode 100644 index 00000000..7accc200 --- /dev/null +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/controller/QueryableApiController.java @@ -0,0 +1,63 @@ +package org.fao.geonet.ogcapi.records.controller; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiParam; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.fao.geonet.ogcapi.records.model.JsonSchema; +import org.fao.geonet.ogcapi.records.service.QueryablesService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; +import springfox.documentation.annotations.ApiIgnore; + +/** + * See https://docs.ogc.org/is/19-079r2/19-079r2.html#rc_queryables and + * https://docs.ogc.org/DRAFTS/20-004.html#_queryables_link + */ +@Api(tags = "OGC API Records") +@Controller +@Slf4j(topic = "org.fao.geonet.ogcapi") +public class QueryableApiController { + + @Autowired + QueryablesService queryablesService; + + /** + * Describe queryables for a collection. + */ + @io.swagger.v3.oas.annotations.Operation( + summary = "Describes queryables for a collection.", + description = "Queryables resource for discovering a list of resource properties with their " + + "types and constraints that may be used to construct filter expressions" + + " on a collection of resources.") + @GetMapping(value = "/collections/{collectionId}/queryables", + produces = {MediaType.APPLICATION_JSON_VALUE, + }) + @ResponseStatus(HttpStatus.OK) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Describe queryables for a collection.") + }) + @ResponseBody + public ResponseEntity queryablesForCollection( + @ApiParam(value = "Identifier (name) of a specific collection", required = true) + @PathVariable("collectionId") String collectionId, + @ApiIgnore HttpServletRequest request, + @ApiIgnore HttpServletResponse response, + @ApiIgnore Model model) throws Exception { + + var jsonSchema = queryablesService.buildQueryables(collectionId); + + return ResponseEntity.ok(jsonSchema); + } +} diff --git a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/JsonItem.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/JsonItem.java new file mode 100644 index 00000000..4edbdf81 --- /dev/null +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/JsonItem.java @@ -0,0 +1,30 @@ +package org.fao.geonet.ogcapi.records.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; + +/** + * THIS IS SIMPLIFIED - SEE FULL SPECIFICATION. + * + *

See the JSON Schema specification. + */ +public class JsonItem { + + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "type") + @XmlElement(name = "type") + public String type; + + //--------------------------------------------- + + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } +} diff --git a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/JsonProperty.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/JsonProperty.java new file mode 100644 index 00000000..0aa2f962 --- /dev/null +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/JsonProperty.java @@ -0,0 +1,155 @@ +package org.fao.geonet.ogcapi.records.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import java.util.List; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; + +/** + * This is for Queryables. Its a json schema. cf https://json-schema.org/draft/2020-12/schema + * + *

https://json-schema.org/learn/miscellaneous-examples + * + *

example: https://demo.pycsw.org/gisdata/collections/metadata:main/queryables?f=json + * + *

THIS IS SIMPLIFIED - SEE FULL SPECIFICATION and JsonItem + */ +public class JsonProperty { + + public static final String TypeString = "string"; + + //---------------------- + + /** + * https://json-schema.org/draft/2020-12/schema. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "title") + @XmlElement(name = "title") + public String title; + + /** + * https://json-schema.org/draft/2020-12/schema. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "type") + @XmlElement(name = "type") + public String type; + + /** + * https://json-schema.org/draft/2020-12/schema. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "description") + @XmlElement(name = "description") + public String description; + + /** + * cf. https://docs.ogc.org/is/19-079r2/19-079r2.html. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "format") + @XmlElement(name = "format") + public String format; + + /** + * cf. https://docs.ogc.org/is/19-079r2/19-079r2.html. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "enum") + @XmlElement(name = "enum") + @com.fasterxml.jackson.annotation.JsonProperty("enum") + public List enumeration; + + /** + * cf. https://docs.ogc.org/is/19-079r2/19-079r2.html. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "x-ogc-role") + @XmlElement(name = "x-ogc-role") + @com.fasterxml.jackson.annotation.JsonProperty("x-ogc-role") + public String xxOgcRole; + + /** + * cf. https://docs.ogc.org/is/19-079r2/19-079r2.html. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "items") + @XmlElement(name = "items") + public JsonItem items; + + //---------------------------------- + + /** + * builds a minimal JsonProperty (part of json schema). + * + * @param type type of the property + * @param title title of the property + * @param description description of the property + */ + public JsonProperty(String type, String title, String description) { + this.type = type; + this.title = title; + this.description = description; + } + + //---------------------------------- + + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getFormat() { + return format; + } + + public void setFormat(String format) { + this.format = format; + } + + public List getEnum() { + return enumeration; + } + + public void setEnum(List enumeration) { + this.enumeration = enumeration; + } + + public String getxOgcRole() { + return xxOgcRole; + } + + public void setxOgcRole(String xxOgcRole) { + this.xxOgcRole = xxOgcRole; + } + + public JsonItem getItems() { + return items; + } + + public void setItems(JsonItem items) { + this.items = items; + } +} diff --git a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/JsonSchema.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/JsonSchema.java new file mode 100644 index 00000000..fad8602f --- /dev/null +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/JsonSchema.java @@ -0,0 +1,116 @@ +package org.fao.geonet.ogcapi.records.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import java.util.Map; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; + +/** + * This is for Queryables. Its a json schema. cf https://json-schema.org/draft/2020-12/schema + * + *

example: https://demo.pycsw.org/gisdata/collections/metadata:main/queryables?f=json + */ +public class JsonSchema { + + /** + * The property $schema is https://json-schema.org/draft/2020-12/schema. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "$schema") + @XmlElement(name = "$schema") + @com.fasterxml.jackson.annotation.JsonProperty("$schema") + public String schema = "https://json-schema.org/draft/2020-12/schema"; + + /** + * The type is object and each property is a queryable. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "type") + @XmlElement(name = "type") + public String type = "object"; + + /** + * The property $id is the URI of the resource without query parameters. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "id") + @XmlElement(name = "id") + public String id; + + /** + * The property $id is the URI of the resource without query parameters. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "title") + @XmlElement(name = "title") + public String title = "Queryables for GeoNetwork Collection"; + + /** + * https://json-schema.org/draft/2020-12/schema. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "description") + @XmlElement(name = "description") + public String description; + + + /** + * The properties for this schema. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "properties") + @XmlElement(name = "properties") + public Map properties; + + //---------------------------------------------- + + + public String getSchema() { + return schema; + } + + public void setSchema(String schema) { + this.schema = schema; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Map getProperties() { + return properties; + } + + public void setProperties(Map properties) { + this.properties = properties; + } +} diff --git a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/service/QueryablesService.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/service/QueryablesService.java new file mode 100644 index 00000000..44e40fb1 --- /dev/null +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/service/QueryablesService.java @@ -0,0 +1,124 @@ +package org.fao.geonet.ogcapi.records.service; + +import java.util.LinkedHashMap; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.fao.geonet.ogcapi.records.model.JsonProperty; +import org.fao.geonet.ogcapi.records.model.JsonSchema; +import org.springframework.stereotype.Service; + +/** + * Basic Service to handle Queryables according to the OGCAPI spec. + */ +@Service +@Slf4j(topic = "org.fao.geonet.ogcapi.records") +public class QueryablesService { + + /** + * build a schema based on collection. It will be based on the underlying elastic index json. + * + *

NOTE: these are hard coded at the moment. + * + * @param collectionId which collection + * @return schema based on collection + */ + public JsonSchema buildQueryables(String collectionId) { + var jsonSchema = new JsonSchema(); + + Map properties = new LinkedHashMap<>(); + jsonSchema.setProperties(properties); + addStandardProperties(properties); + + return jsonSchema; + } + + /** + * cf. https://docs.ogc.org/DRAFTS/20-004.html + * + *

The only mandatory one is "id". + * + * @param properties existing set of properties to add to. + */ + public void addStandardProperties(Map properties) { + + JsonProperty p; + //table 8 + p = new JsonProperty(JsonProperty.TypeString, "id", + "A unique record identifier assigned by the server."); + p.setxOgcRole("id"); + properties.put("id", p); + + p = new JsonProperty(JsonProperty.TypeString, "created", + "The date this record was created in the server."); + properties.put("created", p); + + p = new JsonProperty(JsonProperty.TypeString, "updated", + "The most recent date on which the record was changed."); + properties.put("updated", p); + + //conformsTo -- not in Elastic Index JSON + + p = new JsonProperty(JsonProperty.TypeString, "language", + "The language used for textual values (i.e. titles, descriptions, etc.)" + + " of this record."); + properties.put("language", p); + + p = new JsonProperty(JsonProperty.TypeString, "languages", + "The list of other languages in which this record is available."); + properties.put("languages", p); + + //links -- not in Elastic Index JSON + //linkTemplates -- not in Elastic Index JSON + + //table 9 + + //unclear what this maps to in elastic + p = new JsonProperty(JsonProperty.TypeString, "title", + "The nature or genre of the resource described by this record."); + properties.put("type", p); + + p = new JsonProperty(JsonProperty.TypeString, "title", + "A human-readable name given to the resource described by this record."); + properties.put("title", p); + + p = new JsonProperty(JsonProperty.TypeString, "description", + "A free-text description of the resource described by this record."); + properties.put("description", p); + + p = new JsonProperty(JsonProperty.TypeString, "geometry", + "A spatial extent associated with the resource described by this record."); + properties.put("geometry", p); + + p = new JsonProperty(JsonProperty.TypeString, "time", + "A temporal extent associated with the resource described by this record."); + properties.put("time", p); + + p = new JsonProperty(JsonProperty.TypeString, "keywords", + "A list of free-form keywords or tags associated with the resource" + + " described by this record."); + properties.put("keywords", p); + + p = new JsonProperty(JsonProperty.TypeString, "themes", + "A knowledge organization system used to classify the resource" + + " described by this resource."); + properties.put("themes", p); + + p = new JsonProperty(JsonProperty.TypeString, "contacts", + "A list of contacts qualified by their role(s) in association to the record" + + " or the resource described by this record."); + properties.put("contacts", p); + + //resourceLanguages -- not in Elastic Index JSON + //externalIds -- not in Elastic Index JSON + //formats -- not in Elastic Index JSON + + p = new JsonProperty(JsonProperty.TypeString, "license", + "The legal provisions under which the resource described by this record" + + " is made available."); + properties.put("license", p); + + //rights -- not in Elastic Index JSON + + } + +}