|
12 | 12 | # See the License for the specific language governing permissions and |
13 | 13 | # limitations under the License. |
14 | 14 |
|
| 15 | +import re |
15 | 16 | from typing import Any |
| 17 | +from urllib.parse import urljoin |
16 | 18 |
|
17 | 19 | from rocrate_validator.models import ValidationContext |
18 | 20 | from rocrate_validator.requirements.python import (PyFunctionCheck, check, |
@@ -86,17 +88,80 @@ class FileDescriptorJsonLdFormat(PyFunctionCheck): |
86 | 88 | The file descriptor MUST be a valid JSON-LD file |
87 | 89 | """ |
88 | 90 |
|
| 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 | + |
89 | 157 | def __check_remote_context__(self, context_uri: str) -> bool: |
90 | 158 | # Try to retrieve the context |
91 | 159 | 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 | | - |
97 | 160 | # 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" |
100 | 165 | return True |
101 | 166 | except Exception as e: |
102 | 167 | if logger.isEnabledFor(logging.DEBUG): |
@@ -306,16 +371,8 @@ def __get_remote_context_keys__(self, context_uri: str) -> set: |
306 | 371 | """ Get the keys of the context URI """ |
307 | 372 |
|
308 | 373 | 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 | | - |
316 | 374 | # 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) |
319 | 376 | if not isinstance(jsonLD_ctx, dict): |
320 | 377 | raise RuntimeError("The context is not a dictionary", self) |
321 | 378 | return set(jsonLD_ctx.keys()) |
@@ -400,7 +457,7 @@ def check_compaction(self, context: ValidationContext) -> bool: |
400 | 457 | # Check if k is a term or a URI |
401 | 458 | if k.startswith("http"): |
402 | 459 | 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} "' |
404 | 461 | 'because the compacted format requires simple terms as keys ' |
405 | 462 | '(see https://www.w3.org/TR/json-ld-api/#compaction for more details).', self) |
406 | 463 | else: |
|
0 commit comments