diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/manual/README.md b/docs/manual/README.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/manual/docs/index.md b/docs/manual/docs/index.md new file mode 100644 index 00000000..86de52fd --- /dev/null +++ b/docs/manual/docs/index.md @@ -0,0 +1,17 @@ +--- +hide: + - navigation +--- + +# GeoNetwork MicroServices {#toc} + +Welcome to GeoNetwork MicroServices. + +
+ +- :fontawesome-solid-signs-post: [OGCAPI-Records](ogcapi/index.md) + + --- + + Implementation of OGCAPI-Records. +
\ No newline at end of file diff --git a/docs/manual/docs/ogcapi/catalog.md b/docs/manual/docs/ogcapi/catalog.md new file mode 100644 index 00000000..199a540c --- /dev/null +++ b/docs/manual/docs/ogcapi/catalog.md @@ -0,0 +1,41 @@ +# Catalog.yaml definition {#toc} + + +See [Catalog Definition in the OGCAPI specification](https://docs.ogc.org/DRAFTS/20-004.html#req_records-api_catalog-response). + +The properties come from the source's (portal sub-portal) in the GeoNetwork "sources" table, column "serviceRecord". +This can be configured in the `GN GUI: Admin Console-Settings-sources` for sub-portals. + +There is no GUI to set this for the "main" portal - do this in GN GUI: Admin Console-Settings-Settings-Catalog Service for the Web (CSW)-Record to use for GetCapabilities (`system/csw/capabilityRecordUuid`). + + +|Property| Description | Elastic Index JSON Property | +|--------| ----------- | --------------------------- | +| id | A unique identifier for this catalog. | metadataIdentifier| +| created | The date this collection was created. | createDate| +| updated | The more recent date on which this collection was changed. |changeDate | +| conformsTo | The extensions/conformance classes used in this catalog object. | `not used` | +| type | Fixed value of "Catalog". | always "Catalog" | +| itemType | Fixed value of "record", "catalog" or both. | always "record"| +| title | A human-readable name given to this catalog. | resourceTitleObject| +| description | A free-text description of this catalog. | resourceAbstractObject| +| extent | The spatiotemporal coverage of this catalog. | spatial: geom
temporal: resourceTemporalDateRange | +| crs | A list of coordinate reference systems used for spatiotemporal values. | coordinateSystem| +| keywords | A list of free-form keywords or tags associated with this collection. |tag | +themes | A knowledge organization system used to classify this collection. | allKeywords| +| language | The language used for textual values (i.e. titles, descriptions, etc.) of this collection object. |mainLanguage | +| languages | The list of other languages in which this collection object is available. |otherLanguage | +| recordLanguages | The list of languages in which records from the collection can be represented. |`not used` | +| contacts | A list of contacts qualified by their role(s). |contact | +| license | The legal provisions under which this collection is made available. |MD_LegalConstraintsUseLimitationObject | +| rights | A statement that concerns all rights not addressed by the license such as a copyright statement. | `not used`| +| recordsArrayName | The name of the array property in the catalog used to encode records in-line. The default value is records. | `not used`| +| records | An array of records encoded in-line in the catalog. |`not used` | +| links | A list of links related to this catalog. | `filled with Java code`| +| linkTemplates | A list of link templates related to this catalog. | `not used`| +| schemes | A list of schemes related to this catalog. |`not used`| + +See `ElasticIndexJson2CollectionInfo.java` for more details. + +NOTE: For `Contact.address`, we only fill in the `deliveryPoint` because that's all that's available in the Elastic Index JSON. + diff --git a/docs/manual/docs/ogcapi/conformance.md b/docs/manual/docs/ogcapi/conformance.md new file mode 100644 index 00000000..d377e007 --- /dev/null +++ b/docs/manual/docs/ogcapi/conformance.md @@ -0,0 +1,4 @@ +# Conformance {#toc} + + +See [Conformance in the OGCAPI specification](https://docs.ogc.org/DRAFTS/20-004.html#conformance_class). diff --git a/docs/manual/docs/ogcapi/index.md b/docs/manual/docs/ogcapi/index.md new file mode 100644 index 00000000..0703666f --- /dev/null +++ b/docs/manual/docs/ogcapi/index.md @@ -0,0 +1,15 @@ + +# GeoNetwork MicroServices OGCAPI-Records {#toc} + + +This service module implements the ([OGCAPI-Records](https://ogcapi.ogc.org/records)). + +| Endpoint Name | Endpoint Location | Meaning | +| -------- | ------- | -------- | +| [Landing Page](landingpage) | / | Home Page | +| [Conformance declaration](conformance) | /conformance | Conformance Documents | +| [OpenAPI Documentation](openapi) | /openapi | OpenAPI (Swagger) description document | +| [Record collections](record-collections) | /collections | List of Catalogs
(GN portals) | +| [Record collection](record-collection) | /collections/{collectionId} | Information about a single Catalog
(GN portal) | +| [Records](records) | /collections/{collectionId}/items | Records in a Catalog
Search, etc... | +| [Record](record) | /collections/{collectionId}/items/{recordId} | Single Metadata Record | \ No newline at end of file diff --git a/docs/manual/docs/ogcapi/landingpage.md b/docs/manual/docs/ogcapi/landingpage.md new file mode 100644 index 00000000..b79f840e --- /dev/null +++ b/docs/manual/docs/ogcapi/landingpage.md @@ -0,0 +1,26 @@ +# Landing Page {#toc} + +The Landing Page will show information about the server. It has links to the [Conformance declaration](conformance), the [OpenAPI Documentation](openapi), and the [Record collections](record-collections). + +See [Landing Page in the OGCAPI specification](https://docs.ogc.org/is/17-069r3/17-069r3.html#_api_landing_page). + + +## JSON Model + +The JSON model follows the ["landingPage.yml"](https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/schemas/landingPage.yaml) definition in the OGCAPI Specification. + +| JSON Tag| Meaning | +| -------- | ------- | +| title | Title for the server | +| description | Description of the server | +| links | Links to other documents | + + +The GeoNetwork OGCAPI-Records implementation also include a non-standard item: + +| JSON Tag| Meaning | +| -------- | ------- | +| systemInfo | Metadata for the Server. See the [catalog.yaml](../catalog) description. | + + +This metadata information is taken from the Service Record linked to the GeoNetwork's main portal. diff --git a/docs/manual/docs/ogcapi/openapi.md b/docs/manual/docs/ogcapi/openapi.md new file mode 100644 index 00000000..3c229492 --- /dev/null +++ b/docs/manual/docs/ogcapi/openapi.md @@ -0,0 +1,4 @@ +# OpenApi Description Document {#toc} + +This is the [OpenApi (Swagger) Description Document](https://swagger.io/). + diff --git a/docs/manual/docs/ogcapi/record-collection.md b/docs/manual/docs/ogcapi/record-collection.md new file mode 100644 index 00000000..7c6d18a2 --- /dev/null +++ b/docs/manual/docs/ogcapi/record-collection.md @@ -0,0 +1,7 @@ +# Record Collection {#toc} + +See [Record Collection in the OGCAPI specification](https://docs.ogc.org/DRAFTS/20-004.html#clause-record-collection). + +##JSON Model + +See the [catalog.yaml](../catalog) description. \ No newline at end of file diff --git a/docs/manual/docs/ogcapi/record-collections.md b/docs/manual/docs/ogcapi/record-collections.md new file mode 100644 index 00000000..7dcfe581 --- /dev/null +++ b/docs/manual/docs/ogcapi/record-collections.md @@ -0,0 +1,11 @@ +# Record Collections {#toc} + +This gives a summary of all the Record Collections (Catalogs) on the Server. In GeoNetwork this is the main portal, plus any sub-portals described. + + +See [Record Collections in the OGCAPI specification](https://docs.ogc.org/DRAFTS/20-004.html#record-collections). + + +## JSON MODEL + +See the [catalog.yaml](../catalog) description. \ No newline at end of file diff --git a/docs/manual/docs/ogcapi/record.md b/docs/manual/docs/ogcapi/record.md new file mode 100644 index 00000000..52985c11 --- /dev/null +++ b/docs/manual/docs/ogcapi/record.md @@ -0,0 +1,6 @@ +# Record {#toc} + +See [Record in the OGCAPI specification](https://docs.ogc.org/DRAFTS/20-004.html#clause-record-core). + + + \ No newline at end of file diff --git a/docs/manual/docs/ogcapi/records.md b/docs/manual/docs/ogcapi/records.md new file mode 100644 index 00000000..0c259aeb --- /dev/null +++ b/docs/manual/docs/ogcapi/records.md @@ -0,0 +1,4 @@ +# Records {#toc} + +See [Records in the OGCAPI specification](https://docs.ogc.org/DRAFTS/20-004.html#clause-record-collection). + diff --git a/docs/manual/mkdocs.yml b/docs/manual/mkdocs.yml new file mode 100644 index 00000000..73c44668 --- /dev/null +++ b/docs/manual/mkdocs.yml @@ -0,0 +1,152 @@ +# Project information +site_name: GeoNetwork Opensource Microservices +site_description: GeoNetwork Opensource Microservices. +site_dir: target/html +site_url: https://docs.geonetwork-opensource.org/ + +# Repository +repo_name: geonetwork-microservices +repo_url: https://github.com/geonetwork/geonetwork-microservices +edit_uri: edit/main/docs/manual/docs + +# Copyright +copyright: Copyright © 2024 FAO-UN and others + +extra_css: + - assets/stylesheets/extra.css + +# Configuration +theme: + name: material + language: en + custom_dir: overrides + logo: assets/images/geonetwork-logo.svg + favicon: assets/images/geonetwork-logo.png + icon: + repo: fontawesome/brands/github + palette: + # Palette toggle for light mode + - media: "(prefers-color-scheme: light)" + scheme: default + primary: blue + toggle: + icon: material/weather-night + name: Switch to dark mode + # Palette toggle for dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + toggle: + icon: material/weather-sunny + name: Switch to light mode + features: + - toc.follow + - navigation.tracking + - navigation.top + - navigation.tabs + - navigation.prune + - navigation.indexes + - navigation.footer + - header.autohide + - content.tabs.link + - content.code.copy + - content.action.view + - content.action.edit + - announce.dismiss + +# Plugins - install using: pip3 install -r requirements.txt +plugins: + - exclude: + glob: + - annexes/gallery/bin/README.md + - i18n: + docs_structure: suffix + reconfigure_material: true + languages: + - locale: en + name: English + build: true + default: true + site_name: 'GeoNetwork MicroServices OpenSource' + - locale: fr + name: Français + build: !ENV [FRENCH,true] + site_name: 'GeoNetwork MicroServices OpenSource' + site_description: Catalogue GeoNetwork pour répertorier, rechercher et examiner les enregistrements. + nav_translations: + Home: Home + Search: Search + Record: Record + Map: Map + - search + +# Customizations +extra: + version: + provider: mike + default: stable + alias: true + homepage: https://geonetwork-opensource.org/ + social: + - icon: fontawesome/brands/github + link: https://github.com/geonetwork + - icon: fontawesome/brands/docker + link: https://hub.docker.com/_/geonetwork + - icon: geonetwork/logo_bw + link: https://geonetwork-opensource.org/ + name: GeoNetwork Website + +# For use with --strict to produce failures on build warnings +validation: + nav: + omitted_files: warn + not_found: warn + absolute_links: warn + links: + not_found: warn + absolute_links: warn + unrecognized_links: warn + +# Extensions +# - These are carefully chosen to work with pandoc markdown support for whole document translation +markdown_extensions: + - admonition + - attr_list + - def_list + - pymdownx.details + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + options: + custom_icons: + - overrides/.icons + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.keys + - pymdownx.smartsymbols + - pymdownx.snippets + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format + - pymdownx.tabbed: + alternate_style: true + - tables + - md_in_html + +# Page tree +nav: + - 'GeoNetwork MicroServices': index.md + - 'OGCAPI-Records': + - ogcapi/index.md + - ogcapi/landingpage.md + - ogcapi/conformance.md + - ogcapi/openapi.md + - ogcapi/record-collections.md + - ogcapi/record-collection.md + - ogcapi/records.md + - ogcapi/record.md + diff --git a/docs/manual/overrides/.icons/geonetwork/logo.svg b/docs/manual/overrides/.icons/geonetwork/logo.svg new file mode 100644 index 00000000..d1ebaec7 --- /dev/null +++ b/docs/manual/overrides/.icons/geonetwork/logo.svg @@ -0,0 +1,28 @@ + + + + gn_logo + Created with Sketch. + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/manual/overrides/.icons/geonetwork/logo_bw.svg b/docs/manual/overrides/.icons/geonetwork/logo_bw.svg new file mode 100644 index 00000000..61c0c2d9 --- /dev/null +++ b/docs/manual/overrides/.icons/geonetwork/logo_bw.svg @@ -0,0 +1,15 @@ + + + + gn_logo + Created with Sketch. + + + + diff --git a/docs/manual/overrides/assets/images/geonetwork-logo.png b/docs/manual/overrides/assets/images/geonetwork-logo.png new file mode 100644 index 00000000..f8926bf7 Binary files /dev/null and b/docs/manual/overrides/assets/images/geonetwork-logo.png differ diff --git a/docs/manual/overrides/assets/images/geonetwork-logo.svg b/docs/manual/overrides/assets/images/geonetwork-logo.svg new file mode 100644 index 00000000..73bb4876 --- /dev/null +++ b/docs/manual/overrides/assets/images/geonetwork-logo.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + diff --git a/docs/manual/overrides/assets/stylesheets/extra.css b/docs/manual/overrides/assets/stylesheets/extra.css new file mode 100644 index 00000000..07b39e34 --- /dev/null +++ b/docs/manual/overrides/assets/stylesheets/extra.css @@ -0,0 +1,79 @@ + +img + em, .browser-border + em, .browser-mockup + em { + display: block; + text-align: left; + font-size: 0.7rem; + font-style: normal; +} +.md-typeset .admonition, .md-typeset details { + font-size: 0.75rem; +} + +/* definition list used to display general inputs */ +.md-typeset dl dd { + margin: 10px 0; +} +[dir=ltr] .md-typeset dd { + margin-left: 0; +} +.md-typeset dl dd code { + border: 1px solid rgba(230, 230, 230, 0.7); + padding: 8px 12px; +} + +.browser-border { + border: 1px solid rgba(230, 230, 230, 0.7); + border-radius: 3px; + background: #fff; + margin: 10px 0 5px 0; +} +.browser-mockup { + border: 1px solid rgba(230, 230, 230, 0.7); + border-top: 2em solid rgba(230, 230, 230, 0.7); + /*box-shadow: 0 0.1em 1em 0 rgba(0, 0, 0, 0.4);*/ + position: relative; + border-radius: 3px; + margin: 10px 0 5px 0; +} + +.browser-mockup:before { + display: block; + position: absolute; + content: ''; + top: -1.25em; + left: 1em; + width: 0.5em; + height: 0.5em; + border-radius: 50%; + background-color: #f44; + box-shadow: 0 0 0 2px #f44, 1.5em 0 0 2px #9b3, 3em 0 0 2px #fb5; +} + +.browser-mockup.with-tab:after { + display: block; + position: absolute; + content: ''; + top: -2em; + left: 5.5em; + width: 20%; + height: 0; + border-bottom: 2em solid white; + border-left: 0.8em solid transparent; + border-right: 0.8em solid transparent; +} + +.browser-mockup.with-url:after { + display: block; + position: absolute; + content: ''; + top: -1.6em; + left: 5.5em; + width: calc(100% - 6em); + height: 1.2em; + border-radius: 2px; + background-color: white; +} + +.browser-mockup > * { + display: block; +} diff --git a/docs/manual/overrides/main.html b/docs/manual/overrides/main.html new file mode 100644 index 00000000..f2432066 --- /dev/null +++ b/docs/manual/overrides/main.html @@ -0,0 +1,6 @@ +{% extends "base.html" %} + +{% block outdated %} + GeoNetwork 4.4 is latest. + Click here to go to stable. +{% endblock %} diff --git a/docs/manual/overrides/partials/copyright.html b/docs/manual/overrides/partials/copyright.html new file mode 100755 index 00000000..40f52ada --- /dev/null +++ b/docs/manual/overrides/partials/copyright.html @@ -0,0 +1,41 @@ + + + + diff --git a/docs/manual/pom.xml b/docs/manual/pom.xml new file mode 100644 index 00000000..e74e55a0 --- /dev/null +++ b/docs/manual/pom.xml @@ -0,0 +1,108 @@ + + + + + gn-ms-docs + org.geonetwork-opensource.cloud + 4.4.5-0 + + 4.0.0 + gn-ms-guide + GeoNetwork MicroServices Guide + pom + + + Creative Commons Attribution 3.0 License + http://creativecommons.org/licenses/by/3.0/ + repo + + + + + + + maven-antrun-plugin + 3.0.0 + + + + docs + compile + + run + + + + + + + + + + + + + + + + maven-assembly-plugin + 3.3.0 + + + make-assembly + package + + single + + + + + src/assembly/guide.xml + + + + + + + + maven-deploy-plugin + + true + + + + + maven-install-plugin + + true + + + + + + + + \ No newline at end of file diff --git a/docs/manual/requirements.txt b/docs/manual/requirements.txt new file mode 100644 index 00000000..f9aaca20 --- /dev/null +++ b/docs/manual/requirements.txt @@ -0,0 +1,5 @@ +mkdocs-material>=9.5.3 +mkdocs-static-i18n>=1.0.5 +mkdocs-include-markdown-plugin +mkdocs-exclude +mike==2.0.0 \ No newline at end of file diff --git a/docs/manual/src/assembly/guide.xml b/docs/manual/src/assembly/guide.xml new file mode 100644 index 00000000..58394d44 --- /dev/null +++ b/docs/manual/src/assembly/guide.xml @@ -0,0 +1,13 @@ + + manual + + zip + + + + target/html + + + + + diff --git a/docs/pom.xml b/docs/pom.xml new file mode 100644 index 00000000..1d41005e --- /dev/null +++ b/docs/pom.xml @@ -0,0 +1,45 @@ + + + + + gn-cloud-microservices + org.geonetwork-opensource.cloud + 4.4.5-0 + + 4.0.0 + gn-ms-docs + Root Documentation module + pom + + + General Public License (GPL) + http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt + + + + manual + + \ No newline at end of file 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 0fe1caa4..1f09d24d 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 @@ -52,6 +52,11 @@ public class IndexRecord extends IndexDocument { List> resourceCredit = new ArrayList<>(); @JsonProperty(IndexRecordFieldNames.tag) ArrayList> tag = new ArrayList<>(); + + Map allKeywords = new HashMap<>(); + + @JsonProperty("MD_LegalConstraintsUseLimitationObject") + ArrayList> mdLegalConstraintsUseLimitationObject = new ArrayList<>(); private Integer internalId; private String metadataIdentifier; private IndexDocumentType docType; diff --git a/modules/library/common-index-model/src/main/java/org/fao/geonet/index/model/gn/Link.java b/modules/library/common-index-model/src/main/java/org/fao/geonet/index/model/gn/Link.java index 68dfa15e..2a903394 100644 --- a/modules/library/common-index-model/src/main/java/org/fao/geonet/index/model/gn/Link.java +++ b/modules/library/common-index-model/src/main/java/org/fao/geonet/index/model/gn/Link.java @@ -28,4 +28,7 @@ public class Link { private String group; private String mimeType; private String nilReason; + + private String hash; + private String idx; } diff --git a/modules/library/common-index-model/src/main/java/org/fao/geonet/index/model/gn/Theme.java b/modules/library/common-index-model/src/main/java/org/fao/geonet/index/model/gn/Theme.java new file mode 100644 index 00000000..06c670dc --- /dev/null +++ b/modules/library/common-index-model/src/main/java/org/fao/geonet/index/model/gn/Theme.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.index.model.gn; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlRootElement; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * represents a theme (inside the elastic json allKeywords). + * + *

"allKeywords": { + * "th_inspire-service-taxonomy": { + * "id": "geonetwork.thesaurus.external.theme.inspire-service-taxonomy", + * "theme": "theme", + * "link": "http://localhost:8080/geonetwork/srv/api/registries/vocabularies/external.theme.inspire-service-taxonomy", + * "keywords": [ + * { + * "default": "GEONETWORK", + * "langeng": "GEONETWORK" + * }, + * { + * "default": "OGCAPI", + * "langeng": "OGCAPI" + * } + * ] + * } + * } + * + */ +@Data +@EqualsAndHashCode() +@XmlRootElement(name = "indexRecord") +@XmlAccessorType(XmlAccessType.FIELD) +@JsonIgnoreProperties(ignoreUnknown = true) +public class Theme { + public String id; + public String title; + public String theme; + public String link; + public List> keywords; + public HashMap multilingualTitle; + +} 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 af9d3f67..f13e14df 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 @@ -66,7 +66,7 @@ public void testJsonToPojo() throws IOException { record.resourceTitle.get(defaultText) ); - Assert.assertEquals(49, record.getOtherProperties().size()); + Assert.assertEquals(48, record.getOtherProperties().size()); Assert.assertEquals("gmd:MD_Metadata", record.getRoot()); 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 1723f829..564c1172 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 @@ -10,9 +10,12 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cache.annotation.EnableCaching; import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @SpringBootApplication @@ -27,4 +30,18 @@ public static void main(String[] args) { SpringApplication.run(OgcApiRecordApp.class, args); } + + /** + * Configure CORS to allow all connections. + * @return CORS configuration. + */ + @Bean + public WebMvcConfigurer corsConfigurer() { + return new WebMvcConfigurer() { + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**").allowedOrigins("*"); + } + }; + } } diff --git a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/controller/CapabilitiesApiController.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/controller/CapabilitiesApiController.java index 12395b73..1fb0324c 100644 --- a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/controller/CapabilitiesApiController.java +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/controller/CapabilitiesApiController.java @@ -16,10 +16,11 @@ import org.fao.geonet.common.search.SearchConfiguration.Operations; import org.fao.geonet.domain.Source; import org.fao.geonet.domain.SourceType; +import org.fao.geonet.ogcapi.records.controller.model.CollectionInfo; import org.fao.geonet.ogcapi.records.controller.model.Conformance; import org.fao.geonet.ogcapi.records.controller.model.Content; -import org.fao.geonet.ogcapi.records.controller.model.Link; import org.fao.geonet.ogcapi.records.controller.model.Root; +import org.fao.geonet.ogcapi.records.model.OgcApiLink; import org.fao.geonet.ogcapi.records.model.XsltModel; import org.fao.geonet.ogcapi.records.util.CollectionInfoBuilder; import org.fao.geonet.ogcapi.records.util.LinksItemsBuilder; @@ -67,19 +68,17 @@ public class CapabilitiesApiController { @Autowired ConcurrentMapCacheManager cacheManager; - + @Autowired + MediaTypeUtil mediaTypeUtil; + @Autowired + CollectionInfoBuilder collectionInfoBuilder; @Autowired private SourceRepository sourceRepository; - @Autowired private SearchConfiguration configuration; - @Autowired - MediaTypeUtil mediaTypeUtil; - /** * Landing page end-point. - * */ @io.swagger.v3.oas.annotations.Operation( summary = "Landing page.", @@ -111,16 +110,37 @@ public ResponseEntity getLandingPage(@ApiIgnore HttpServletRequest request String label = source.getLabel("eng"); root.title(StringUtils.isEmpty(label) ? source.getName() : label); root.description(""); + + Locale locale = LocaleContextHolder.getLocale(); + + var language = locale.getISO3Language(); + + CollectionInfo collectionInfo = collectionInfoBuilder + .buildFromSource(source, language, requestBaseUrl, + configuration.getFormat(mediaType), configuration, request); + if (collectionInfo != null) { + //put title and description in the main obj + if (StringUtils.hasText(collectionInfo.getTitle())) { + root.setTitle(collectionInfo.getTitle()); + } + if (StringUtils.hasText(collectionInfo.getDescription())) { + root.setDescription(collectionInfo.getDescription()); + } + } + root.setSystemInfo(collectionInfo); } - root.addLinksItem(new Link() + root.addLinksItem(new OgcApiLink() .href(requestBaseUrl) - .rel("self").type(MediaType.APPLICATION_JSON.toString())); + .rel("self") + .type(MediaType.APPLICATION_JSON.toString())); - configuration.getFormats(Operations.root).forEach(f -> root.addLinksItem(new Link() - .href(requestBaseUrl + "collections?f=" + f.getName()) - .type("Catalogue collections") - .rel("self").type(f.getMimeType()))); + configuration.getFormats(Operations.root).forEach(f -> + root.addLinksItem(new OgcApiLink() + .href(requestBaseUrl + "collections?f=" + f.getName()) + .type("Catalogue collections") + .rel("self") + .type(f.getMimeType()))); addOpenApiLinks(root, requestBaseUrl); addConformanceLinks(root, requestBaseUrl); @@ -145,12 +165,12 @@ public ResponseEntity getLandingPage(@ApiIgnore HttpServletRequest request private void addOpenApiLinks(Root root, String baseUrl) { String title = "The OpenAPI Documentation"; - root.addLinksItem(new Link() + root.addLinksItem(new OgcApiLink() .href(baseUrl + "openapi") .title(title) .rel("service-doc").type(MediaType.TEXT_HTML_VALUE)); - root.addLinksItem(new Link() + root.addLinksItem(new OgcApiLink() .href(baseUrl + "openapi?f=json") .title(title + " as JSON") .rel("service-desc").type(MediaType.APPLICATION_JSON_VALUE)); @@ -158,15 +178,18 @@ private void addOpenApiLinks(Root root, String baseUrl) { private void addConformanceLinks(Root root, String baseUrl) { String title = "The Conformance classes"; - root.addLinksItem(new Link() + + root.addLinksItem(new OgcApiLink() .href(baseUrl + CONFORMANCE_REL) .title(title) - .rel(CONFORMANCE_REL).type(MediaType.TEXT_HTML_VALUE)); + .rel(CONFORMANCE_REL) + .type(MediaType.TEXT_HTML_VALUE)); - root.addLinksItem(new Link() + root.addLinksItem(new OgcApiLink() .href(baseUrl + CONFORMANCE_REL + "?f=json") .title(title + " as JSON") - .rel(CONFORMANCE_REL).type(MediaType.APPLICATION_JSON_VALUE)); + .rel(CONFORMANCE_REL) + .type(MediaType.APPLICATION_JSON_VALUE)); } @@ -202,7 +225,7 @@ public ResponseEntity conformanceDeclaration(@ApiIgnore HttpServlet "http://www.opengis.net/spec/ogcapi-records-1/1.0/conf/html", "http://www.opengis.net/spec/ogcapi-records-1/1.0/conf/json", "http://www.opengis.net/spec/ogcapi-records-1/1.0/conf/sorting" - )); + )); if (!mediaType.equals(MediaType.TEXT_HTML)) { return ResponseEntity.ok(conformance); @@ -224,7 +247,6 @@ public ResponseEntity conformanceDeclaration(@ApiIgnore HttpServlet /** * Collections information end-point. - * */ @io.swagger.v3.oas.annotations.Operation( summary = "Collections available from this API.", @@ -256,11 +278,12 @@ public ResponseEntity describeCollections(@ApiIgnore HttpServletRequest List sources = sourceRepository.findAll(); sources.forEach(s -> content.addCollectionsItem( - CollectionInfoBuilder.buildFromSource( - s, language, requestBaseUrl, configuration.getFormat(mediaType), configuration))); + collectionInfoBuilder.buildFromSource( + s, language, requestBaseUrl, configuration.getFormat(mediaType), + configuration, request))); // TODO: Accept format parameter. - List linkList = LinksItemsBuilder.build( + List linkList = LinksItemsBuilder.build( configuration.getFormat(mediaType), requestBaseUrl, language, configuration); linkList.forEach(content::addLinksItem); diff --git a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/controller/CollectionApiController.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/controller/CollectionApiController.java index 56a4c867..096ae997 100644 --- a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/controller/CollectionApiController.java +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/controller/CollectionApiController.java @@ -1,3 +1,8 @@ +/** + * (c) 2020 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.controller; import io.swagger.annotations.Api; @@ -62,18 +67,16 @@ public class CollectionApiController { @Autowired @Qualifier("xsltViewResolver") ViewResolver viewResolver; - - @Autowired - private CollectionService collectionService; - @Autowired MessageSource messages; - - @Autowired - private SearchConfiguration configuration; - @Autowired MediaTypeUtil mediaTypeUtil; + @Autowired + CollectionInfoBuilder collectionInfoBuilder; + @Autowired + private CollectionService collectionService; + @Autowired + private SearchConfiguration configuration; /** * Describe a collection. @@ -126,9 +129,9 @@ public ResponseEntity describeCollection( String requestBaseUrl = request.getRequestURL() .toString().replace(collectionId, ""); - CollectionInfo collectionInfo = CollectionInfoBuilder + CollectionInfo collectionInfo = collectionInfoBuilder .buildFromSource(source, language, requestBaseUrl, - configuration.getFormat(mediaType), configuration); + configuration.getFormat(mediaType), configuration, request); return ResponseEntity.ok(collectionInfo); 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 c5282407..57cea862 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 @@ -54,6 +54,7 @@ import org.fao.geonet.ogcapi.records.model.Item; import org.fao.geonet.ogcapi.records.model.XsltModel; import org.fao.geonet.ogcapi.records.service.CollectionService; +import org.fao.geonet.ogcapi.records.service.RecordService; import org.fao.geonet.ogcapi.records.util.MediaTypeUtil; import org.fao.geonet.ogcapi.records.util.RecordsEsQueryBuilder; import org.fao.geonet.ogcapi.records.util.XmlUtil; @@ -111,6 +112,8 @@ public class ItemApiController { MediaTypeUtil mediaTypeUtil; @Autowired DcatConverter dcatConverter; + @Autowired + RecordService recordService; /** * Describe a collection item. @@ -167,7 +170,8 @@ public ResponseEntity collectionsCollectionIdItemsRecordIdGet( || mediaType.equals(GnMediaType.APPLICATION_GEOJSON)) { try { String type = mediaType.equals(MediaType.APPLICATION_JSON) ? "json" : "geojson"; - JsonNode recordAsJson = getRecordAsJson(collectionId, recordId, request, source, type); + JsonNode recordAsJson = recordService.getRecordAsJson(collectionId, recordId, + request, source, type); streamResult(response, recordAsJson.toPrettyString(), @@ -307,7 +311,7 @@ private ResponseEntity collectionsCollectionIdItemsRecordIdGetAsJsonLd( || GnMediaType.APPLICATION_RDF_XML_VALUE.equals(acceptHeader); boolean isLinkedData = (isTurtle || isRdfXml || isDcat); - JsonNode recordAsJson = getRecordAsJson(collectionId, recordId, request, source, + JsonNode recordAsJson = recordService.getRecordAsJson(collectionId, recordId, request, source, isLinkedData ? "json" : "schema.org"); if (isLinkedData) { @@ -372,9 +376,11 @@ private ResponseEntity collectionsCollectionIdItemsRecordIdGetAsXml( try { String collectionFilter = collectionService.retrieveCollectionFilter(source, true); - String query = recordsEsQueryBuilder.buildQuerySingleRecord(recordId, collectionFilter, null); + String query = recordsEsQueryBuilder.buildQuerySingleRecord(recordId, + collectionFilter, null); - String queryResponse = proxy.searchAndGetResult(request.getSession(), request, query, null); + String queryResponse = proxy.searchAndGetResult(request.getSession(), + request, query, null); Document queryResult = XmlUtil.parseXmlString(queryResponse); String total = queryResult.getChildNodes().item(0).getAttributes().getNamedItem("total") @@ -417,7 +423,8 @@ private ResponseEntity collectionsCollectionIdItemsRecordIdGetAsHtml( } try { - JsonNode recordAsJson = getRecordAsJson(collectionId, recordId, request, source, "json"); + JsonNode recordAsJson = recordService.getRecordAsJson(collectionId, recordId, + request, source, "json"); Metadata metadataRecord = metadataRepository.findOneByUuid(recordId); if (metadataRecord == null) { @@ -463,43 +470,6 @@ private ResponseEntity collectionsCollectionIdItemsRecordIdGetAsHtml( } - private JsonNode getRecordAsJson( - String collectionId, - String recordId, - HttpServletRequest request, - Source source, - String type) throws Exception { - String collectionFilter = collectionService.retrieveCollectionFilter(source, true); - String query = recordsEsQueryBuilder.buildQuerySingleRecord(recordId, collectionFilter, null); - - String queryResponse = proxy.searchAndGetResult(request.getSession(), request, query, null); - - ObjectMapper mapper = new ObjectMapper(); - JsonFactory factory = mapper.getFactory(); - JsonParser parser = factory.createParser(queryResponse); - JsonNode actualObj = mapper.readTree(parser); - - JsonNode totalValue = - "json".equals(type) - ? actualObj.get("hits").get("total").get("value") - : actualObj.get("size"); - - if ((totalValue == null) || (totalValue.intValue() == 0)) { - throw new ResponseStatusException(HttpStatus.NOT_FOUND, - messages.getMessage(EXCEPTION_COLLECTION_ITEM_NOT_FOUND, - new String[]{recordId, collectionId}, - request.getLocale())); - } - - if ("json".equals(type)) { - return actualObj.get("hits").get("hits").get(0); - } else { - String elementName = "schema.org".equals(type) ? "dataFeedElement" : "features"; - return actualObj.get(elementName).get(0); - } - } - - private List setDefaultRssSortBy(List sortby, HttpServletRequest request) { boolean isRss = "rss".equals(request.getParameter("f")) || (request.getHeader(HttpHeaders.ACCEPT) != null @@ -605,6 +575,7 @@ private ResponseEntity collectionsCollectionIdItemsGetAsHtml( String collectionFilter = collectionService.retrieveCollectionFilter(source, false); String query = recordsEsQueryBuilder .buildQuery(q, externalids, bbox, startindex, limit, collectionFilter, sortby, null); + EsSearchResults results = new EsSearchResults(); try { results = proxy 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/controller/model/CollectionInfo.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/controller/model/CollectionInfo.java index 0d05f71f..35a29957 100644 --- a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/controller/model/CollectionInfo.java +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/controller/model/CollectionInfo.java @@ -1,5 +1,12 @@ +/** + * (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.controller.model; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; @@ -10,45 +17,350 @@ import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlRootElement; +import org.fao.geonet.ogcapi.records.model.OgcApiContact; +import org.fao.geonet.ogcapi.records.model.OgcApiExtent; +import org.fao.geonet.ogcapi.records.model.OgcApiLanguage; +import org.fao.geonet.ogcapi.records.model.OgcApiLink; +import org.fao.geonet.ogcapi.records.model.OgcApiSchema; +import org.fao.geonet.ogcapi.records.model.OgcApiTheme; /** - * CollectionInfo entity. + * cf. https://github.com/opengeospatial/ogcapi-records/blob/master/core/openapi/schemas/catalog.yaml + * + *

CollectionInfo entity. */ @JacksonXmlRootElement(localName = "CollectionInfo") @XmlRootElement(name = "CollectionInfo") @XmlAccessorType(XmlAccessType.FIELD) -public class CollectionInfo { +public class CollectionInfo { + + /** + * Fixed value of "Catalog". + */ + //required cf. https://github.com/opengeospatial/ogcapi-records/blob/master/core/openapi/schemas/catalog.yaml + @JsonProperty("type") + @JacksonXmlProperty(localName = "type") + private String type = "catalog"; + + // cf. https://github.com/opengeospatial/ogcapi-records/blob/master/core/openapi/schemas/catalog.yaml + // NOTE: spec says this should be "record" or ["record","catalog"]. + /**Fixed value of "record", "catalog" or both.*/ + @JsonProperty("itemType") + @JacksonXmlProperty(localName = "itemType") + private String itemType = "record"; + + + /** + * The identifier of the catalog. + */ @JsonProperty("id") @JacksonXmlProperty(localName = "id") private String id; + /** + * A human-readable name given to the resource. + */ @JsonProperty("title") @JacksonXmlProperty(localName = "title") private String title; + /** + * A free-text account of the resource. + */ @JsonProperty("description") @JacksonXmlProperty(localName = "description") private String description; + /** + * links for this object. + */ @JsonProperty("links") @JacksonXmlProperty(localName = "links") - - private List links = new ArrayList<>(); + @JsonInclude(Include.NON_EMPTY) + private List links = new ArrayList<>(); + /** + * The spatiotemporal coverage of this catalog. + */ @JsonProperty("extent") @JacksonXmlProperty(localName = "extent") - private Extent extent; + private OgcApiExtent extent; + /** + * The list of supported coordinate reference systems. + */ @JsonProperty("crs") @JacksonXmlProperty(localName = "crs") - + @JsonInclude(Include.NON_EMPTY) private List crs = null; + /** + * A list of contacts qualified by their role(s) in association to the record or the resource + * described by the record. + */ + @JsonProperty("contacts") + @JacksonXmlProperty(localName = "contacts") + @JsonInclude(Include.NON_EMPTY) + private List contacts = null; + + /** + * The date this record was created in the server. format: date-time + */ + @JsonProperty("created") + @JacksonXmlProperty(localName = "created") + @JsonInclude(Include.NON_EMPTY) + private String created = null; + + /** + * The most recent date on which the record was changed. format: date-time + */ + @JsonProperty("updated") + @JacksonXmlProperty(localName = "updated") + @JsonInclude(Include.NON_EMPTY) + private String updated = null; + + /** + * The topic or topics of the resource. Typically represented using free-form keywords, tags, key + * phrases, or classification codes. + */ + @JsonProperty("keywords") + @JacksonXmlProperty(localName = "keywords") + @JsonInclude(Include.NON_EMPTY) + private List keywords = null; + + /** + * The language used for textual values in this record representation. + */ + @JsonProperty("language") + @JacksonXmlProperty(localName = "language") + @JsonInclude(Include.NON_EMPTY) + private OgcApiLanguage language = null; + + /** + * This list of languages in which this record is available. + */ + @JsonProperty("languages") + @JacksonXmlProperty(localName = "languages") + @JsonInclude(Include.NON_EMPTY) + private List languages = null; + + /** + * A knowledge organization system used to classify the resource. + **/ + @JsonProperty("themes") + @JsonInclude(Include.NON_EMPTY) + @JacksonXmlProperty(localName = "themes") + private List themes = null; + + + /** + * A legal document under which the resource is made available. If the resource is being made + * available under a common license then use an SPDX license id (https://spdx.org/licenses/). If + * the resource is being made available under multiple common licenses then use an SPDX license + * expression v2.3 string (https://spdx.github.io/spdx-spec/v2.3/SPDX-license-expressions/) If the + * resource is being made available under one or more licenses that haven't been assigned an SPDX + * identifier or one or more custom licenses then use a string value of 'other' and include one or + * more links (rel="license") in the `link` section of the record to the file(s) that contains the + * text of the license(s). There is also the case of a resource that is private or unpublished and + * is thus unlicensed; in this case do not register such a resource in the catalog in the first + * place since there is no point in making such a resource discoverable. + */ + @JsonProperty("license") + @JacksonXmlProperty(localName = "license") + @JsonInclude(Include.NON_EMPTY) + private String license = null; + + + /** + * A statement that concerns all rights not addressed by the license such as a copyright + * statement. + */ + @JsonProperty("rights") + @JacksonXmlProperty(localName = "rights") + @JsonInclude(Include.NON_EMPTY) + private String rights = null; + + + /** + * Each string in the array SHALL be the name of a sortable. + */ + @JsonProperty("defaultSortOrder") + @JacksonXmlProperty(localName = "defaultSortOrder") + @JsonInclude(Include.NON_EMPTY) + private List defaultSortOrder = null; + + + //-------------------------------------------------------------------------- + // these are properties in the written spec, but not in the .yaml definition + //-------------------------------------------------------------------------- + + /** + * The extensions/conformance classes used in this catalog object. + */ + @JsonProperty("conformsTo") + @JacksonXmlProperty(localName = "conformsTo") + @JsonInclude(Include.NON_EMPTY) + private String conformsTo = null; + + /**The list of languages in which records from the collection can be represented.*/ + @JsonProperty("recordLanguages") + @JacksonXmlProperty(localName = "recordLanguages") + @JsonInclude(Include.NON_EMPTY) + private List recordLanguages = null; + + + /** + * The name of the array property in the catalog used to encode records in-line. + * The default value is records. + */ + @JsonProperty("recordsArrayName") + @JacksonXmlProperty(localName = "recordsArrayName") + @JsonInclude(Include.NON_EMPTY) + private String recordsArrayName = null; + + /**A list of schemes related to this catalog.*/ + @JsonProperty("schemes") + @JacksonXmlProperty(localName = "schemes") + @JsonInclude(Include.NON_EMPTY) + private List schemes = null; + + + //-------------------------------------------------------------------------- + + + public String getConformsTo() { + return conformsTo; + } + + public void setConformsTo(String conformsTo) { + this.conformsTo = conformsTo; + } + + public List getRecordLanguages() { + return recordLanguages; + } + + public void setRecordLanguages(List recordLanguages) { + this.recordLanguages = recordLanguages; + } + + public String getRecordsArrayName() { + return recordsArrayName; + } + + public void setRecordsArrayName(String recordsArrayName) { + this.recordsArrayName = recordsArrayName; + } + + public List getSchemes() { + return schemes; + } + + public void setSchemes(List schemes) { + this.schemes = schemes; + } + + public List getDefaultSortOrder() { + return defaultSortOrder; + } + + public void setDefaultSortOrder(List defaultSortOrder) { + this.defaultSortOrder = defaultSortOrder; + } + + public String getRights() { + return rights; + } + + public void setRights(String rights) { + this.rights = rights; + } + + public String getLicense() { + return license; + } + + public void setLicense(String license) { + this.license = license; + } + public CollectionInfo id(String id) { this.id = id; return this; } + public List getThemes() { + return themes; + } + + public void setThemes(List themes) { + this.themes = themes; + } + + public OgcApiLanguage getLanguage() { + return language; + } + + public void setLanguage(OgcApiLanguage language) { + this.language = language; + } + + public List getLanguages() { + return languages; + } + + public void setLanguages(List languages) { + this.languages = languages; + } + + public List getKeywords() { + return keywords; + } + + public void setKeywords(List keywords) { + this.keywords = keywords; + } + + public String getCreated() { + return created; + } + + public void setCreated(String created) { + this.created = created; + } + + public String getUpdated() { + return updated; + } + + public void setUpdated(String updated) { + this.updated = updated; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getItemType() { + return itemType; + } + + public void setItemType(String itemType) { + this.itemType = itemType; + } + + public List getContacts() { + return contacts; + } + + public void setContacts(List contacts) { + this.contacts = contacts; + } + /** * Identifier of the collection used, for example, in URIs. */ @@ -97,12 +409,13 @@ public void setDescription(String description) { this.description = description; } - public CollectionInfo links(List links) { + + public CollectionInfo links(List links) { this.links = links; return this; } - public CollectionInfo addLinksItem(Link linksItem) { + public CollectionInfo addLinksItem(OgcApiLink linksItem) { this.links.add(linksItem); return this; } @@ -111,15 +424,15 @@ public CollectionInfo addLinksItem(Link linksItem) { * Get links. */ @ApiModelProperty(example = "[{\"href\":\"http://data.example.org/collections/buildings/items\",\"rel\":\"item\",\"type\":\"application/geo+json\",\"title\":\"Buildings\"},{\"href\":\"http://example.org/concepts/building.html\",\"rel\":\"describedBy\",\"type\":\"text/html\",\"title\":\"Coverage for buildings\"}]", required = true, value = "") - public List getLinks() { + public List getLinks() { return links; } - public void setLinks(List links) { + public void setLinks(List links) { this.links = links; } - public CollectionInfo extent(Extent extent) { + public CollectionInfo extent(OgcApiExtent extent) { this.extent = extent; return this; } @@ -128,11 +441,11 @@ public CollectionInfo extent(Extent extent) { * Get extent. */ @ApiModelProperty(value = "") - public Extent getExtent() { + public OgcApiExtent getExtent() { return extent; } - public void setExtent(Extent extent) { + public void setExtent(OgcApiExtent extent) { this.extent = extent; } @@ -155,8 +468,8 @@ public CollectionInfo addCrsItem(String crsItem) { /** * The coordinate reference systems in which geometries may be retrieved. Coordinate reference * systems are identified by a URI. The first coordinate reference system is the coordinate - * reference system that is used by default. This is always - * \"http://www.opengis.net/def/crs/OGC/1.3/CRS84\", i.e. WGS84 longitude/latitude. + * reference system that is used by default. This is always \"http://www.opengis.net/def/crs/OGC/1.3/CRS84\", + * i.e. WGS84 longitude/latitude. */ @ApiModelProperty(value = "The coordinate reference systems in which geometries may be retrieved. Coordinate reference systems are identified by a URI. The first coordinate reference system is the coordinate reference system that is used by default. This is always \"http://www.opengis.net/def/crs/OGC/1.3/CRS84\", i.e. WGS84 longitude/latitude.") public List getCrs() { @@ -194,7 +507,7 @@ public int hashCode() { public String toString() { StringBuilder sb = new StringBuilder(); sb.append("class CollectionInfo {\n"); - + sb.append(" name: ").append(toIndentedString(id)).append("\n"); sb.append(" title: ").append(toIndentedString(title)).append("\n"); sb.append(" description: ").append(toIndentedString(description)).append("\n"); @@ -206,8 +519,8 @@ public String toString() { } /** - * Convert the given object to string with each line indented by 4 spaces - * (except the first line). + * Convert the given object to string with each line indented by 4 spaces (except the first + * line). */ private String toIndentedString(java.lang.Object o) { if (o == null) { diff --git a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/controller/model/Content.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/controller/model/Content.java index c490f43a..9664f534 100644 --- a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/controller/model/Content.java +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/controller/model/Content.java @@ -10,6 +10,7 @@ import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlRootElement; +import org.fao.geonet.ogcapi.records.model.OgcApiLink; /** * Content entity. @@ -17,23 +18,24 @@ @JacksonXmlRootElement(localName = "Content") @XmlRootElement(name = "Content") @XmlAccessorType(XmlAccessType.FIELD) -public class Content { +public class Content { + @JsonProperty("links") @JacksonXmlProperty(localName = "links") - - private List links = new ArrayList<>(); + + private List links = new ArrayList<>(); @JsonProperty("collections") @JacksonXmlProperty(localName = "collections") - + private List collections = new ArrayList<>(); - public Content links(List links) { + public Content links(List links) { this.links = links; return this; } - public Content addLinksItem(Link linksItem) { + public Content addLinksItem(OgcApiLink linksItem) { this.links.add(linksItem); return this; } @@ -42,11 +44,11 @@ public Content addLinksItem(Link linksItem) { * Get links. */ @ApiModelProperty(example = "[{\"href\":\"http://data.example.org/collections.json\",\"rel\":\"self\",\"type\":\"application/json\",\"title\":\"this document\"},{\"href\":\"http://data.example.org/collections.html\",\"rel\":\"alternate\",\"type\":\"text/html\",\"title\":\"this document as HTML\"},{\"href\":\"http://schemas.example.org/1.0/foobar.xsd\",\"rel\":\"describedBy\",\"type\":\"application/xml\",\"title\":\"XML schema for Acme Corporation data\"}]", required = true, value = "") - public List getLinks() { + public List getLinks() { return links; } - public void setLinks(List links) { + public void setLinks(List links) { this.links = links; } @@ -95,7 +97,7 @@ public int hashCode() { public String toString() { StringBuilder sb = new StringBuilder(); sb.append("class Content {\n"); - + sb.append(" links: ").append(toIndentedString(links)).append("\n"); sb.append(" collections: ").append(toIndentedString(collections)).append("\n"); sb.append("}"); @@ -103,8 +105,8 @@ public String toString() { } /** - * Convert the given object to string with each line indented by 4 spaces - * (except the first line). + * Convert the given object to string with each line indented by 4 spaces (except the first + * line). */ private String toIndentedString(java.lang.Object o) { if (o == null) { diff --git a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/controller/model/CrsEnum.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/controller/model/CrsEnum.java new file mode 100644 index 00000000..cc2dfe11 --- /dev/null +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/controller/model/CrsEnum.java @@ -0,0 +1,47 @@ +/** + * (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.controller.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Coordinate reference system of the coordinates in the spatial extent (property `spatial`). + * In the Core, only WGS84 longitude/latitude is supported. Extensions may support additional + * coordinate reference systems. + */ +public enum CrsEnum { + HTTP_WWW_OPENGIS_NET_DEF_CRS_OGC_1_3_CRS84("http://www.opengis.net/def/crs/OGC/1.3/CRS84"); + + private String value; + + CrsEnum(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } + + @Override + public String toString() { + return String.valueOf(value); + } + + /** + * Get a CrsEnum value from a string representation. + */ + @JsonCreator + public static CrsEnum fromValue(String value) { + for (CrsEnum b : CrsEnum.values()) { + if (b.value.equals(value)) { + return b; + } + } + throw new IllegalArgumentException("Unexpected value '" + value + "'"); + } +} \ No newline at end of file diff --git a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/controller/model/Extent.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/controller/model/Extent.java deleted file mode 100644 index ced3f92f..00000000 --- a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/controller/model/Extent.java +++ /dev/null @@ -1,268 +0,0 @@ -package org.fao.geonet.ogcapi.records.controller.model; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonValue; -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; -import io.swagger.annotations.ApiModelProperty; -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlRootElement; - -/** - * Extent entity. - */ -@JacksonXmlRootElement(localName = "Extent") -@XmlRootElement(name = "Extent") -@XmlAccessorType(XmlAccessType.FIELD) -public class Extent { - /** - * Coordinate reference system of the coordinates in the spatial extent (property `spatial`). - * In the Core, only WGS84 longitude/latitude is supported. Extensions may support additional - * coordinate reference systems. - */ - public enum CrsEnum { - HTTP_WWW_OPENGIS_NET_DEF_CRS_OGC_1_3_CRS84("http://www.opengis.net/def/crs/OGC/1.3/CRS84"); - - private String value; - - CrsEnum(String value) { - this.value = value; - } - - @JsonValue - public String getValue() { - return value; - } - - @Override - public String toString() { - return String.valueOf(value); - } - - /** - * Get a CrsEnum value from a string representation. - */ - @JsonCreator - public static CrsEnum fromValue(String value) { - for (CrsEnum b : CrsEnum.values()) { - if (b.value.equals(value)) { - return b; - } - } - throw new IllegalArgumentException("Unexpected value '" + value + "'"); - } - } - - @JsonProperty("crs") - @JacksonXmlProperty(localName = "crs") - private CrsEnum crs = CrsEnum.HTTP_WWW_OPENGIS_NET_DEF_CRS_OGC_1_3_CRS84; - - @JsonProperty("spatial") - @JacksonXmlProperty(localName = "spatial") - - private List spatial = null; - - /** - * Temporal reference system of the coordinates in the temporal extent (property `temporal`). - * In the Core, only the Gregorian calendar is supported. Extensions may support additional - * temporal reference systems. - */ - public enum TrsEnum { - HTTP_WWW_OPENGIS_NET_DEF_UOM_ISO_8601_0_GREGORIAN("http://www.opengis.net/def/uom/ISO-8601/0/Gregorian"); - - private String value; - - TrsEnum(String value) { - this.value = value; - } - - @JsonValue - public String getValue() { - return value; - } - - @Override - public String toString() { - return String.valueOf(value); - } - - /** - * Get a TrsEnum value from a string representation. - */ - @JsonCreator - public static TrsEnum fromValue(String value) { - for (TrsEnum b : TrsEnum.values()) { - if (b.value.equals(value)) { - return b; - } - } - throw new IllegalArgumentException("Unexpected value '" + value + "'"); - } - } - - @JsonProperty("trs") - @JacksonXmlProperty(localName = "trs") - private TrsEnum trs = TrsEnum.HTTP_WWW_OPENGIS_NET_DEF_UOM_ISO_8601_0_GREGORIAN; - - @JsonProperty("temporal") - @JacksonXmlProperty(localName = "temporal") - - private List temporal = null; - - public Extent crs(CrsEnum crs) { - this.crs = crs; - return this; - } - - /** - * Coordinate reference system of the coordinates in the spatial extent (property `spatial`). - * In the Core, only WGS84 longitude/latitude is supported. Extensions may support additional - * coordinate reference systems. - */ - @ApiModelProperty(value = "Coordinate reference system of the coordinates in the spatial extent " - + "(property `spatial`). In the Core, only WGS84 longitude/latitude is supported. " - + "Extensions may support additional coordinate reference systems.") - public CrsEnum getCrs() { - return crs; - } - - public void setCrs(CrsEnum crs) { - this.crs = crs; - } - - public Extent spatial(List spatial) { - this.spatial = spatial; - return this; - } - - /** - * Adds a spatial item value. - */ - public Extent addSpatialItem(BigDecimal spatialItem) { - if (this.spatial == null) { - this.spatial = new ArrayList<>(); - } - this.spatial.add(spatialItem); - return this; - } - - /** - * West, north, east, south edges of the spatial extent. The minimum and maximum values apply - * to the coordinate reference system WGS84 longitude/latitude that is supported in the Core. - * If, for example, a projected coordinate reference system is used, the minimum and maximum - * values need to be adjusted. - */ - @ApiModelProperty(example = "[-180,-90,180,90]", value = "West, north, east, south edges of the " - + "spatial extent. The minimum and maximum values apply to the coordinate reference system " - + "WGS84 longitude/latitude that is supported in the Core. If, for example, a projected " - + "coordinate reference system is used, the minimum and maximum values need to be adjusted.") - public List getSpatial() { - return spatial; - } - - public void setSpatial(List spatial) { - this.spatial = spatial; - } - - public Extent trs(TrsEnum trs) { - this.trs = trs; - return this; - } - - /** - * Temporal reference system of the coordinates in the temporal extent (property `temporal`). - * In the Core, only the Gregorian calendar is supported. Extensions may support additional - * temporal reference systems. - */ - @ApiModelProperty(value = "Temporal reference system of the coordinates in the temporal extent " - + "(property `temporal`). In the Core, only the Gregorian calendar is supported. " - + "Extensions may support additional temporal reference systems.") - public TrsEnum getTrs() { - return trs; - } - - public void setTrs(TrsEnum trs) { - this.trs = trs; - } - - public Extent temporal(List temporal) { - this.temporal = temporal; - return this; - } - - /** - * Adds a temporal item. - */ - public Extent addTemporalItem(String temporalItem) { - if (this.temporal == null) { - this.temporal = new ArrayList<>(); - } - this.temporal.add(temporalItem); - return this; - } - - /** - * Begin and end times of the temporal extent. - */ - @ApiModelProperty(example = "[\"2011-11-11T12:22:11.000Z\",\"2012-11-24T12:32:43.000Z\"]", - value = "Begin and end times of the temporal extent.") - public List getTemporal() { - return temporal; - } - - public void setTemporal(List temporal) { - this.temporal = temporal; - } - - - @Override - public boolean equals(java.lang.Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Extent extent = (Extent) o; - return Objects.equals(this.crs, extent.crs) - && Objects.equals(this.spatial, extent.spatial) - && Objects.equals(this.trs, extent.trs) - && Objects.equals(this.temporal, extent.temporal); - } - - @Override - public int hashCode() { - return Objects.hash(crs, spatial, trs, temporal); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("class Extent {\n"); - - sb.append(" crs: ").append(toIndentedString(crs)).append("\n"); - sb.append(" spatial: ").append(toIndentedString(spatial)).append("\n"); - sb.append(" trs: ").append(toIndentedString(trs)).append("\n"); - sb.append(" temporal: ").append(toIndentedString(temporal)).append("\n"); - sb.append("}"); - return sb.toString(); - } - - /** - * Convert the given object to string with each line indented by 4 spaces - * (except the first line). - */ - private String toIndentedString(java.lang.Object o) { - if (o == null) { - return "null"; - } - return o.toString().replace("\n", "\n "); - } -} - diff --git a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/controller/model/Link.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/controller/model/Link.java deleted file mode 100644 index 76d91c41..00000000 --- a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/controller/model/Link.java +++ /dev/null @@ -1,169 +0,0 @@ -package org.fao.geonet.ogcapi.records.controller.model; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; -import io.swagger.annotations.ApiModelProperty; -import java.util.Objects; -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlRootElement; - -/** - * Link entity. - */ -@JacksonXmlRootElement(localName = "Link") -@XmlRootElement(name = "Link") -@XmlAccessorType(XmlAccessType.FIELD) -public class Link { - @JsonProperty("href") - @JacksonXmlProperty(localName = "href") - private String href; - - @JsonProperty("rel") - @JacksonXmlProperty(localName = "rel") - private String rel; - - @JsonProperty("title") - @JacksonXmlProperty(localName = "title") - private String title; - - @JsonProperty("type") - @JacksonXmlProperty(localName = "type") - private String type; - - @JsonProperty("hreflang") - @JacksonXmlProperty(localName = "hreflang") - private String hreflang; - - public Link href(String href) { - this.href = href; - return this; - } - - /** - * Get href. - * */ - @ApiModelProperty(required = true, value = "") - public String getHref() { - return href; - } - - public void setHref(String href) { - this.href = href; - } - - public Link rel(String rel) { - this.rel = rel; - return this; - } - - /** - * Get rel. - */ - @ApiModelProperty(example = "prev", value = "") - public String getRel() { - return rel; - } - - public void setRel(String rel) { - this.rel = rel; - } - - public Link type(String type) { - this.type = type; - return this; - } - - /** - * Get title. - */ - @ApiModelProperty(example = "The OpenAPI documentation", value = "") - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public Link title(String title) { - this.title = title; - return this; - } - - /** - * Get type. - */ - @ApiModelProperty(example = "application/geo+json", value = "") - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } - - public Link hreflang(String hreflang) { - this.hreflang = hreflang; - return this; - } - - /** - * Get hreflang. - */ - @ApiModelProperty(example = "en", value = "") - public String getHreflang() { - return hreflang; - } - - public void setHreflang(String hreflang) { - this.hreflang = hreflang; - } - - - @Override - public boolean equals(java.lang.Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Link link = (Link) o; - return Objects.equals(this.href, link.href) - && Objects.equals(this.rel, link.rel) - && Objects.equals(this.type, link.type) - && Objects.equals(this.hreflang, link.hreflang); - } - - @Override - public int hashCode() { - return Objects.hash(href, rel, type, hreflang); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("class Link {\n"); - - sb.append(" href: ").append(toIndentedString(href)).append("\n"); - sb.append(" rel: ").append(toIndentedString(rel)).append("\n"); - sb.append(" type: ").append(toIndentedString(type)).append("\n"); - sb.append(" hreflang: ").append(toIndentedString(hreflang)).append("\n"); - sb.append("}"); - return sb.toString(); - } - - /** - * Convert the given object to string with each line indented by 4 spaces - * (except the first line). - */ - private String toIndentedString(java.lang.Object o) { - if (o == null) { - return "null"; - } - return o.toString().replace("\n", "\n "); - } -} - diff --git a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/controller/model/Root.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/controller/model/Root.java index 521c9a10..0c1d4bf5 100644 --- a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/controller/model/Root.java +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/controller/model/Root.java @@ -1,5 +1,7 @@ package org.fao.geonet.ogcapi.records.controller.model; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; @@ -10,17 +12,44 @@ import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlRootElement; +import org.fao.geonet.ogcapi.records.model.OgcApiLink; /** * Root entity. + * + *

Used for the landing page. */ @JacksonXmlRootElement(localName = "Root") @XmlRootElement(name = "Root") @XmlAccessorType(XmlAccessType.FIELD) public class Root { + + /** + * This is the collection info for the main-portal. + * + *

THIS IS NON-STANDARD (not in the ogcapi spec)! + * + *

The landing page JSON also includes a new property systemInfo which contains a catalog.yaml + * object in it. + * I'm not sure if this is allowed in the spec, but it allows for more metadata about the + * entire GN system ("ogcapi-records" server). This is useful for making a nicer landing page + * (cf. pygeoapi's landing page). + */ + @JsonProperty("systemInfo") + @JacksonXmlProperty(localName = "systemInfo") + @JsonInclude(Include.NON_EMPTY) + private CollectionInfo systemInfo; + + @JsonProperty("title") @JacksonXmlProperty(localName = "title") private String title; + @JsonProperty("description") + @JacksonXmlProperty(localName = "description") + private String description; + @JsonProperty("links") + @JacksonXmlProperty(localName = "links") + private List links = new ArrayList<>(); /** * Get title. @@ -39,10 +68,6 @@ public Root title(String title) { return this; } - @JsonProperty("description") - @JacksonXmlProperty(localName = "description") - private String description; - /** * Get description. */ @@ -60,17 +85,12 @@ public Root description(String description) { return this; } - @JsonProperty("links") - @JacksonXmlProperty(localName = "links") - - private List links = new ArrayList<>(); - - public Root links(List links) { + public Root links(List links) { this.links = links; return this; } - public Root addLinksItem(Link linksItem) { + public Root addLinksItem(OgcApiLink linksItem) { this.links.add(linksItem); return this; } @@ -79,14 +99,21 @@ public Root addLinksItem(Link linksItem) { * Get links. */ @ApiModelProperty(example = "[{\"href\":\"http://data.example.org/\",\"rel\":\"self\",\"type\":\"application/json\",\"title\":\"this document\"},{\"href\":\"http://data.example.org/api\",\"rel\":\"service\",\"type\":\"application/openapi+json;version=3.0\",\"title\":\"the API definition\"},{\"href\":\"http://data.example.org/conformance\",\"rel\":\"conformance\",\"type\":\"application/json\",\"title\":\"OGC conformance classes implemented by this API\"},{\"href\":\"http://data.example.org/collections\",\"title\":\"Metadata about the resource collections\"}]", required = true, value = "") - public List getLinks() { + public List getLinks() { return links; } - public void setLinks(List links) { + public void setLinks(List links) { this.links = links; } + public CollectionInfo getSystemInfo() { + return systemInfo; + } + + public void setSystemInfo(CollectionInfo systemInfo) { + this.systemInfo = systemInfo; + } @Override public boolean equals(java.lang.Object o) { 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/model/OgcApiAddress.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiAddress.java new file mode 100644 index 00000000..25bdfa0d --- /dev/null +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiAddress.java @@ -0,0 +1,126 @@ +/** + * (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 com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import java.util.ArrayList; +import java.util.List; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * see "address" in https://github.com/opengeospatial/ogcapi-records/blob/master/core/openapi/schemas/contact.yaml + * + *

Physical location at which contact can be made. + */ +@XmlRootElement(name = "address") +@XmlAccessorType(XmlAccessType.FIELD) +public class OgcApiAddress { + + /** + * Address lines for the location. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "deliveryPoint") + @XmlElement(name = "deliveryPoint") + public List deliveryPoint = new ArrayList<>(); + + /** + * City for the location. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "city") + @XmlElement(name = "city") + public String city; + + /** + * State or province of the location. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "administrativeArea") + @XmlElement(name = "administrativeArea") + public String administrativeArea; + + /** + * ZIP or other postal code. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "postalCode") + @XmlElement(name = "postalCode") + public String postalCode; + + /** + * Country of the physical address. ISO 3166-1 is recommended. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "country") + @XmlElement(name = "country") + public String country; + + /** + * The type of address (e.g. office, home, etc.). + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "roles") + @XmlElement(name = "roles") + public List roles = new ArrayList<>(); + + public OgcApiAddress(String address) { + deliveryPoint.add(address); + } + + public List getDeliveryPoint() { + return deliveryPoint; + } + + public void setDeliveryPoint(List deliveryPoint) { + this.deliveryPoint = deliveryPoint; + } + + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } + + public String getAdministrativeArea() { + return administrativeArea; + } + + public void setAdministrativeArea(String administrativeArea) { + this.administrativeArea = administrativeArea; + } + + public String getPostalCode() { + return postalCode; + } + + public void setPostalCode(String postalCode) { + this.postalCode = postalCode; + } + + public String getCountry() { + return country; + } + + public void setCountry(String country) { + this.country = country; + } + + public List getRoles() { + return roles; + } + + public void setRoles(List roles) { + this.roles = roles; + } +} diff --git a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiComponent.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiComponent.java new file mode 100644 index 00000000..ed0e4be7 --- /dev/null +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiComponent.java @@ -0,0 +1,18 @@ +/** + * (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; + +/** + * OgcApiComponent. + * + *

cf. https://github.com/opengeospatial/ogcapi-records/blob/master/core/openapi/schemas/schema.yaml + * + *

TODO - This is a very complex object with many pattern properties. I've left this empty + * for now. When this is needed, please fill in (also, include the x-* properties used elsewhere). + */ +public class OgcApiComponent { + +} diff --git a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiConcept.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiConcept.java new file mode 100644 index 00000000..7d8f1918 --- /dev/null +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiConcept.java @@ -0,0 +1,92 @@ +/** + * (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 com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; + + +/** + * cf. https://github.com/opengeospatial/ogcapi-records/blob/master/core/openapi/schemas/theme.yaml + * + *

Entity/concept identifiers from this knowledge + * system. it is recommended that a resolvable URI be used for each entity/concept identifier. + */ +public class OgcApiConcept { + + + /** + * An identifier for the concept. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "id") + @XmlElement(name = "id") + public String id; + + /** + * A human readable title for the concept. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "title") + @XmlElement(name = "title") + public String title; + + /** + * A human readable description for the concept. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "description") + @XmlElement(name = "description") + public String description; + + /** + * A URI providing further description of the concept. format: uri + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "url") + @XmlElement(name = "url") + public String url; + + public OgcApiConcept(String id, String url) { + this.id = id; + this.url = url; + } + + + 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 String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } +} diff --git a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiContact.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiContact.java new file mode 100644 index 00000000..02cafb8f --- /dev/null +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiContact.java @@ -0,0 +1,345 @@ +/** + * (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 com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; +import javax.xml.bind.annotation.XmlRootElement; +import org.fao.geonet.index.model.gn.Contact; +import org.fao.geonet.ogcapi.records.util.JsonUtils; +import org.springframework.util.StringUtils; + +/** + * https://github.com/opengeospatial/ogcapi-records/blob/master/core/openapi/schemas/contact.yaml + * + *

Identification of, and means of communication with, person responsible + * for the resource. + */ +@XmlRootElement(name = "contact") +@XmlAccessorType(XmlAccessType.FIELD) +public class OgcApiContact { + + /** + * A value uniquely identifying a contact. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "identifier") + @XmlElement(name = "identifier") + private String identifier; + + /** + * The name of the responsible person. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "name") + @XmlElement(name = "name") + private String name; + + /** + * The name of the role or position of the responsible person taken from the organization's formal + * organizational hierarchy or chart. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "position") + @XmlElement(name = "position") + private String position; + + /** + * Organization/affiliation of the contact. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "organization") + @XmlElement(name = "organization") + private String organization; + + /** + * Graphic identifying a contact. The link relation should be `icon` and the media type should be + * an image media type. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "logo") + @XmlElement(name = "logo") + private OgcApiLink logo; + + /** + * Telephone numbers at which contact can be made. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "phones") + @XmlElement(name = "phones") + private List phones = new ArrayList<>(); + + /** + * Email addresses at which contact can be made. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "emails") + @XmlElement(name = "emails") + private List emails = new ArrayList<>(); + + /** + * Physical location at which contact can be made. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "addresses") + @XmlElement(name = "addresses") + private List addresses = new ArrayList<>(); + + /** + * On-line information about the contact. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "links") + @XmlElement(name = "links") + private List links = new ArrayList<>(); + + /** + * Time period when the contact can be contacted. + * + *

example: "Hours: Mo-Fr 10am-7pm Sa 10am-22pm Su 10am-21pm" + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "hoursOfService") + @XmlElement(name = "hoursOfService") + private String hoursOfService; + + /** + * Supplemental instructions on how or when to contact the responsible party. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "contactInstructions") + @XmlElement(name = "contactInstructions") + private String contactInstructions; + + /** + * The set of named duties, job functions and/or permissions associated with this contact. (e.g. + * developer, administrator, etc.). + */ + @JsonInclude(JsonInclude.Include.NON_NULL) + @XmlElementWrapper(name = "roles") + @XmlElement(name = "roles") + private List roles = new ArrayList<>(); + + public OgcApiContact() { + } + + + /** + * create from the Elastic Index JSON node for the contact. + * + * @param contactInfo from parsed Elastic Index for a single contact + * @return parsed contact + */ + public static OgcApiContact fromIndexMap(Contact contactInfo) { + if (contactInfo == null) { + return null; + } + OgcApiContact result = new OgcApiContact(); + if (StringUtils.hasText(contactInfo.getRole())) { + result.getRoles().add(contactInfo.getRole()); + } + if (contactInfo.getOrganisation() != null && !contactInfo.getOrganisation().isEmpty()) { + result.setOrganization( + JsonUtils.getLangString(contactInfo.getOrganisation())); + } + if (StringUtils.hasText(contactInfo.getEmail())) { + result.getEmails().add(new OgcApiEmail(contactInfo.getEmail())); + } + + if (StringUtils.hasText(contactInfo.getPosition())) { + result.setPosition(contactInfo.getPosition()); + } + + if (StringUtils.hasText(contactInfo.getIndividual())) { + result.setName(contactInfo.getIndividual()); + } + + if (StringUtils.hasText(contactInfo.getPosition())) { + result.setPosition(contactInfo.getPosition()); + } + + if (StringUtils.hasText(contactInfo.getPhone())) { + result.getPhones().add(new OgcApiPhone(contactInfo.getPhone())); + } + + if (StringUtils.hasText(contactInfo.getAddress())) { + result.getAddresses().add(new OgcApiAddress(contactInfo.getAddress())); + } + + if (StringUtils.hasText(contactInfo.getLogo())) { + var logo = new OgcApiLink(contactInfo.getLogo(), "image/*"); + logo.setRel("icon"); + result.setLogo(logo); + } + + if (StringUtils.hasText(contactInfo.getWebsite())) { + var website = new OgcApiLink(contactInfo.getWebsite(), "text/html"); + result.getLinks().add(website); + } + + return result; + } + + /** + * create from the Elastic Index JSON node for the contact. + * + * @param contactMap from Elastic Index for a single contact + * @return parsed contact + */ + public static OgcApiContact fromIndexMap(Map contactMap) { + if (contactMap == null) { + return null; + } + OgcApiContact result = new OgcApiContact(); + if (StringUtils.hasText((String) contactMap.get("role"))) { + result.getRoles().add(contactMap.get("role").toString()); + } + if (contactMap.get("organisationObject") != null) { + result.setOrganization( + JsonUtils.getLangString(contactMap.get("organisationObject"))); + } + if (StringUtils.hasText((String) contactMap.get("email"))) { + result.getEmails().add(new OgcApiEmail(contactMap.get("email").toString())); + } + + if (StringUtils.hasText((String) contactMap.get("position"))) { + result.setPosition(contactMap.get("position").toString()); + } + + if (StringUtils.hasText((String) contactMap.get("individual"))) { + result.setName(contactMap.get("individual").toString()); + } + + if (StringUtils.hasText((String) contactMap.get("position"))) { + result.setPosition(contactMap.get("position").toString()); + } + + if (StringUtils.hasText((String) contactMap.get("phone"))) { + result.getPhones().add(new OgcApiPhone(contactMap.get("phone").toString())); + } + + if (StringUtils.hasText((String) contactMap.get("address"))) { + result.getAddresses().add(new OgcApiAddress(contactMap.get("address").toString())); + } + + if (StringUtils.hasText((String) contactMap.get("logo"))) { + var logo = new OgcApiLink(contactMap.get("logo").toString(), "image/*"); + logo.setRel("icon"); + result.setLogo(logo); + } + + if (StringUtils.hasText((String) contactMap.get("website"))) { + var website = new OgcApiLink(contactMap.get("website").toString(), "text/html"); + result.getLinks().add(website); + } + + return result; + } + + + public String getIdentifier() { + return identifier; + } + + public void setIdentifier(String identifier) { + this.identifier = identifier; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getPosition() { + return position; + } + + public void setPosition(String position) { + this.position = position; + } + + public String getOrganization() { + return organization; + } + + public void setOrganization(String organization) { + this.organization = organization; + } + + public OgcApiLink getLogo() { + return logo; + } + + public void setLogo(OgcApiLink logo) { + this.logo = logo; + } + + public List getPhones() { + return phones; + } + + public void setPhones(List phones) { + this.phones = phones; + } + + public List getEmails() { + return emails; + } + + public void setEmails(List emails) { + this.emails = emails; + } + + public List getAddresses() { + return addresses; + } + + public void setAddresses(List addresses) { + this.addresses = addresses; + } + + public List getLinks() { + return links; + } + + public void setLinks(List links) { + this.links = links; + } + + public String getHoursOfService() { + return hoursOfService; + } + + public void setHoursOfService(String hoursOfService) { + this.hoursOfService = hoursOfService; + } + + public String getContactInstructions() { + return contactInstructions; + } + + public void setContactInstructions(String contactInstructions) { + this.contactInstructions = contactInstructions; + } + + public List getRoles() { + return roles; + } + + public void setRoles(List roles) { + this.roles = roles; + } +} diff --git a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiEmail.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiEmail.java new file mode 100644 index 00000000..6d84cdb4 --- /dev/null +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiEmail.java @@ -0,0 +1,64 @@ +/** + * (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 com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import java.util.ArrayList; +import java.util.List; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; +import javax.xml.bind.annotation.XmlRootElement; + + +/** + * cf. https://github.com/opengeospatial/ogcapi-records/blob/master/core/openapi/schemas/contact.yaml + * + *

Email addresses at which contact can be made. + */ +@XmlRootElement(name = "email") +@XmlAccessorType(XmlAccessType.FIELD) +public class OgcApiEmail { + + /** + * The value is the email number itself. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "value") + @XmlElement(name = "value") + private String value; + + /** + * The type of email (e.g. home, work, etc.). + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "roles") + @XmlElement(name = "roles") + private List roles = new ArrayList<>(); + + public OgcApiEmail(String email) { + value = email; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public List getRoles() { + return roles; + } + + public void setRoles(List roles) { + this.roles = roles; + } +} + diff --git a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiExtent.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiExtent.java new file mode 100644 index 00000000..e92fcdde --- /dev/null +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiExtent.java @@ -0,0 +1,63 @@ +/** + * (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 com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; + +/** + * https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/schemas/extent.yaml + * + *

The extent of the features in the collection. In the Core only spatial and temporal extents + * are specified. Extensions may add additional members to represent other extents, for example, + * thermal or pressure ranges. + * + *

An array of extents is provided for each extent type (spatial, temporal). The first item in + * the array describes the overall extent of the data. All subsequent items describe more precise + * extents, e.g., to identify clusters of data. Clients only interested in the overall extent will + * only need to access the first extent in the array. + */ +public class OgcApiExtent { + + /** + * The spatial extent of the features in the collection. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "spatial") + @XmlElement(name = "spatial") + public OgcApiSpatialExtent spatial; + + /** + * The temporal extent of the features in the collection. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "temporal") + @XmlElement(name = "temporal") + public OgcApiTemporalExtent temporal; + + public OgcApiExtent(OgcApiSpatialExtent spatial, OgcApiTemporalExtent temporal) { + this.spatial = spatial; + this.temporal = temporal; + } + + public OgcApiSpatialExtent getSpatial() { + return spatial; + } + + public void setSpatial(OgcApiSpatialExtent spatial) { + this.spatial = spatial; + } + + public OgcApiTemporalExtent getTemporal() { + return temporal; + } + + public void setTemporal(OgcApiTemporalExtent temporal) { + this.temporal = temporal; + } +} diff --git a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiExternalDocumentation.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiExternalDocumentation.java new file mode 100644 index 00000000..8ce6ae23 --- /dev/null +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiExternalDocumentation.java @@ -0,0 +1,58 @@ +/** + * (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 com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; + +/** + * OgcApiExternalDocumentation. + * + *

cf. https://github.com/opengeospatial/ogcapi-records/blob/master/core/openapi/schemas/schema.yaml + * + *

I haven't included the "x-*" pattern properties. + */ +public class OgcApiExternalDocumentation { + + /** + * undefined in spec. + * + *

format: uri-reference + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "url") + @XmlElement(name = "url") + private String url; + + /** + * undefined in spec. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "description") + @XmlElement(name = "description") + private String description; + + //------------------------------------------ + + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } +} diff --git a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiInfo.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiInfo.java new file mode 100644 index 00000000..66197701 --- /dev/null +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiInfo.java @@ -0,0 +1,72 @@ +/** + * (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 com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; + + +/** + * cf https://github.com/opengeospatial/ogcapi-records/blob/master/core/openapi/schemas/schema.yaml + * + *

I haven't included the "x-*" pattern properties. + */ +public class OgcApiInfo { + + /** + * undefined in spec. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "title") + @XmlElement(name = "title") + private String title; + + /** + * undefined in spec. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "description") + @XmlElement(name = "description") + private String description; + + /** + * undefined in spec. + * + *

format: uri-reference + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "termsOfService") + @XmlElement(name = "termsOfService") + private String termsOfService; + + /** + * undefined in spec. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "contact") + @XmlElement(name = "contact") + private OgcApiSchemaContact contact; + + + /** + * undefined in spec. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "license") + @XmlElement(name = "license") + private OgcApiLicense license; + + /** + * undefined in spec. + * + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "version") + @XmlElement(name = "version") + private String version; +} diff --git a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiLanguage.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiLanguage.java new file mode 100644 index 00000000..ba52a986 --- /dev/null +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiLanguage.java @@ -0,0 +1,93 @@ +/** + * (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 com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; + +/** + * cf. https://github.com/opengeospatial/ogcapi-records/blob/master/core/openapi/schemas/language.yaml + * + *

The language used for textual values in this record. + */ +public class OgcApiLanguage { + + /** + * The language tag as per RFC-5646. example: el + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "code") + @XmlElement(name = "code") + public String code; + + /** + * The untranslated name of the language. example: Ελληνικά + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "name") + @XmlElement(name = "name") + public String name; + + /** + * The name of the language in another well-understood language, usually English. example: Greek + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "alternate") + @XmlElement(name = "alternate") + public String alternate; + + + /** + * The direction for text in this language. The default, `ltr` (left-to-right), represents the + * most common situation. However, care should be taken to set the value of `dir` appropriately if + * the language direction is not `ltr`. Other values supported are `rtl` (right-to-left), `ttb` + * (top-to-bottom), and `btt` (bottom-to-top). enum: - ltr - rtl - ttb - btt + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "dir") + @XmlElement(name = "dir") + public String dir; + + + public OgcApiLanguage(String code) { + this.code = code; + } + + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getAlternate() { + return alternate; + } + + public void setAlternate(String alternate) { + this.alternate = alternate; + } + + public String getDir() { + return dir; + } + + public void setDir(String dir) { + this.dir = dir; + } +} diff --git a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiLicense.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiLicense.java new file mode 100644 index 00000000..8ead1968 --- /dev/null +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiLicense.java @@ -0,0 +1,58 @@ +/** + * (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 com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; + +/** + * OgcApiLicense. + * + *

cf https://github.com/opengeospatial/ogcapi-records/blob/master/core/openapi/schemas/schema.yaml + * + *

I haven't included the "x-*" pattern properties. + */ +public class OgcApiLicense { + + /** + * undefined in spec. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "name") + @XmlElement(name = "name") + private String name; + + + /** + * undefined in spec. + * + *

format: uri-reference + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "url") + @XmlElement(name = "url") + private String url; + + //----------------------------------------------------- + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } +} diff --git a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiLink.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiLink.java new file mode 100644 index 00000000..69897aac --- /dev/null +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiLink.java @@ -0,0 +1,252 @@ +/** + * (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 com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import java.util.Objects; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * https://github.com/opengeospatial/ogcapi-records/blob/master/core/openapi/schemas/linkBase.yaml + * + *

also see: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/schemas/link.yaml + * + *

NOTE: + * These specs aren't really consistent. The first: + * a) doesn't have a href (this is obviously a mistake). + * b) has a created/updated field (not in the other spec). Recommend not using this. + * + *

Note - there is a length field, but its value isn't defined. Recommend not using this. + * + *

Represents a link. + */ +@XmlRootElement(name = "link") +@XmlAccessorType(XmlAccessType.FIELD) +public class OgcApiLink { + + /** + * The type or semantics of the relation. example: alternate + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "rel") + @XmlElement(name = "rel") + private String rel; + + /** + * A hint indicating what the media type of the result of dereferencing the link should be. + * example: application/geo+json + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "type") + @XmlElement(name = "type") + private String type; + + /** + * A hint indicating what the language of the result of dereferencing the link should be. example: + * en + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "hreflang") + @XmlElement(name = "hreflang") + private String hreflang; + + /** + * Used to label the destination of a link such that it can be used as a human-readable + * identifier. example: "Trierer Strasse 70, 53115 Bonn" + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "title") + @XmlElement(name = "title") + private String title; + + /** + * undocumented in spec. + * + *

This probably shouldn't be used because its not defined. + */ + @JsonInclude(Include.NON_DEFAULT) + @XmlElementWrapper(name = "length") + @XmlElement(name = "length") + private Integer length; + + /** + * Date of creation of the resource pointed to by the link. format: date-time + * + *

This is only is one of the link specifications. This probably shouldn't be used. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "created") + @XmlElement(name = "created") + private String created; + + /** + * description: Most recent date on which the resource pointed to by the link was changed. format: + * date-time + * + *

This is only is one of the link specifications. This probably shouldn't be used. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "updated") + @XmlElement(name = "updated") + private String updated; + + /** + * example: http://data.example.com/buildings/123 + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "href") + @XmlElement(name = "href") + private String href; + + + public OgcApiLink() { + + } + + public OgcApiLink(String href, String type) { + this.href = href; + this.type = type; + } + + + public String getRel() { + return rel; + } + + public void setRel(String rel) { + this.rel = rel; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getHreflang() { + return hreflang; + } + + public void setHreflang(String hreflang) { + this.hreflang = hreflang; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public Integer getLength() { + return length; + } + + public void setLength(Integer length) { + this.length = length; + } + + public String getCreated() { + return created; + } + + public void setCreated(String created) { + this.created = created; + } + + public String getUpdated() { + return updated; + } + + public void setUpdated(String updated) { + this.updated = updated; + } + + public String getHref() { + return href; + } + + public void setHref(String href) { + this.href = href; + } + + + @Override + public boolean equals(java.lang.Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + OgcApiLink link = (OgcApiLink) o; + return Objects.equals(this.href, link.getHref()) + && Objects.equals(this.rel, link.getRel()) + && Objects.equals(this.type, link.getType()) + && Objects.equals(this.hreflang, link.getHreflang()) + + && Objects.equals(this.created, link.getCreated()) + && Objects.equals(this.updated, link.getUpdated()) + && Objects.equals(this.length, link.getLength()); + } + + @Override + public int hashCode() { + return Objects.hash(href, rel, type, hreflang); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class Link {\n"); + + sb.append(" href: ").append(toIndentedString(href)).append("\n"); + sb.append(" rel: ").append(toIndentedString(rel)).append("\n"); + sb.append(" type: ").append(toIndentedString(type)).append("\n"); + sb.append(" hreflang: ").append(toIndentedString(hreflang)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first + * line). + */ + private String toIndentedString(java.lang.Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + public OgcApiLink href(String s) { + setHref(s); + return this; + } + + public OgcApiLink title(String s) { + setTitle(s); + return this; + } + + public OgcApiLink rel(String s) { + setRel(s); + return this; + } + + public OgcApiLink type(String s) { + setType(s); + return this; + } +} diff --git a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiPath.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiPath.java new file mode 100644 index 00000000..21d50493 --- /dev/null +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiPath.java @@ -0,0 +1,19 @@ +/** + * (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; + + +/** + * OgcApiPath. + * + *

https://github.com/opengeospatial/ogcapi-records/blob/master/core/openapi/schemas/schema.yaml + * + *

TODO -This is a very complex object with many pattern properties. I've left this empty for + * now. When this is needed, please fill in (also, include the x-* properties used elsewhere). + */ +public class OgcApiPath { + +} diff --git a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiPhone.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiPhone.java new file mode 100644 index 00000000..e92b0f4f --- /dev/null +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiPhone.java @@ -0,0 +1,58 @@ +/** + * (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 com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import java.util.ArrayList; +import java.util.List; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * cf. https://github.com/opengeospatial/ogcapi-records/blob/master/core/openapi/schemas/contact.yaml + * + *

Represents the Contact's phone number. + */ +@XmlRootElement(name = "phone") +@XmlAccessorType(XmlAccessType.FIELD) +public class OgcApiPhone { + + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "value") + @XmlElement(name = "value") + private String value; + + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "roles") + @XmlElement(name = "roles") + private List roles = new ArrayList<>(); + + + public OgcApiPhone(String phoneNumber) { + value = phoneNumber; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public List getRoles() { + return roles; + } + + public void setRoles(List roles) { + this.roles = roles; + } + +} diff --git a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiReference.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiReference.java new file mode 100644 index 00000000..95574c6b --- /dev/null +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiReference.java @@ -0,0 +1,37 @@ +/** + * (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 com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; + +/** + * OgcApiReference. + * cf https://github.com/opengeospatial/ogcapi-records/blob/master/core/openapi/schemas/schema.yaml + */ +public class OgcApiReference { + + /** + * format: uri-reference. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "ref") + @XmlElement(name = "ref$") + public String ref; + + //------------------------------------------------ + + + public String getRef() { + return ref; + } + + public void setRef(String ref) { + this.ref = ref; + } +} diff --git a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiSchema.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiSchema.java new file mode 100644 index 00000000..6d149c5b --- /dev/null +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiSchema.java @@ -0,0 +1,154 @@ +/** + * (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 com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import java.util.List; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * https://github.com/opengeospatial/ogcapi-records/blob/master/core/openapi/schemas/schema.yaml + * + *

A scheme related to this catalog. + * + *

The description of OpenAPI v3.0.x documents, as defined by + * https://spec.openapis.org/oas/v3.0.3 + * + *

NOTE - THIS ISN'T WELL DEFINED IN THE SPECIFICATION (OR .yaml). + * I've done my best. + * + *

I haven't included the "x-*" pattern properties + * + *

NOTE - THE SCHEMA OBJECT ISN'T FULLY DESCRIBED IN THE SUB-PROPERTIES SINCE THEY CONTAIN + * DIFFERENT TYPES OF PATTERN PROPERTIES + * + *

TODO - when these objects are needed (currently not). Please go through the spec and + * .yaml to determine what's needed and that all the objects are fully filled out. + */ +@XmlRootElement(name = "schema") +@XmlAccessorType(XmlAccessType.FIELD) +public class OgcApiSchema { + + /** + * Undefined in the spec. Likely the openapi version number. + * + *

pattern: ^3\.0\.\d(-.+)?$ + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "openapi") + @XmlElement(name = "openapi") + private String openapi; + + + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "info") + @XmlElement(name = "info") + private OgcApiInfo info; + + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "externalDocs") + @XmlElement(name = "externalDocs") + private OgcApiExternalDocumentation externalDocs; + + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "servers") + @XmlElement(name = "servers") + private List servers; + + + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "security") + @XmlElement(name = "security") + private List security; + + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "tags") + @XmlElement(name = "tags") + private List tags; + + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "paths") + @XmlElement(name = "paths") + private OgcApiPath paths; + + + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "components") + @XmlElement(name = "components") + private OgcApiComponent components; + + //--------------------------------------------------------------------- + + + public String getOpenapi() { + return openapi; + } + + public void setOpenapi(String openapi) { + this.openapi = openapi; + } + + public OgcApiInfo getInfo() { + return info; + } + + public void setInfo(OgcApiInfo info) { + this.info = info; + } + + public OgcApiExternalDocumentation getExternalDocs() { + return externalDocs; + } + + public void setExternalDocs(OgcApiExternalDocumentation externalDocs) { + this.externalDocs = externalDocs; + } + + public List getServers() { + return servers; + } + + public void setServers(List servers) { + this.servers = servers; + } + + public List getSecurity() { + return security; + } + + public void setSecurity(List security) { + this.security = security; + } + + public List getTags() { + return tags; + } + + public void setTags(List tags) { + this.tags = tags; + } + + public OgcApiPath getPaths() { + return paths; + } + + public void setPaths(OgcApiPath paths) { + this.paths = paths; + } + + public OgcApiComponent getComponents() { + return components; + } + + public void setComponents(OgcApiComponent components) { + this.components = components; + } +} diff --git a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiSchemaContact.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiSchemaContact.java new file mode 100644 index 00000000..c2a425e3 --- /dev/null +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiSchemaContact.java @@ -0,0 +1,76 @@ +/** + * (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 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 different (simpler) from the "normal" OgcApiContact. + * + *

cf. https://github.com/opengeospatial/ogcapi-records/blob/master/core/openapi/schemas/schema.yaml + * + *

I haven't included the "x-*" pattern properties. + */ +public class OgcApiSchemaContact { + + /** + * undefined in spec. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "name") + @XmlElement(name = "name") + private String name; + + /** + * undefined in spec. + * + *

format: uri-reference + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "url") + @XmlElement(name = "url") + private String url; + + /** + * undefined in spec. + * + *

format: email + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "email") + @XmlElement(name = "email") + private String email; + + //----------------------------------------------------- + + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } +} diff --git a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiSecurityRequirement.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiSecurityRequirement.java new file mode 100644 index 00000000..bb8bafdb --- /dev/null +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiSecurityRequirement.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 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; + +/** + * OgcApiSecurityRequirement. + * + *

https://github.com/opengeospatial/ogcapi-records/blob/master/core/openapi/schemas/schema.yaml + */ +public class OgcApiSecurityRequirement { + + /** + * undefined in spec. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "items") + @XmlElement(name = "items") + private List items; + + //------------------------------------------ + + + public List getItems() { + return items; + } + + public void setItems(List items) { + this.items = items; + } +} diff --git a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiServer.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiServer.java new file mode 100644 index 00000000..f21f24b5 --- /dev/null +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiServer.java @@ -0,0 +1,59 @@ +/** + * (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 com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; + +/** + * OgcApiServer. + * + *

https://github.com/opengeospatial/ogcapi-records/blob/master/core/openapi/schemas/schema.yaml + * + *

I haven't included the "x-*" pattern properties. + * + *

I haven't included the variables ("ServerVariable") additional properties. + */ +public class OgcApiServer { + + /** + * undefined in spec. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "url") + @XmlElement(name = "url") + private String url; + + /** + * undefined in spec. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "description") + @XmlElement(name = "description") + private String description; + + + //---------------------------------------------------------- + + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } +} diff --git a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiSpatialExtent.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiSpatialExtent.java new file mode 100644 index 00000000..35d62da5 --- /dev/null +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiSpatialExtent.java @@ -0,0 +1,155 @@ +/** + * (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 static org.fao.geonet.ogcapi.records.util.JsonUtils.getAsDouble; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; + + +/** + * cf. https://github.com/opengeospatial/ogcapi-features/blob/master/core/openapi/schemas/extent.yaml + * + *

The spatial extent of the features in the collection. + */ +public class OgcApiSpatialExtent { + + public static final String CRS84 = "http://www.opengis.net/def/crs/OGC/1.3/CRS84"; + public static final String CRS84h = "http://www.opengis.net/def/crs/OGC/0/CRS84h"; + + /** + * One or more bounding boxes that describe the spatial extent of the dataset. In the Core only a + * single bounding box is supported. + * + *

Extensions may support additional areas. The first bounding box describes the overall + * spatial extent of the data. All subsequent bounding boxes describe more precise bounding boxes, + * e.g.,to identify clusters of data. Clients only interested in the overall spatial extent will + * only need to access the first bounding box in the array. + * + *

---- + * + *

Each bounding box is provided as four or six numbers, depending on whether the coordinate + * reference system includes a vertical axis (height or depth): + * + *

Lower left corner, coordinate axis 1 Lower left corner, coordinate axis 2 Minimum value, + * coordinate axis 3 (optional) Upper right corner, coordinate axis 1 Upper right corner, + * coordinate axis 2 Maximum value, coordinate axis 3 (optional) + * + *

If the value consists of four numbers, the coordinate reference system is WGS 84 + * longitude/latitude (http://www.opengis.net/def/crs/OGC/1.3/CRS84) unless a different coordinate + * reference system is specified in `crs`. + * + *

If the value consists of six numbers, the coordinate reference system is WGS 84 + * longitude/latitude/ellipsoidal height (http://www.opengis.net/def/crs/OGC/0/CRS84h) unless a + * different coordinate reference system is specified in `crs`. + * + *

For WGS 84 longitude/latitude the values are in most cases the sequence of minimum + * longitude, minimum latitude, maximum longitude and maximum latitude. However, in cases where + * the box spans the antimeridian the first value (west-most box edge) is larger than the third + * value (east-most box edge). + * + *

If the vertical axis is included, the third and the sixth number are the bottom and the + * top of the 3-dimensional bounding box. + * + *

If a feature has multiple spatial geometry properties, it is the decision of the server + * whether only a single spatial geometry property is used to determine the extent or all relevant + * geometries. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "bbox") + @XmlElement(name = "bbox") + public List bbox = new ArrayList<>(); + /** + * Coordinate reference system of the coordinates in the spatial extent (property `bbox`). The + * default reference system is WGS 84 longitude/latitude. In the Core the only other supported + * coordinate reference system is WGS 84 longitude/latitude/ellipsoidal height for coordinates + * with height. Extensions may support additional coordinate reference systems and add additional + * enum values. + * + *

enum: - 'http://www.opengis.net/def/crs/OGC/1.3/CRS84' - 'http://www.opengis.net/def/crs/OGC/0/CRS84h' + * default: 'http://www.opengis.net/def/crs/OGC/1.3/CRS84' + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "crs") + @XmlElement(name = "crs") + public String crs = CRS84; + + /** + * Create OgcApiSpatialExtent from the "geom" section of the GN Index record. Typically this is a + * polygon: + * + *

"geom":[{"type":"Polygon","coordinates": + * [[[37.0,-3.0],[156.0,-3.0],[156.0,83.0],[37.0,83.0],[37.0,-3.0]]]}] + * + *

you should call this method with the {"type":"Polygon", + * "coordinates":[[[37.0,-3.0],[156.0,-3.0],[156.0,83.0],[37.0,83.0],[37.0,-3.0]]]} object + * + * @param map from the GN Index Record JSON + * @return parsed OgcApiSpatialExtent + */ + public static OgcApiSpatialExtent fromGnIndexRecord(Map map) { + if (map == null + || !map.containsKey("type") + || !map.get("type").equals("Polygon") + || !map.containsKey("coordinates") + || !(map.get("coordinates") instanceof List)) { + return null; + } + var coords = (List) ((List) map.get("coordinates")).get(0); + + double xmin = Double.MAX_VALUE; + double ymin = Double.MAX_VALUE; + double xmax = Double.MIN_VALUE; + double ymax = Double.MIN_VALUE; + + for (var myCoord : coords) { + var coord = (List) myCoord; + var x = getAsDouble(coord.get(0)); + var y = getAsDouble(coord.get(1)); + if (x < xmin) { + xmin = x; + } + if (y < ymin) { + ymin = y; + } + if (x > xmax) { + xmax = x; + } + if (y > ymax) { + ymax = y; + } + } + + var result = new OgcApiSpatialExtent(); + result.crs = CRS84; + result.bbox = Arrays.asList(xmin, ymin, xmax, ymax); + return result; + } + + + public List getBbox() { + return bbox; + } + + public void setBbox(List bbox) { + this.bbox = bbox; + } + + public String getCrs() { + return crs; + } + + public void setCrs(String crs) { + this.crs = crs; + } +} diff --git a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiTag.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiTag.java new file mode 100644 index 00000000..ed7eac15 --- /dev/null +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiTag.java @@ -0,0 +1,47 @@ +/** + * (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 com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; + +/** + * OgcApiTag. + * + *

cf. https://github.com/opengeospatial/ogcapi-records/blob/master/core/openapi/schemas/schema.yaml + * + *

I haven't included the "x-*" pattern properties. + */ +public class OgcApiTag { + + /** + * undefined in spec. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "name") + @XmlElement(name = "name") + private String name; + + /** + * undefined in spec. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "description") + @XmlElement(name = "description") + private String description; + + /** + * undefined in spec. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "externalDocs") + @XmlElement(name = "externalDocs") + private OgcApiExternalDocumentation externalDocs; + +} diff --git a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiTemporalExtent.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiTemporalExtent.java new file mode 100644 index 00000000..83357956 --- /dev/null +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiTemporalExtent.java @@ -0,0 +1,122 @@ +/** + * (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 com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; +import org.fao.geonet.index.model.gn.DateRange; + +/** + * cf. https://github.com/opengeospatial/ogcapi-features/blob/master/core/openapi/schemas/extent.yaml + * + *

The temporal extent of the features in the collection. + */ +public class OgcApiTemporalExtent { + + public static final String DefaultTRS = "http://www.opengis.net/def/uom/ISO-8601/0/Gregorian"; + /** + * One or more time intervals that describe the temporal extent of the dataset. In the Core only a + * single time interval is supported. + * + *

Extensions may support multiple intervals. The first time interval describes the overall + * temporal extent of the data. All subsequent time intervals describe more precise time + * intervals, e.g., to identify clusters of data. Clients only interested in the overall temporal + * extent will only need to access the first time interval in the array (a pair of lower and upper + * bound instants). + * + *

--- + * + *

Begin and end times of the time interval. The timestamps are in the temporal coordinate + * reference system specified in `trs`. By default this is the Gregorian calendar. + * + *

The value `null` at start or end is supported and indicates a half-bounded interval. type: + * array minItems: 2 maxItems: 2 + * + *

format: date-time nullable: true example: - '2011-11-11T12:22:11Z' - null + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "interval") + @XmlElement(name = "interval") + public List interval = new ArrayList<>(); + + /** + * not yet official. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "resolution") + @XmlElement(name = "resolution") + public String resolution; + + /** + * Coordinate reference system of the coordinates in the temporal extent (property `interval`). + * The default reference system is the Gregorian calendar. In the Core this is the only supported + * temporal coordinate reference system. Extensions may support additional temporal coordinate + * reference systems and add additional enum values. + * + *

enum: - 'http://www.opengis.net/def/uom/ISO-8601/0/Gregorian' default: + * 'http://www.opengis.net/def/uom/ISO-8601/0/Gregorian' + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "trs") + @XmlElement(name = "trs") + public String trs = DefaultTRS; + + /** + * Create OgcApiTemporalExtent from the "resourceTemporalExtentDateRange" section of the GN Index + * record. Typically this looks like this: + * + *

"resourceTemporalExtentDateRange": [ { "gte": "2000-01-01T12:29:00.000Z", "lte": + * "2008-01-08T12:29:00.000Z" } ], + * + *

you should call this method with the { "gte": "2000-01-01T12:29:00.000Z", "lte": + * "2008-01-08T12:29:00.000Z" } object + * + * @param map from the Elastic Index Record + * @return parsed OgcApiTemporalExtent + */ + public static OgcApiTemporalExtent fromGnIndexRecord(DateRange map) { + if (map == null) { + return null; + } + + String start = map.getGte(); + String end = map.getLte(); + + var result = new OgcApiTemporalExtent(); + result.trs = DefaultTRS; + result.interval = Arrays.asList(start, end); + return result; + } + + public List getInterval() { + return interval; + } + + public void setInterval(List interval) { + this.interval = interval; + } + + public String getResolution() { + return resolution; + } + + public void setResolution(String resolution) { + this.resolution = resolution; + } + + public String getTrs() { + return trs; + } + + public void setTrs(String trs) { + this.trs = trs; + } +} diff --git a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiTheme.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiTheme.java new file mode 100644 index 00000000..ded41a77 --- /dev/null +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/model/OgcApiTheme.java @@ -0,0 +1,125 @@ +/** + * (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 static org.fao.geonet.ogcapi.records.util.JsonUtils.getAsString; +import static org.fao.geonet.ogcapi.records.util.JsonUtils.getLangString; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; +import org.fao.geonet.index.model.gn.Theme; +import org.springframework.util.StringUtils; + +/** + * cf https://github.com/opengeospatial/ogcapi-records/blob/master/core/openapi/schemas/theme.yaml + * + *

This is the ogcapi "theme" object model. + * + *

From ogcapi-records spec: + * + *

Themes are concepts associated with the resource(s) that a record describes taken from + * one or more formal knowledge organization systems or schemes. + */ +public class OgcApiTheme { + + /** + * One or more entity/concept identifiers from this knowledge system. it is recommended that a + * resolvable URI be used for each entity/concept identifier. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "concepts") + @XmlElement(name = "concepts") + public List concepts; + + /** + * An identifier for the knowledge organization system used to classify the resource. It is + * recommended that the identifier be a resolvable URI. The list of schemes used in a searchable + * catalog can be determined by inspecting the server's OpenAPI document or, if the server + * implements CQL2, by exposing a queryable (e.g. named `scheme`) and enumerating the list of + * schemes in the queryable's schema definition. + */ + @JsonInclude(Include.NON_EMPTY) + @XmlElementWrapper(name = "schema") + @XmlElement(name = "schema") + public String schema; + + public OgcApiTheme(String schema) { + this.schema = schema; + this.concepts = new ArrayList<>(); + } + + /** + * Parse the Elastic (JSON) Index document. Call with linkedServiceRecord.get("allKeyWords"). + * + * @param map map from Elastic (JSON) Index document representing "allKeyWords" + * @return map will be parsed into a corresponding List of OgcApiTheme + */ + public static List parseElasticIndex(Map map) { + if (map == null) { + return null; + } + List result = new ArrayList<>(); + for (var theme : map.values()) { + var themeSchema = getAsString(theme.getLink()); + //at least put something here! + if (!StringUtils.hasText(themeSchema) + && theme.getMultilingualTitle() != null + && !theme.getMultilingualTitle().isEmpty()) { + var multilingualTitle = theme.getMultilingualTitle(); + var multilingualTitleLink = multilingualTitle.get("link"); + + if (StringUtils.hasText(multilingualTitleLink)) { + themeSchema = multilingualTitleLink; + } else { + themeSchema = getLangString(multilingualTitle); + } + } + if (!StringUtils.hasText(themeSchema) && theme.getTitle() != null) { + themeSchema = theme.getTitle(); + } + if (!StringUtils.hasText(themeSchema) && theme.getTheme() != null) { + themeSchema = theme.getTheme(); + } + if (!StringUtils.hasText(themeSchema) && theme.getId() != null) { + themeSchema = theme.getId(); + } + var theme2 = new OgcApiTheme(themeSchema); + + List conceptsList = theme.getKeywords(); + for (var anConcept : conceptsList) { + var concept = (Map) anConcept; + var link = getAsString(concept.get("link")); + var ogcConcept = new OgcApiConcept(getLangString(anConcept), link); + theme2.getConcepts().add(ogcConcept); + } + if (theme2.getConcepts().size() != 0) { + result.add(theme2); + } + } + return result; + } + + public List getConcepts() { + return concepts; + } + + public void setConcepts(List concepts) { + this.concepts = concepts; + } + + public String getSchema() { + return schema; + } + + public void setSchema(String schema) { + this.schema = schema; + } +} 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 + + } + +} 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 new file mode 100644 index 00000000..4231816e --- /dev/null +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/service/RecordService.java @@ -0,0 +1,182 @@ +/** + * (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.service; + +import static org.fao.geonet.ogcapi.records.controller.ItemApiController.EXCEPTION_COLLECTION_ITEM_NOT_FOUND; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Map; +import javax.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.fao.geonet.common.search.ElasticSearchProxy; +import org.fao.geonet.domain.Setting; +import org.fao.geonet.domain.Source; +import org.fao.geonet.domain.SourceType; +import org.fao.geonet.ogcapi.records.util.RecordsEsQueryBuilder; +import org.fao.geonet.repository.SettingRepository; +import org.fao.geonet.repository.SourceRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.MessageSource; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import org.springframework.web.server.ResponseStatusException; + + +@Service +@Slf4j(topic = "org.fao.geonet.ogcapi.records") +public class RecordService { + + @Autowired + CollectionService collectionService; + + @Autowired + RecordsEsQueryBuilder recordsEsQueryBuilder; + + @Autowired + ElasticSearchProxy proxy; + + @Autowired + MessageSource messages; + + + @Autowired + SettingRepository settingRepository; + + @Autowired + SourceRepository sourceRepository; + + + /** + * For a source (sub-portal), get the ServiceRecord's uuid. + * 1. usually, its part of the source object (source#getServiceRecord()) + * 2. however, if its the full-portal, there is no GUI to edit this. In this case + * we use the CSW setting (system/csw/capabilityRecordUuid). + * @param source which sub-portal? + * @return uuid of the linked record or null + */ + public String getLinkedRecordUuid(Source source) { + if (source == null) { + return null; + } + + var uuid = source.getServiceRecord(); + if (StringUtils.isEmpty(uuid) || uuid.trim().equals("-1")) { + if (source.getType().equals(SourceType.portal)) { + // this is the main portal - we have a 2nd way to get the link (admin->settings->CSW) + Setting setting = settingRepository.getOne("system/csw/capabilityRecordUuid"); + uuid = setting.getStoredValue(); + } + } + if (StringUtils.isEmpty(uuid) || uuid.trim().equals("-1")) { + return null; + } + return uuid; + } + + + /** + * Gets the main GN portal (from GN DB table "sources"). + * + * @return Gets the main GN portal (from GN DB table "sources") + */ + public Source getMainPortal() { + return sourceRepository.findByType(SourceType.portal, null).get(0); + } + + + /** + * Given a source, get the parsed Elastic Index JSON. + * + * @param request user request (for security) + * @param source which sub-portal to get the linked serviceRecord + * @return parsed Elastic Index JSON for the source's linked serviceRecord + */ + public Map getLinkedServiceRecord(HttpServletRequest request, + Source source) { + + var mainPortal = getMainPortal(); + + var uuid = getLinkedRecordUuid(source); + + if (StringUtils.isEmpty(uuid) || uuid.trim().equals("-1")) { + return null; + } + try { + var info = getRecordAsJson(mainPortal.getUuid(), + uuid, request, mainPortal, + "json") + .get("_source"); + ObjectMapper mapper = new ObjectMapper(); + Map result = mapper.convertValue(info, + new TypeReference>() { + }); + return result; + } catch (Exception ex) { + // mislinked record? not published? error processing the record? + log.error(String.format( + "An error occurred while trying to retrieve the linked ServiceRecord for sub-portal " + + "'%s' (link uuid '%s'). Error is: %s", + source.getUuid(), uuid, ex.getMessage())); + } + return null; + } + + + /** + * Get the Elastic Index JSON. + * + * @param collectionId which collection is the record apart of + * @param recordId which uuid to get + * @param request incomming user request (for security) + * @param source from the GN DB "source" table for this collectionId + * @param type what type of record + * @return parsed as a JSON object + * @throws Exception if there was a problem retrieving the record + */ + public JsonNode getRecordAsJson( + String collectionId, + String recordId, + HttpServletRequest request, + Source source, + String type) throws Exception { + + String collectionFilter = collectionService.retrieveCollectionFilter(source, true); + String query = recordsEsQueryBuilder.buildQuerySingleRecord(recordId, collectionFilter, null); + + String queryResponse = proxy.searchAndGetResult(request.getSession(), request, query, null); + + ObjectMapper mapper = new ObjectMapper(); + JsonFactory factory = mapper.getFactory(); + JsonParser parser = factory.createParser(queryResponse); + JsonNode actualObj = mapper.readTree(parser); + + JsonNode totalValue = + "json".equals(type) + ? actualObj.get("hits").get("total").get("value") + : actualObj.get("size"); + + if ((totalValue == null) || (totalValue.intValue() == 0)) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, + messages.getMessage(EXCEPTION_COLLECTION_ITEM_NOT_FOUND, + new String[]{recordId, collectionId}, + request.getLocale())); + } + + if ("json".equals(type)) { + return actualObj.get("hits").get("hits").get(0); + } else { + String elementName = "schema.org".equals(type) ? "dataFeedElement" : "features"; + return actualObj.get(elementName).get(0); + } + + } + +} diff --git a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/util/CollectionInfoBuilder.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/util/CollectionInfoBuilder.java index ac253470..caf23cd8 100644 --- a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/util/CollectionInfoBuilder.java +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/util/CollectionInfoBuilder.java @@ -1,34 +1,72 @@ +/** + * (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.util; +import static java.util.Arrays.asList; import static org.fao.geonet.ogcapi.records.util.LinksItemsBuilder.getHref; -import java.math.BigDecimal; import java.net.URI; -import java.util.Arrays; import java.util.List; import java.util.Optional; +import javax.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; import org.fao.geonet.common.search.SearchConfiguration; import org.fao.geonet.common.search.SearchConfiguration.Format; import org.fao.geonet.domain.Source; import org.fao.geonet.domain.SourceType; import org.fao.geonet.ogcapi.records.controller.model.CollectionInfo; -import org.fao.geonet.ogcapi.records.controller.model.Extent; -import org.fao.geonet.ogcapi.records.controller.model.Extent.CrsEnum; -import org.fao.geonet.ogcapi.records.controller.model.Extent.TrsEnum; -import org.fao.geonet.ogcapi.records.controller.model.Link; +import org.fao.geonet.ogcapi.records.controller.model.CrsEnum; +import org.fao.geonet.ogcapi.records.model.OgcApiExtent; +import org.fao.geonet.ogcapi.records.model.OgcApiLink; +import org.fao.geonet.ogcapi.records.model.OgcApiSpatialExtent; +import org.fao.geonet.ogcapi.records.service.RecordService; +import org.fao.geonet.repository.SettingRepository; +import org.fao.geonet.repository.SourceRepository; +import org.fao.geonet.view.ViewUtility; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +@Service +@Slf4j(topic = "org.fao.geonet.ogcapi.records") public class CollectionInfoBuilder { - private CollectionInfoBuilder() { - throw new IllegalStateException("Utility class"); + @Value("${gn.legacy.url}") + String geonetworkUrl; + + @Autowired + RecordService recordService; + @Autowired + ElasticIndexJson2CollectionInfo elasticIndexJson2CollectionInfo; + + + public CollectionInfoBuilder() { + } + /** * Build Collection info from source table. + * + * @param source from the GN DB Table "sources" + * @param language which language is the request wanting + * @param baseUrl base url for the ogcapi + * @param format what format are the results requested in + * @param configuration config for searching + * @param request user request (for security) + * @return CollectionInfo filled in from GN DB Table "sources" and the Elastic Index JSON */ - public static CollectionInfo buildFromSource(Source source, String language, - String baseUrl, Optional format, SearchConfiguration configuration) { + public CollectionInfo buildFromSource(Source source, + String language, + String baseUrl, + Optional format, + SearchConfiguration configuration, + HttpServletRequest request) { String name; if (source.getType() == SourceType.portal) { @@ -38,6 +76,7 @@ public static CollectionInfo buildFromSource(Source source, String language, } CollectionInfo collectionInfo = new CollectionInfo(); + collectionInfo.setId(name); String label = source.getLabel(language); // The source label may contain a description @@ -48,30 +87,48 @@ public static CollectionInfo buildFromSource(Source source, String language, collectionInfo.setTitle( titleAndDescription.length > 1 ? titleAndDescription[0] : label); collectionInfo - .setCrs(Arrays.asList(CrsEnum.HTTP_WWW_OPENGIS_NET_DEF_CRS_OGC_1_3_CRS84.getValue())); + .setCrs(asList(CrsEnum.HTTP_WWW_OPENGIS_NET_DEF_CRS_OGC_1_3_CRS84.getValue())); - // TODO: Review values - Extent extent = new Extent(); - extent.crs(CrsEnum.HTTP_WWW_OPENGIS_NET_DEF_CRS_OGC_1_3_CRS84); - extent.setSpatial(Arrays.asList(new BigDecimal(-180), new BigDecimal(-90), - new BigDecimal(180), new BigDecimal(90))); + var spatialExtent = new OgcApiSpatialExtent(); + spatialExtent.crs = OgcApiSpatialExtent.CRS84; + spatialExtent.setBbox(asList(-180.0, -90.0, 180.0, 90.0)); + var extent = new OgcApiExtent(spatialExtent, null); collectionInfo.setExtent(extent); - extent.setTrs(TrsEnum.HTTP_WWW_OPENGIS_NET_DEF_UOM_ISO_8601_0_GREGORIAN); - // TODO: Accept format parameter. baseUrl = baseUrl + (!baseUrl.endsWith("/") ? "/" : ""); URI collectionUri = URI.create(baseUrl).resolve(name); - Link currentDoc = new Link(); + OgcApiLink currentDoc = new OgcApiLink(); currentDoc.setRel("self"); currentDoc.setHref(getHref(collectionUri.toString(), format)); currentDoc.setType(format.get().getMimeType()); currentDoc.setHreflang(language); - List linkList = LinksItemsBuilder.build( + List linkList = LinksItemsBuilder.build( format, collectionUri.toString(), language, configuration); linkList.forEach(collectionInfo::addLinksItem); + + var gnbase = geonetworkUrl; + if (!gnbase.endsWith("/")) { + gnbase += "/"; + } + //assume its a png + var url = URI.create(gnbase).resolve("images/logos/" + source.getUuid() + ".png"); + var link = new OgcApiLink(); + link.setHref(url.toString()); + link.setRel("icon"); + link.setType("image/png"); + collectionInfo.addLinksItem(link); + + + var linkedServiceRecord = + recordService.getLinkedServiceRecord(request, source); + elasticIndexJson2CollectionInfo.injectLinkedServiceRecordInfo(collectionInfo, + linkedServiceRecord); + return collectionInfo; } + + } 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 new file mode 100644 index 00000000..454fb4bc --- /dev/null +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/util/ElasticIndexJson2CollectionInfo.java @@ -0,0 +1,298 @@ +package org.fao.geonet.ogcapi.records.util; + +import static org.fao.geonet.ogcapi.records.util.JsonUtils.getLangString; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.codehaus.jackson.map.type.TypeFactory; +import org.fao.geonet.index.model.gn.IndexRecord; +import org.fao.geonet.ogcapi.records.controller.model.CollectionInfo; +import org.fao.geonet.ogcapi.records.model.OgcApiContact; +import org.fao.geonet.ogcapi.records.model.OgcApiExtent; +import org.fao.geonet.ogcapi.records.model.OgcApiLanguage; +import org.fao.geonet.ogcapi.records.model.OgcApiSpatialExtent; +import org.fao.geonet.ogcapi.records.model.OgcApiTemporalExtent; +import org.fao.geonet.ogcapi.records.model.OgcApiTheme; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + + +/** + * Takes an Elastic Index Json and injects it inside a collectioninfo. + */ +@Service +@Slf4j(topic = "org.fao.geonet.ogcapi.records.util") +public class ElasticIndexJson2CollectionInfo { + + + /** + * inject the "extra" info from the LinkedServiceRecord into the CollectionInfo. + * + * @param collectionInfo collection metadata we've gathered so far (usually not much) + * @param linkedServiceRecord Parsed JSON map of the linked Service record (GN's DB "source" + * "serviceRecord") + */ + public void injectLinkedServiceRecordInfo(CollectionInfo collectionInfo, + Map linkedServiceRecord) { + if (linkedServiceRecord == null) { + return; + } + ObjectMapper objectMapper = org.fao.geonet.index.JsonUtils.getObjectMapper(); + + IndexRecord indexRecord = objectMapper.convertValue( + linkedServiceRecord, + IndexRecord.class); + + injectLinkedServiceRecordInfo(collectionInfo, indexRecord); + } + + /** + * inject the "extra" info from the LinkedServiceRecord into the CollectionInfo. + * + * @param collectionInfo collection metadata we've gathered so far (usually not much) + * @param indexRecord Parsed JSON of the linked Service record (GN's DB "source" + * "serviceRecord") + */ + public void injectLinkedServiceRecordInfo(CollectionInfo collectionInfo, + IndexRecord indexRecord) { + if (indexRecord == null) { + return; + } + + set(getTitle(collectionInfo, indexRecord), collectionInfo, "title"); + set(getDescription(collectionInfo, indexRecord), collectionInfo, "description"); + + set(getContacts(collectionInfo, indexRecord), collectionInfo, "contacts"); + + OgcApiSpatialExtent spatialExtent = getSpatialExtent(indexRecord); + OgcApiTemporalExtent temporalExtent = getTemporalExtent(indexRecord); + OgcApiExtent extent = new OgcApiExtent(spatialExtent, temporalExtent); + if (spatialExtent != null || temporalExtent != null) { + set(extent,collectionInfo, "extent"); + } + + set(getCrs(collectionInfo, indexRecord), collectionInfo, "crs"); + set(getCreateDate(collectionInfo, indexRecord), collectionInfo, "created"); + set(getChangeDate(collectionInfo, indexRecord), collectionInfo, "updated"); + set(getTags(collectionInfo, indexRecord), collectionInfo, "keywords"); + + set(getLanguage(collectionInfo, indexRecord), collectionInfo, "language"); + set(getOtherLangs(collectionInfo, indexRecord), collectionInfo, "languages"); + + set(getThemes(collectionInfo, indexRecord), collectionInfo, "themes"); + + set(getLicenses(collectionInfo, indexRecord), collectionInfo, "license"); + set(getRights(collectionInfo, indexRecord), collectionInfo, "rights"); + } + + + /** + * use reflection to set a value. We do this, so we can handle all sorts of different object (i.e. + * geojson) and parts of the various ogcapi specifications (not just record). + * + * @param val object value + * @param mainObject object to set the property on + * @param propertyName name of the property + */ + public void set(Object val, Object mainObject, String propertyName) { + if (val == null || mainObject == null || !StringUtils.hasText(propertyName)) { + return; //nothing to do + } + //see if there is a field of that name (should probably cache) + try { + var field = mainObject.getClass().getDeclaredField(propertyName); + field.setAccessible(true); + field.set(mainObject, val); + } catch (Exception e) { + return; + } + + } + + + + //its a bit unclear what to do here - there's a big difference between MD record, + // Elastic Index JSON versus what's expected in the ogcapi license field. We do the simple + // action + private String getRights(CollectionInfo collectionInfo, + IndexRecord indexRecord) { + var myLiceseList = indexRecord.getMdLegalConstraintsUseLimitationObject(); + if (myLiceseList != null && !myLiceseList.isEmpty()) { + if (myLiceseList.size() > 1) { + var license2 = getLangString(myLiceseList.get(1)); + return license2; + } + } + return null; + } + + + //its a bit unclear what to do here - there's a big difference between MD record, + // Elastic Index JSON versus what's expected in the ogcapi license field. We do the simple + // action + private String getLicenses(CollectionInfo collectionInfo, + IndexRecord indexRecord) { + var myLiceseList = indexRecord.getMdLegalConstraintsUseLimitationObject(); + if (myLiceseList != null && !myLiceseList.isEmpty()) { + if (myLiceseList.size() > 0) { + var license = getLangString(myLiceseList.get(0)); + return license; + } + + } + return null; + } + + //process allKeywords to get themes + private List getThemes(CollectionInfo collectionInfo, + IndexRecord indexRecord) { + var myThemeKeywords = indexRecord.getAllKeywords(); + return OgcApiTheme.parseElasticIndex(myThemeKeywords); + } + + //process otherLanguage to get the other languages + private ArrayList getOtherLangs(CollectionInfo collectionInfo, + IndexRecord indexRecord) { + var langs = indexRecord.getOtherLanguage(); + if (langs != null) { + var result = new ArrayList(); + for (var lang : langs) { + result.add(new OgcApiLanguage(lang)); + } + return result; + } + return null; + } + + //process main language + private OgcApiLanguage getLanguage(CollectionInfo collectionInfo, + IndexRecord indexRecord) { + var myMainLang = indexRecord.getMainLanguage(); + if (myMainLang != null && StringUtils.hasText(myMainLang)) { + return new OgcApiLanguage(myMainLang); + } + return null; + } + + //handle the "tags" to create keywords + private ArrayList getTags(CollectionInfo collectionInfo, IndexRecord indexRecord) { + var myTags = indexRecord.getTag(); + if (myTags != null) { + var result = new ArrayList(); + var tags = myTags; + collectionInfo.setKeywords(new ArrayList<>()); + for (var tag : tags) { + result.add(getLangString(tag)); + } + return result; + } + return null; + } + + //process the change date + private String getChangeDate(CollectionInfo collectionInfo, + IndexRecord indexRecord) { + var updateDate = indexRecord.getChangeDate(); + if (updateDate != null && StringUtils.hasText(updateDate)) { + return updateDate; + } + return null; + } + + //process the creation date + private String getCreateDate(CollectionInfo collectionInfo, + IndexRecord indexRecord) { + var createDate = indexRecord.getCreateDate(); + if (createDate != null && StringUtils.hasText(createDate)) { + return createDate; + } + return null; + } + + //process CRS + private ArrayList getCrs(CollectionInfo collectionInfo, IndexRecord indexRecord) { + var crss = indexRecord.getCoordinateSystem(); + var result = new ArrayList(); + if (crss != null) { + for (var crs : crss) { + result.add(crs); + } + return result; + } + return null; + } + + //combine spatial and temporal extent + private void handleExtent(CollectionInfo collectionInfo, OgcApiSpatialExtent spatialExtent, + OgcApiTemporalExtent temporalExtent) { + OgcApiExtent extent = new OgcApiExtent(spatialExtent, temporalExtent); + if (spatialExtent != null || temporalExtent != null) { + collectionInfo.setExtent(extent); + } + } + + //process Temporal Extent + private OgcApiTemporalExtent getTemporalExtent(IndexRecord indexRecord) { + OgcApiTemporalExtent temporalExtent = null; + var myTemporalExtent = indexRecord.getResourceTemporalExtentDateRange(); + if (myTemporalExtent != null || myTemporalExtent.isEmpty()) { + myTemporalExtent = indexRecord.getResourceTemporalDateRange(); + } + if (myTemporalExtent != null || myTemporalExtent.isEmpty()) { + temporalExtent = OgcApiTemporalExtent.fromGnIndexRecord(myTemporalExtent.get(0)); + } + return temporalExtent; + } + + //process the spatial extent + 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); + } + return spatialExtent; + } + + //process contracts + private List getContacts(CollectionInfo collectionInfo, + IndexRecord indexRecord) { + var contacts = indexRecord.getContact(); + if (contacts != null && !contacts.isEmpty()) { + var result = new ArrayList(); + + for (var contactInfo : contacts) { + var contact = OgcApiContact.fromIndexMap(contactInfo); + result.add(contact); + } + return result; + } + return null; + } + + //override description (abstract) from attached ServiceRecord + private String getDescription(CollectionInfo collectionInfo, + IndexRecord indexRecord) { + var desc = getLangString(indexRecord.getResourceAbstract()); + return desc; + } + + + //override title from attached ServiceRecord + private String getTitle(CollectionInfo collectionInfo, IndexRecord indexRecord) { + var title = getLangString(indexRecord.getResourceTitle()); + return title; + } +} diff --git a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/util/JsonUtils.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/util/JsonUtils.java new file mode 100644 index 00000000..fa4c5481 --- /dev/null +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/util/JsonUtils.java @@ -0,0 +1,61 @@ +/** + * (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.util; + +import java.util.Map; + +public class JsonUtils { + + /** + * todo - be language aware (send in desired language). Move to utility class. + * + * @param jsonNode json node for the potentially multi-language string + * @return "correct" language value. + */ + public static String getLangString(Object jsonNode) { + if (jsonNode == null) { + return null; + } + if ((jsonNode instanceof String)) { + return (String) jsonNode; + } + if (jsonNode instanceof Map) { + var map = (Map) jsonNode; + if (map.containsKey("default")) { + return map.get("default").toString(); + } + return null; + } + return null; + } + + /** + * Simple utility class to get a JSON value as a string. + * + * @param o json object + * @return null or o.toString() + */ + public static String getAsString(Object o) { + if (o == null) { + return null; + } + var result = o.toString(); + return result; + } + + /** + * Simple utility class to get a JSON value as a double. + * + * @param o json object (could be integer or double) + * @return null or double value + */ + public static Double getAsDouble(Object o) { + if (o == null) { + return null; + } + return ((Number) o).doubleValue(); + } +} diff --git a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/util/LinksItemsBuilder.java b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/util/LinksItemsBuilder.java index 18aebc3b..33154fb6 100644 --- a/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/util/LinksItemsBuilder.java +++ b/modules/services/ogc-api-records/src/main/java/org/fao/geonet/ogcapi/records/util/LinksItemsBuilder.java @@ -5,7 +5,7 @@ import java.util.Optional; import org.fao.geonet.common.search.SearchConfiguration; import org.fao.geonet.common.search.SearchConfiguration.Format; -import org.fao.geonet.ogcapi.records.controller.model.Link; +import org.fao.geonet.ogcapi.records.model.OgcApiLink; import org.springframework.http.MediaType; public class LinksItemsBuilder { @@ -17,23 +17,23 @@ private LinksItemsBuilder() { /** * Build items. */ - public static List build( + public static List build( Optional format, String url, String language, SearchConfiguration configuration) { - Link currentDoc = new Link(); + OgcApiLink currentDoc = new OgcApiLink(); Format linkFormat = format.get(); currentDoc.setRel("self"); currentDoc.setHref(getHref(url, format)); currentDoc.setType(format.get().getMimeType()); currentDoc.setHreflang(language); - List links = new ArrayList<>(); + List links = new ArrayList<>(); links.add(currentDoc); for (MediaType supportedMediaType : MediaTypeUtil.defaultSupportedMediaTypes) { if (!supportedMediaType.toString().equals(linkFormat.getMimeType())) { Optional f = configuration.getFormat(supportedMediaType); - Link alternateDoc = new Link(); + OgcApiLink alternateDoc = new OgcApiLink(); alternateDoc.setRel("alternate"); alternateDoc.setHref(getHref(url, f)); alternateDoc.setType(supportedMediaType.toString()); 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 81711090..f8a86621 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 @@ -14,6 +14,7 @@ import org.elasticsearch.geometry.Rectangle; import org.elasticsearch.index.query.BoolQueryBuilder; import org.elasticsearch.index.query.GeoShapeQueryBuilder; +import org.elasticsearch.index.query.Operator; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.builder.SearchSourceBuilder; @@ -141,11 +142,15 @@ public String buildQuery( } } - String filterQueryString = configuration.getQueryFilter(); + String filterQueryString = "(" + configuration.getQueryFilter() + ")"; if (StringUtils.isNotEmpty(collectionFilter)) { - filterQueryString += " " + collectionFilter; + filterQueryString += " (" + collectionFilter + ")"; } - boolQuery.filter(QueryBuilders.queryStringQuery(filterQueryString)); + + var bbQuery = QueryBuilders.queryStringQuery(filterQueryString); + bbQuery.defaultOperator(Operator.AND); + boolQuery.filter(bbQuery); + sourceBuilder.query(boolQuery); sourceBuilder.trackTotalHits(configuration.getTrackTotalHits()); log.debug("OGC API query: {}", sourceBuilder.toString()); diff --git a/modules/services/ogc-api-records/src/test/java/org/fao/geonet/ogcapi/records/util/ElasticIndexJson2CollectionInfoTest.java b/modules/services/ogc-api-records/src/test/java/org/fao/geonet/ogcapi/records/util/ElasticIndexJson2CollectionInfoTest.java new file mode 100644 index 00000000..2d5a6c54 --- /dev/null +++ b/modules/services/ogc-api-records/src/test/java/org/fao/geonet/ogcapi/records/util/ElasticIndexJson2CollectionInfoTest.java @@ -0,0 +1,134 @@ +/** + * (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.util; + +import org.apache.commons.io.IOUtils; +import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.map.type.TypeFactory; +import org.fao.geonet.ogcapi.records.controller.model.CollectionInfo; +import org.junit.BeforeClass; +import org.junit.Test; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +public class ElasticIndexJson2CollectionInfoTest { + + + // sample Elastic Index JSON + public static String sample1ElasticIndexJson; + public static Map sample1ElasticIndexParsed; + + + + ElasticIndexJson2CollectionInfo elasticIndexJson2CollectionInfo = new ElasticIndexJson2CollectionInfo(); + + + @BeforeClass + public static void setupClass() throws IOException { + + + + + sample1ElasticIndexJson = IOUtils.toString( + ClassLoader.getSystemClassLoader().getResourceAsStream("org/fao/geonet/ogcapi/records/util/sampleCollectionServiceRecordElasticIndex.json"), + "UTF-8"); + + sample1ElasticIndexParsed = new ObjectMapper().readValue( sample1ElasticIndexJson, + TypeFactory.mapType(HashMap.class, String.class, Object.class)); + sample1ElasticIndexParsed = (Map ) sample1ElasticIndexParsed.get("hits"); + sample1ElasticIndexParsed = (Map ) ((List)sample1ElasticIndexParsed.get("hits")).get(0); + sample1ElasticIndexParsed = (Map )sample1ElasticIndexParsed.get("_source"); + + + } + + + /** + * Simple test case to read in the Elastic Index JSON, and parse it into a CollectionInfo. + */ + @Test + public void testFull() { + var collectionInfo = new CollectionInfo(); + elasticIndexJson2CollectionInfo.injectLinkedServiceRecordInfo(collectionInfo, sample1ElasticIndexParsed); + + assertEquals("catalog", collectionInfo.getType()); + assertEquals("record", collectionInfo.getItemType()); + + assertEquals("GeoCat Demo OGCIAPI sub-portal", collectionInfo.getTitle()); + assertEquals("This is a sub-portal for testing OGCAPI.", collectionInfo.getDescription()); + + assertEquals("http://www.opengis.net/def/crs/OGC/1.3/CRS84", collectionInfo.getCrs().get(0)); + assertEquals("2024-09-10T19:10:58.536961Z", collectionInfo.getCreated()); + assertEquals("2024-09-16T17:07:51.673194Z", collectionInfo.getUpdated()); + assertEquals("eng", collectionInfo.getLanguage().code); + assertEquals("use limitation", collectionInfo.getLicense()); + + //languages + assertEquals(3, collectionInfo.getLanguages().size()); + assertEquals("dut", collectionInfo.getLanguages().get(0).code); + assertEquals("spa", collectionInfo.getLanguages().get(1).code); + assertEquals("eng", collectionInfo.getLanguages().get(2).code); + + //keywords + assertEquals(6, collectionInfo.getKeywords().size()); + assertEquals("GEONETWORK", collectionInfo.getKeywords().get(0)); + assertEquals("OSGeo", collectionInfo.getKeywords().get(1)); + assertEquals("GeoCat", collectionInfo.getKeywords().get(2)); + assertEquals("OGCAPI", collectionInfo.getKeywords().get(3)); + assertEquals("Algeria", collectionInfo.getKeywords().get(4)); + assertEquals("Antarctica", collectionInfo.getKeywords().get(5)); + + + //themes + assertEquals(2, collectionInfo.getThemes().size()); + //first theme + assertEquals("otherKeywords-theme", collectionInfo.getThemes().get(0).schema); + assertEquals(4, collectionInfo.getThemes().get(0).getConcepts().size()); + assertEquals("GEONETWORK", collectionInfo.getThemes().get(0).getConcepts().get(0).id); + assertEquals("OSGeo", collectionInfo.getThemes().get(0).getConcepts().get(1).id); + assertEquals("GeoCat", collectionInfo.getThemes().get(0).getConcepts().get(2).id); + assertEquals("OGCAPI", collectionInfo.getThemes().get(0).getConcepts().get(3).id); + + //2nd theme + assertEquals("http://localhost:8080/geonetwork/srv/api/registries/vocabularies/external.place.regions", collectionInfo.getThemes().get(1).schema); + assertEquals(2, collectionInfo.getThemes().get(1).getConcepts().size()); + assertEquals("Algeria", collectionInfo.getThemes().get(1).getConcepts().get(0).id); + assertEquals("Antarctica", collectionInfo.getThemes().get(1).getConcepts().get(1).id); + + //contacts + assertEquals(1, collectionInfo.getContacts().size()); + assertEquals("Jody Garnett", collectionInfo.getContacts().get(0).getName()); + assertEquals("Hat Wearer Extraordinaire", collectionInfo.getContacts().get(0).getPosition()); + assertEquals("GeoCat Canada Ltd", collectionInfo.getContacts().get(0).getOrganization()); + //phones + assertEquals(1, collectionInfo.getContacts().get(0).getPhones().size()); + assertEquals("+1 (250) 213-1219", collectionInfo.getContacts().get(0).getPhones().get(0).getValue()); + //email + assertEquals(1, collectionInfo.getContacts().get(0).getEmails().size()); + assertEquals("jody.garnett@geocat.net", collectionInfo.getContacts().get(0).getEmails().get(0).getValue()); + //address + assertEquals(1, collectionInfo.getContacts().get(0).getAddresses().size()); + assertEquals(1, collectionInfo.getContacts().get(0).getAddresses().get(0).getDeliveryPoint().size()); + assertEquals("3613 Doncaster Drr, Victoria, BC, V8P 3W5, Canada", collectionInfo.getContacts().get(0).getAddresses().get(0).getDeliveryPoint().get(0)); + + //extent - spatial + assertEquals("http://www.opengis.net/def/crs/OGC/1.3/CRS84", collectionInfo.getExtent().getSpatial().getCrs()); + assertEquals(4, collectionInfo.getExtent().getSpatial().getBbox().size()); + assertEquals(-180.0, collectionInfo.getExtent().getSpatial().getBbox().get(0).doubleValue() ,0); + assertEquals(-90.0, collectionInfo.getExtent().getSpatial().getBbox().get(1).doubleValue() ,0); + assertEquals(180.0, collectionInfo.getExtent().getSpatial().getBbox().get(2).doubleValue() ,0); + assertEquals(90.0, collectionInfo.getExtent().getSpatial().getBbox().get(3).doubleValue() ,0); + //extent - temporal + assertEquals("http://www.opengis.net/def/uom/ISO-8601/0/Gregorian", collectionInfo.getExtent().getTemporal().getTrs()); + assertEquals(2, collectionInfo.getExtent().getTemporal().getInterval().size()); + assertEquals("2016-02-29T20:00:00.000Z", collectionInfo.getExtent().getTemporal().getInterval().get(0)); + assertEquals("2016-02-29T20:00:00.000Z", collectionInfo.getExtent().getTemporal().getInterval().get(1)); + + } +} diff --git a/modules/services/ogc-api-records/src/test/resources/org/fao/geonet/ogcapi/records/util/sampleCollectionServiceRecord.xml b/modules/services/ogc-api-records/src/test/resources/org/fao/geonet/ogcapi/records/util/sampleCollectionServiceRecord.xml new file mode 100644 index 00000000..c4ac0b5d --- /dev/null +++ b/modules/services/ogc-api-records/src/test/resources/org/fao/geonet/ogcapi/records/util/sampleCollectionServiceRecord.xml @@ -0,0 +1,787 @@ + + + + 9bac358b-11ec-4293-aeef-5a077b778412 + + + + + + + + + + + + + + Jody Garnett + + + Jody Garnett + + + + + GeoCat Canada Ltd + + + GeoCat Canada Ltd + + + + + Hat Wearer Extraordinaire + + + Hat Wearer Extraordinaire + + + + + + + + + +1 (250) 213-1219 + + + + + + + + + + 3613 Doncaster Drr + + + 3613 Doncaster Drr + + + + + Victoria + + + BC + + + V8P 3W5 + + + Canada + + + Canada + + + + + jody.garnett@geocat.net + + + jody.garnett@geocat.net + + + + + + + + + http://geocat.net + + + + + + + + Please call between 9:00am and 5:00pm + + + Please call between 9:00am and 5:00pm + + + + + + + + + + + + 2024-09-16T17:07:51.673224Z + + + ISO 19115:2003/19139 + + + 1.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + http://www.opengis.net/def/crs/OGC/1.3/CRS84 + + + http://www.opengis.net/def/crs/OGC/1.3/CRS84 + + + + + + + + + + + + + + + + GeoCat Demo OGCIAPI sub-portal + + + GeoCat Demo OGCIAPI sub-portal + + + + + + + + 2016-02-29T12:00:00 + + + + + + + + + + This is a sub-portal for testing OGCAPI. + + + This is a sub-portal for testing OGCAPI. + + + + + + + + + + Jody Garnett + + + Jody Garnett + + + + + GeoCat Canada Ltd + + + GeoCat Canada Ltd + + + + + position + + + position + + + + + + + + + +1 (250) 213-1219 + + + + + + + + + + 3613 Doncaster Dr + + + 3613 Doncaster Dr + + + + + Victoria + + + BC + + + V8P 3W5 + + + Canada + + + Canada + + + + + jody.garnett@geocat.net + + + jody.garnett@geocat.net + + + + + + + + + http://geocat.net + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + http://localhost:8080/geonetwork/srv/api/records/9bac358b-11ec-4293-aeef-5a077b778412/attachments/jody.jpg + + + + + + + GEONETWORK + + + GEONETWORK + + + + + OSGeo + + + OSGeo + + + + + GeoCat + + + GeoCat + + + + + OGCAPI + + + OGCAPI + + + + + + + + + + + + + + + + + + + + INSPIRE Service taxonomy + + + INSPIRE Service taxonomy + + + + + + + 2010-04-22 + + + + + + + + + + geonetwork.thesaurus.external.theme.inspire-service-taxonomy + + + + + + + + + + + Algeria + + + Algeria + + + + + + + + + + + Antarctica + + + Antarctica + + + + + + + + + + + + + + + + Continents, countries, sea regions of the world. + + + Continents, countries, sea regions of the world. + + + + + + + 2015-07-17 + + + + + + + + + + geonetwork.thesaurus.external.place.regions + + + + + + + + + + + use limitation + + + use limitation + + + + + + + + + + + No Conditions Apply + + + No Conditions Apply + + + + + blah + + + blah + + + + + + + other + + + 1.1.1 + + + + + NONE + + + + + + + + + -180.00 + + + 180.00 + + + -90.00 + + + 90.00 + + + + + + + + + + + + GetCapabilities + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + invocable + + + invocable + + + + + + + 2014-12-11 + + + + + + + + + + Conformant to the INSPIRE SDS specifications. + + + Conformant to the INSPIRE SDS specifications. + + + + + true + + + + + + + + + availability + + + availability + + + + + + + INSPIRE_service_availability + + + INSPIRE_service_availability + + + + + + + + + + 90 + + + + + + + + + performance + + + performance + + + + + + + INSPIRE_service_performance + + + INSPIRE_service_performance + + + + + + + + + + 0.5 + + + + + + + + + capacity + + + capacity + + + + + + + INSPIRE_service_capacity + + + INSPIRE_service_capacity + + + + + + + + + + 50 + + + + + + + + + + + + + Description of technical specification + + + Description of technical specification + + + + + + + 2014-12-11 + + + + + + + + + + Conformant to the cited specifications. + + + Conformant to the cited specifications. + + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/services/ogc-api-records/src/test/resources/org/fao/geonet/ogcapi/records/util/sampleCollectionServiceRecordElasticIndex.json b/modules/services/ogc-api-records/src/test/resources/org/fao/geonet/ogcapi/records/util/sampleCollectionServiceRecordElasticIndex.json new file mode 100644 index 00000000..02a60fed --- /dev/null +++ b/modules/services/ogc-api-records/src/test/resources/org/fao/geonet/ogcapi/records/util/sampleCollectionServiceRecordElasticIndex.json @@ -0,0 +1,538 @@ +{ + "took": 67, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 1, + "relation": "eq" + }, + "max_score": 1.8064759, + "hits": [ + { + "_index": "gn-records", + "_id": "9bac358b-11ec-4293-aeef-5a077b778412", + "_score": 1.8064759, + "_ignored": [ + "overview.data.keyword" + ], + "_source": { + "docType": "metadata", + "document": "", + "metadataIdentifier": "9bac358b-11ec-4293-aeef-5a077b778412", + "standardNameObject": { + "default": "ISO 19115:2003/19139", + "langeng": "ISO 19115:2003/19139" + }, + "standardVersionObject": { + "default": "1.0", + "langeng": "1.0" + }, + "indexingDate": 1726506476530, + "dateStamp": "2024-09-16T17:07:51.673224Z", + "mainLanguage": "eng", + "otherLanguage": [ + "dut", + "spa", + "eng" + ], + "otherLanguageId": [ + "NL", + "ES", + "EN" + ], + "cl_characterSet": [ + { + "key": "utf8", + "default": "UTF8", + "langeng": "UTF8", + "langdut": "utf8", + "langspa": "UTF8", + "link": "http://standards.iso.org/iso/19139/resources/gmxCodelists.xml#MD_CharacterSetCode" + } + ], + "resourceType": [ + "service" + ], + "OrgObject": { + "default": "GeoCat Canada Ltd", + "langeng": "GeoCat Canada Ltd" + }, + "pointOfContactOrgObject": { + "default": "GeoCat Canada Ltd", + "langeng": "GeoCat Canada Ltd" + }, + "contact": [ + { + "organisationObject": { + "default": "GeoCat Canada Ltd", + "langeng": "GeoCat Canada Ltd" + }, + "role": "pointOfContact", + "email": "jody.garnett@geocat.net", + "website": "http://geocat.net", + "logo": "", + "individual": "Jody Garnett", + "position": "Hat Wearer Extraordinaire", + "phone": "+1 (250) 213-1219", + "address": "3613 Doncaster Drr, Victoria, BC, V8P 3W5, Canada" + } + ], + "cl_hierarchyLevel": [ + { + "key": "service", + "default": "Service", + "langeng": "Service", + "langdut": "service", + "langspa": "Servicio", + "link": "http://standards.iso.org/iso/19139/resources/gmxCodelists.xml#MD_ScopeCode" + } + ], + "cl_status": [ + { + "key": "completed", + "default": "Completed", + "langeng": "Completed", + "langdut": "compleet", + "langspa": "Terminado", + "link": "http://standards.iso.org/iso/19139/resources/gmxCodelists.xml#MD_ProgressCode" + } + ], + "cl_type": [ + { + "key": "theme", + "default": "Theme", + "langeng": "Theme", + "langdut": "theme", + "langspa": "Tema", + "link": "http://standards.iso.org/iso/19139/resources/gmxCodelists.xml#MD_KeywordTypeCode" + }, + { + "key": "place", + "default": "Place", + "langeng": "Place", + "langdut": "place", + "langspa": "Lugar", + "link": "http://standards.iso.org/iso/19139/resources/gmxCodelists.xml#MD_KeywordTypeCode" + } + ], + "cl_accessConstraints": [ + { + "key": "otherRestrictions", + "default": "Other restrictions", + "langeng": "Other restrictions", + "langdut": "anders", + "langspa": "Otras restricciones", + "link": "http://standards.iso.org/iso/19139/resources/gmxCodelists.xml#MD_RestrictionCode" + } + ], + "cl_useConstraints": [ + { + "key": "restricted", + "default": "Restricted", + "langeng": "Restricted", + "langdut": "niet\n toegankelijk\n ", + "langspa": "Restricted", + "link": "http://standards.iso.org/iso/19139/resources/gmxCodelists.xml#MD_RestrictionCode" + } + ], + "cl_couplingType": [ + { + "key": "tight", + "default": "Tight", + "langeng": "Tight", + "langdut": "tight", + "langspa": "Apretado", + "link": "http://www.isotc211.org/2005/iso19119/resources/Codelist/gmxCodelists.xml#SV_CouplingType" + } + ], + "cl_DCP": [ + { + "key": "XML", + "default": "XML", + "langeng": "XML", + "langdut": "XML", + "langspa": "XML", + "link": "http://inspire.ec.europa.eu/metadata-codelist/DCPList" + } + ], + "resourceTitleObject": { + "default": "GeoCat Demo OGCIAPI sub-portal", + "langeng": "GeoCat Demo OGCIAPI sub-portal" + }, + "resourceAltTitleObject": [ + { + "default": "", + "lang": "" + } + ], + "revisionDateForResource": [ + "2016-02-29T20:00:00.000Z" + ], + "revisionYearForResource": "2016", + "revisionMonthForResource": "2016-02", + "resourceDate": [ + { + "type": "revision", + "date": "2016-02-29T20:00:00.000Z" + } + ], + "resourceTemporalDateRange": [ + { + "gte": "2016-02-29T20:00:00.000Z", + "lte": "2016-02-29T20:00:00.000Z" + } + ], + "resourceAbstractObject": { + "default": "This is a sub-portal for testing OGCAPI.", + "langeng": "This is a sub-portal for testing OGCAPI." + }, + "OrgForResourceObject": { + "default": "GeoCat Canada Ltd", + "langeng": "GeoCat Canada Ltd" + }, + "pointOfContactOrgForResourceObject": { + "default": "GeoCat Canada Ltd", + "langeng": "GeoCat Canada Ltd" + }, + "contactForResource": [ + { + "organisationObject": { + "default": "GeoCat Canada Ltd", + "langeng": "GeoCat Canada Ltd" + }, + "role": "pointOfContact", + "email": "jody.garnett@geocat.net", + "website": "http://geocat.net", + "logo": "", + "individual": "Jody Garnett", + "position": "position", + "phone": "+1 (250) 213-1219", + "address": "3613 Doncaster Dr, Victoria, BC, V8P 3W5, Canada" + }, + { + "organisationObject": {}, + "role": "", + "email": "", + "website": "", + "logo": "", + "individual": "", + "position": "", + "phone": "", + "address": "" + } + ], + "hasOverview": "true", + "tag": [ + { + "default": "GEONETWORK", + "langeng": "GEONETWORK" + }, + { + "default": "OSGeo", + "langeng": "OSGeo" + }, + { + "default": "GeoCat", + "langeng": "GeoCat" + }, + { + "default": "OGCAPI", + "langeng": "OGCAPI" + }, + { + "default": "Algeria", + "langeng": "Algeria", + "link": "http://www.naturalearthdata.com/ne_admin#Country/DZA", + "key": "http://www.naturalearthdata.com/ne_admin#Country/DZA" + }, + { + "default": "Antarctica", + "langeng": "Antarctica", + "link": "http://www.naturalearthdata.com/ne_admin#Country/Indeterminate/ATA", + "key": "http://www.naturalearthdata.com/ne_admin#Country/Indeterminate/ATA" + } + ], + "tagNumber": "6", + "isOpenData": "false", + "keywordType-theme": [ + { + "default": "GEONETWORK", + "langeng": "GEONETWORK" + }, + { + "default": "OSGeo", + "langeng": "OSGeo" + }, + { + "default": "GeoCat", + "langeng": "GeoCat" + }, + { + "default": "OGCAPI", + "langeng": "OGCAPI" + } + ], + "keywordType-place": [ + { + "default": "Algeria", + "langeng": "Algeria", + "link": "http://www.naturalearthdata.com/ne_admin#Country/DZA" + }, + { + "default": "Antarctica", + "langeng": "Antarctica", + "link": "http://www.naturalearthdata.com/ne_admin#Country/Indeterminate/ATA" + } + ], + "th_otherKeywords-themeNumber": "4", + "th_otherKeywords-theme": [ + { + "default": "GEONETWORK", + "langeng": "GEONETWORK" + }, + { + "default": "OSGeo", + "langeng": "OSGeo" + }, + { + "default": "GeoCat", + "langeng": "GeoCat" + }, + { + "default": "OGCAPI", + "langeng": "OGCAPI" + } + ], + "th_regionsNumber": "2", + "th_regions": [ + { + "default": "Algeria", + "langeng": "Algeria", + "link": "http://www.naturalearthdata.com/ne_admin#Country/DZA" + }, + { + "default": "Antarctica", + "langeng": "Antarctica", + "link": "http://www.naturalearthdata.com/ne_admin#Country/Indeterminate/ATA" + } + ], + "th_inspire-service-taxonomyNumber": "0", + "th_inspire-service-taxonomy": [], + "allKeywords": { + "th_otherKeywords-theme": { + "title": "otherKeywords-theme", + "theme": "theme", + "keywords": [ + { + "default": "GEONETWORK", + "langeng": "GEONETWORK" + }, + { + "default": "OSGeo", + "langeng": "OSGeo" + }, + { + "default": "GeoCat", + "langeng": "GeoCat" + }, + { + "default": "OGCAPI", + "langeng": "OGCAPI" + } + ] + }, + "th_regions": { + "id": "geonetwork.thesaurus.external.place.regions", + "multilingualTitle": { + "default": "Continents, countries, sea regions of the world.", + "langeng": "Continents, countries, sea regions of the world.", + "link": "http://geonetwork-opensource.org/thesaurus/naturalearth-and-seavox" + }, + "theme": "place", + "link": "http://localhost:8080/geonetwork/srv/api/registries/vocabularies/external.place.regions", + "keywords": [ + { + "default": "Algeria", + "langeng": "Algeria", + "link": "http://www.naturalearthdata.com/ne_admin#Country/DZA" + }, + { + "default": "Antarctica", + "langeng": "Antarctica", + "link": "http://www.naturalearthdata.com/ne_admin#Country/Indeterminate/ATA" + } + ] + }, + "th_inspire-service-taxonomy": { + "id": "geonetwork.thesaurus.external.theme.inspire-service-taxonomy", + "multilingualTitle": { + "default": "INSPIRE Service taxonomy", + "langeng": "INSPIRE Service taxonomy" + }, + "theme": "theme", + "link": "http://localhost:8080/geonetwork/srv/api/registries/vocabularies/external.theme.inspire-service-taxonomy", + "keywords": [] + } + }, + "th_regions_tree": { + "default": [ + "Africa", + "Africa^Algeria", + "Antarctica" + ], + "key": [ + "http://www.naturalearthdata.com/ne_admin#Continent/Africa", + "http://www.naturalearthdata.com/ne_admin#Continent/Africa^http://www.naturalearthdata.com/ne_admin#Country/DZA", + "http://www.naturalearthdata.com/ne_admin#Country/Indeterminate/ATA" + ] + }, + "MD_LegalConstraintsOtherConstraintsObject": [ + { + "default": "No Conditions Apply", + "langeng": "No Conditions Apply", + "link": "http://inspire.ec.europa.eu/registry/metadata-codelist/ConditionsApplyingToAccessAndUse/noConditionsApply" + }, + { + "default": "blah", + "langeng": "blah" + } + ], + "MD_LegalConstraintsUseLimitationObject": [ + { + "default": "use limitation", + "langeng": "use limitation" + } + ], + "licenseObject": [ + { + "default": "No Conditions Apply", + "langeng": "No Conditions Apply", + "link": "http://inspire.ec.europa.eu/registry/metadata-codelist/ConditionsApplyingToAccessAndUse/noConditionsApply" + }, + { + "default": "blah", + "langeng": "blah" + } + ], + "geom": [ + { + "type": "Polygon", + "coordinates": [ + [ + [ + -180, + -90 + ], + [ + 180, + -90 + ], + [ + 180, + 90 + ], + [ + -180, + 90 + ], + [ + -180, + -90 + ] + ] + ] + } + ], + "location": "0,0", + "serviceType": "other", + "serviceTypeVersion": "1.1.1", + "coordinateSystem": [ + "http://www.opengis.net/def/crs/OGC/1.3/CRS84" + ], + "crsDetails": [ + { + "code": "http://www.opengis.net/def/crs/OGC/1.3/CRS84", + "codeSpace": "", + "name": "http://www.opengis.net/def/crs/OGC/1.3/CRS84", + "url": "" + } + ], + "specificationConformance": [ + { + "title": "invocable", + "date": "2014-12-11", + "link": "http://inspire.ec.europa.eu/metadata-codelist/Category/invocable", + "explanation": "Conformant to the INSPIRE SDS specifications.", + "pass": "true" + }, + { + "title": "Description of technical specification", + "date": "2014-12-11", + "link": "http://link,to/the.technical.specification", + "explanation": "Conformant to the cited specifications.", + "pass": "true" + } + ], + "conformTo_invocable": "true", + "conformTo_Descriptionoftechnicalspecification": "true", + "recordGroup": "9bac358b-11ec-4293-aeef-5a077b778412", + "recordOwner": "admin admin", + "uuid": "9bac358b-11ec-4293-aeef-5a077b778412", + "displayOrder": "0", + "groupPublishedId": [ + "2", + "1", + "0" + ], + "popularity": 27, + "userinfo": "admin|admin|admin|Administrator", + "groupPublished": [ + "sample", + "all", + "intranet" + ], + "isPublishedToAll": "true", + "record": "record", + "draft": "n", + "changeDate": "2024-09-16T17:07:51.673194Z", + "id": "117", + "createDate": "2024-09-10T19:10:58.536961Z", + "isPublishedToIntranet": "true", + "owner": "1", + "groupOwner": "2", + "logo": "/images/logos/9a8e760d-08cc-4406-b120-bb3f7d02c94b.png", + "hasxlinks": "false", + "featureOfRecord": "record", + "isPublishedToGuest": "false", + "extra": "null", + "documentStandard": "iso19139", + "valid": "-1", + "isTemplate": "n", + "feedbackCount": "0", + "rating": "0", + "isHarvested": "false", + "userSavedCount": "0", + "sourceCatalogue": "9a8e760d-08cc-4406-b120-bb3f7d02c94b", + "overview": [ + {} + ] + }, + "edit": false, + "owner": false, + "isPublishedToAll": true, + "view": true, + "notify": false, + "download": true, + "dynamic": true, + "featured": false, + "selected": false + } + ] + } +} \ No newline at end of file