diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst index 6f8e3f3ac..cfb72ce13 100644 --- a/docs/source/configuration.rst +++ b/docs/source/configuration.rst @@ -667,6 +667,45 @@ templates using dataset level templating. To learn more about Jinja2 templates, context: - datetime: https://schema.org/DateTime +Injecting verbatim JSON-LD contexts +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Sometimes it is desirable to override pygeoapi's default handling of JSON and JSON-LD resources +so that the provided ``context`` entries inside ``linked-data`` are injected into the objects +exactly as provided. + +This behavior can be enabled by setting ``inject_verbatim_context`` to ``true`` inside +the Linked Data configuration for the resource: + +.. code-block:: yaml + + linked-data: + context: + - https://example.com/my-context.jsonld + inject_verbatim_context: true + +With ``inject_verbatim_context`` enabled, both JSON and JSON-LD item resources will have +a ``@context`` property with the provided linked data context entries, and no JSON-LD +manipulation will be performed by pygeoapi. + +Additionally, some semantically enabled resources may provide their own ``@id`` (i.e., URI) value, +which may be different from the one used by pygeoapi. The ``replace_id_field`` setting +inside ``linked-data`` can be used to instruct pygeoapi to override a given property +in the object with its own item URL: + +.. code-block:: yaml + + linked-data: + context: + - https://example.com/my-context.jsonld + inject_verbatim_context: true + replace_id_field: id + +In the example above, the ``id`` field will be overwritten with the item URL on +the pygeoapi service. + +.. Important:: + ``inject_verbatim_context`` must be enabled for ``replace_id_field`` to work. Validating the configuration ---------------------------- diff --git a/pygeoapi/api/itemtypes.py b/pygeoapi/api/itemtypes.py index a1e1bda16..71381c23f 100644 --- a/pygeoapi/api/itemtypes.py +++ b/pygeoapi/api/itemtypes.py @@ -956,6 +956,9 @@ def get_collection_item(api: API, request: APIRequest, # locale (or fallback default locale) l10n.set_response_language(headers, prv_locale, request.locale) + config_dataset = api.config['resources'][dataset] + linked_data = config_dataset.get('linked-data') + if request.format == F_HTML: # render tpl_config = api.get_dataset_templates(dataset) content['title'] = l10n.translate(collections[dataset]['title'], @@ -973,7 +976,12 @@ def get_collection_item(api: API, request: APIRequest, content, request.locale) return headers, HTTPStatus.OK, content - elif request.format == F_JSONLD: + elif request.format == F_JSONLD or ( + request.format == F_JSON + and linked_data + and all(linked_data.get(k) + for k in ('context', 'inject_verbatim_context')) + ): content = geojson2jsonld( api, content, dataset, uri, (p.uri_field or 'id') ) diff --git a/pygeoapi/api/stac.py b/pygeoapi/api/stac.py index 37dfbedc9..ac18ae8c3 100644 --- a/pygeoapi/api/stac.py +++ b/pygeoapi/api/stac.py @@ -56,7 +56,7 @@ ) from pygeoapi.util import ( filter_dict_by_key_value, get_current_datetime, get_provider_by_type, - render_j2_template, to_json + render_j2_template, to_json, get_base_url ) from . import APIRequest, API, FORMAT_TYPES, F_JSON, F_HTML @@ -200,6 +200,8 @@ def get_stac_path(api: API, request: APIRequest, content['links'].extend( stac_collections[dataset].get('links', [])) + linked_data = api.config['resources'][dataset].get('linked-data') + if request.format == F_HTML: # render content['path'] = path if 'assets' in content: # item view @@ -227,6 +229,18 @@ def get_stac_path(api: API, request: APIRequest, return headers, HTTPStatus.OK, content + elif (linked_data + and all(linked_data.get(k) + for k in ('context', 'inject_verbatim_context'))): + content = { + '@context': linked_data['context'], + **content + } + if replace_id_field := linked_data.get('replace_id_field'): + content[replace_id_field] = (f"{get_base_url(api.config)}" + f"/stac/{path}") + pass + return headers, HTTPStatus.OK, to_json(content, api.pretty_print) else: # send back file diff --git a/pygeoapi/linked_data.py b/pygeoapi/linked_data.py index 4b0edb1d1..8365e515d 100644 --- a/pygeoapi/linked_data.py +++ b/pygeoapi/linked_data.py @@ -195,59 +195,71 @@ def geojson2jsonld(cls, data: dict, dataset: str, context = linked_data.get('context', []).copy() templates = cls.get_dataset_templates(dataset) - defaultVocabulary = { - 'schema': 'https://schema.org/', - 'gsp': 'http://www.opengis.net/ont/geosparql#', - 'type': '@type' - } - - if identifier: - # Expand properties block - data.update(data.pop('properties')) - - # Include multiple geometry encodings - if (data.get('geometry') is not None): - jsonldify_geometry(data) - - data['@id'] = identifier - + if (identifier and context + and linked_data.get('inject_verbatim_context', False)): + ldjsonData = { + '@context': context, + **data + } + if replace_id_field := linked_data.get('replace_id_field'): + ldjsonData[replace_id_field] = identifier else: - # Collection of jsonld - defaultVocabulary.update({ - 'features': 'schema:itemListElement', - 'FeatureCollection': 'schema:itemList' - }) - - ds_url = url_join(cls.get_collections_url(), dataset) - data['@id'] = ds_url - - for i, feature in enumerate(data['features']): - # Get URI for each feature - identifier_ = feature.get(id_field, - feature['properties'].get(id_field, '')) - if not is_url(str(identifier_)): - identifier_ = f"{ds_url}/items/{feature['id']}" # noqa - # Include multiple geometry encodings - if feature.get('geometry') is not None: - jsonldify_geometry(feature) - - data['features'][i] = { - '@id': identifier_, - 'type': 'schema:Place', - **feature.pop('properties'), - **feature - } - - if data.get('timeStamp', False): - data['https://schema.org/sdDatePublished'] = data.pop('timeStamp') + defaultVocabulary = { + 'schema': 'https://schema.org/', + 'gsp': 'http://www.opengis.net/ont/geosparql#', + 'type': '@type' + } - data['links'] = data.pop('links') + if identifier: + # Expand properties block + data.update(data.pop('properties')) - ldjsonData = { - '@context': [defaultVocabulary, *(context or [])], - **data - } + # Include multiple geometry encodings + if (data.get('geometry') is not None): + jsonldify_geometry(data) + + data['@id'] = identifier + + else: + # Collection of jsonld + defaultVocabulary.update({ + 'features': 'schema:itemListElement', + 'FeatureCollection': 'schema:itemList' + }) + + ds_url = url_join(cls.get_collections_url(), dataset) + data['@id'] = ds_url + + for i, feature in enumerate(data['features']): + # Get URI for each feature + identifier_ = feature.get( + id_field, + feature['properties'].get(id_field, '') + ) + if not is_url(str(identifier_)): + identifier_ = f"{ds_url}/items/{feature['id']}" # noqa + + # Include multiple geometry encodings + if feature.get('geometry') is not None: + jsonldify_geometry(feature) + + data['features'][i] = { + '@id': identifier_, + 'type': 'schema:Place', + **feature.pop('properties'), + **feature + } + + if data.get('timeStamp', False): + data['https://schema.org/sdDatePublished'] = data.pop('timeStamp') + + data['links'] = data.pop('links') + + ldjsonData = { + '@context': [defaultVocabulary, *(context or [])], + **data + } if identifier: # Render jsonld template for single item