Skip to content

Commit 517175d

Browse files
authored
Merge pull request crs4#154 from kikkomep/fix/issue-120
feat: support remote JSON-LD context retrieval via alternate link headers
2 parents 635c86b + 46ae808 commit 517175d

2 files changed

Lines changed: 407 additions & 17 deletions

File tree

rocrate_validator/profiles/ro-crate/must/0_file_descriptor_format.py

Lines changed: 74 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import re
1516
from typing import Any
17+
from urllib.parse import urljoin
1618

1719
from rocrate_validator.models import ValidationContext
1820
from rocrate_validator.requirements.python import (PyFunctionCheck, check,
@@ -86,17 +88,80 @@ class FileDescriptorJsonLdFormat(PyFunctionCheck):
8688
The file descriptor MUST be a valid JSON-LD file
8789
"""
8890

91+
def __get_remote_context__(self, context_uri: str) -> object:
92+
raw_data = HttpRequester().get(context_uri, headers={"Accept": "application/ld+json, application/json"})
93+
if raw_data.status_code != 200:
94+
raise RuntimeError(f"Unable to retrieve the JSON-LD context '{context_uri}'", self)
95+
logger.debug(f"Retrieved context from {context_uri}")
96+
97+
# Check if the response header contains the correct content type
98+
content_type = raw_data.headers.get("Content-Type", "")
99+
is_valid_content_type = "application/ld+json" in content_type or "application/json" in content_type
100+
# If the content type is not application/ld+json or application/json,
101+
# try to find an alternate link for the JSON-LD context in the response header
102+
if not is_valid_content_type:
103+
logger.debug(
104+
f"The retrieved context from {context_uri} "
105+
f"does not have a Content-Type of application/ld+json or application/json: "
106+
f"the actual Content-Type is {content_type}. "
107+
)
108+
# check if the response header contains an alternate link location for the JSON-LD context
109+
# (https headers are case-insensitive, according to RFC 7230,
110+
# so we can use .get() without worrying about the case)
111+
link_header = raw_data.headers.get("Link", "")
112+
logger.debug(f"Checking Link header for alternate JSON-LD context: {link_header}")
113+
has_alternate_link = ('rel="alternate"' in link_header and
114+
('type="application/ld+json"' in link_header or
115+
'type="application/json"' in link_header))
116+
117+
if has_alternate_link:
118+
logger.debug(f"Found alternate link for JSON-LD context in Link header: {link_header}")
119+
# extract the URL of the alternate link
120+
match = re.search(r'<([^>]+)>;\s*rel="alternate";\s*type="application/(ld\+json|json)"', link_header)
121+
if match:
122+
alternate_url = match.group(1)
123+
# If the alternate URL is relative, resolve it against the original context URI
124+
if not alternate_url.startswith("http"):
125+
alternate_url = urljoin(context_uri, alternate_url)
126+
logger.debug(f"Trying to retrieve JSON-LD context from alternate URL: {alternate_url}")
127+
raw_data = HttpRequester().get(alternate_url, headers={
128+
"Accept": "application/ld+json, application/json"})
129+
if raw_data.status_code != 200:
130+
raise RuntimeError(
131+
f"Unable to retrieve the JSON-LD context from alternate URL '{alternate_url}'", self)
132+
logger.debug(f"Retrieved context from alternate URL {alternate_url}")
133+
content_type = raw_data.headers.get("Content-Type", "")
134+
if "application/ld+json" not in content_type and "application/json" not in content_type:
135+
raise RuntimeError(
136+
f"The retrieved context from alternate URL {alternate_url} "
137+
"does not have a Content-Type of application/ld+json or application/json: "
138+
f"the actual Content-Type is {content_type}. ", self)
139+
else:
140+
logger.debug(f"No valid alternate link found in Link header: {link_header}")
141+
raise RuntimeError(
142+
f"Unable to retrieve the JSON-LD context from {context_uri} and no valid "
143+
f"alternate link found in Link header: {link_header}", self)
144+
else:
145+
logger.debug(f"No alternate link for JSON-LD context found in Link header: {link_header}")
146+
raise RuntimeError(
147+
f"Unable to retrieve the JSON-LD context from {context_uri} "
148+
f"and no alternate link found in Link header: {link_header}", self)
149+
150+
# Try to parse the JSON-LD and access the context
151+
jsonLD = raw_data.json()["@context"]
152+
# logger.warning(f"Retrieved JSON-LD context: {jsonLD}")
153+
assert isinstance(jsonLD, dict)
154+
# return the JSON-LD context
155+
return jsonLD
156+
89157
def __check_remote_context__(self, context_uri: str) -> bool:
90158
# Try to retrieve the context
91159
try:
92-
raw_data = HttpRequester().get(context_uri, headers={"Accept": "application/ld+json"})
93-
if raw_data.status_code != 200:
94-
raise RuntimeError(f"Unable to retrieve the JSON-LD context '{context_uri}'", self)
95-
logger.debug(f"Retrieved context from {context_uri}")
96-
97160
# Try to parse the JSON-LD and access the context
98-
jsonLD = raw_data.json()["@context"]
99-
assert isinstance(jsonLD, dict)
161+
jsonLD = self.__get_remote_context__(context_uri)
162+
assert isinstance(
163+
jsonLD, dict), f"The retrieved context from {context_uri} is not \
164+
a valid JSON-LD context: it is not a dictionary"
100165
return True
101166
except Exception as e:
102167
if logger.isEnabledFor(logging.DEBUG):
@@ -306,16 +371,8 @@ def __get_remote_context_keys__(self, context_uri: str) -> set:
306371
""" Get the keys of the context URI """
307372

308373
logger.debug(f"Retrieving context from {context_uri}...")
309-
# Try to retrieve the context
310-
raw_data = HttpRequester().get(context_uri, headers={"Accept": "application/ld+json"})
311-
if raw_data.status_code != 200:
312-
raise RuntimeError(f"Unable to retrieve the JSON-LD context '{context_uri}'")
313-
314-
logger.debug(f"Retrieved context from {context_uri}")
315-
316374
# Get the keys of the context
317-
jsonLD = raw_data.json()
318-
jsonLD_ctx = jsonLD["@context"]
375+
jsonLD_ctx = self.__get_remote_context__(context_uri)
319376
if not isinstance(jsonLD_ctx, dict):
320377
raise RuntimeError("The context is not a dictionary", self)
321378
return set(jsonLD_ctx.keys())
@@ -400,7 +457,7 @@ def check_compaction(self, context: ValidationContext) -> bool:
400457
# Check if k is a term or a URI
401458
if k.startswith("http"):
402459
context.result.add_issue(
403-
f'The The {v} occurrence{suffix} of the "{k}" URI cannot be used as a key{suffix} "'
460+
f'The {v} occurrence{suffix} of the "{k}" URI cannot be used as a key{suffix} "'
404461
'because the compacted format requires simple terms as keys '
405462
'(see https://www.w3.org/TR/json-ld-api/#compaction for more details).', self)
406463
else:

0 commit comments

Comments
 (0)