1212# See the License for the specific language governing permissions and
1313# limitations under the License.
1414
15+ import re
1516from typing import Any
17+ from urllib .parse import urljoin
1618
17- from rocrate_validator .utils import log as logging
1819from rocrate_validator .models import ValidationContext
1920from rocrate_validator .requirements .python import (PyFunctionCheck , check ,
2021 requirement )
22+ from rocrate_validator .utils import log as logging
2123from rocrate_validator .utils .http import HttpRequester
2224
2325# set up logging
@@ -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 ())
@@ -339,9 +396,27 @@ def add_unexpected_key(k: str, u_keys: dict) -> None:
339396 # If the entity is a dictionary, check each key
340397 if isinstance (entity , dict ):
341398 for k , v in entity .items ():
342- if k not in context_keys and k not in SKIP_KEYS :
399+ # If the key is in the skip keys, skip it
400+ if k in SKIP_KEYS :
401+ logger .debug (f"Key { k } is a reserved JSON-LD keyword, skipping" )
402+
403+ # If the key is not in the context keys,
404+ # it can be used in compacted format only if it is a valid compact IRI
405+ # with a prefix that is in the context
406+ elif k not in context_keys :
343407 logger .debug (f"Key { k } not in context keys" )
344- add_unexpected_key (k , unexpected_keys )
408+
409+ # Try to get the prefix of the compact IRI, if it has one
410+ prefix = k .split (":" , 1 )[0 ] if ":" in k else None
411+ logger .debug (f"Checking prefix { prefix } of key { k } " )
412+ # If the key does not have a prefix (no colon) or the prefix is not in the context keys,
413+ # it cannot be used as a key in compacted format
414+ if prefix is None or prefix not in context_keys :
415+ logger .debug (
416+ f"Key { k } does not have a valid prefix in context keys, adding to unexpected keys" )
417+ add_unexpected_key (k , unexpected_keys )
418+
419+ # If the value is a dictionary or a list, check its keys recursively
345420 if isinstance (v , (dict , list )):
346421 self .__check_entity_keys__ (v , context_keys , unexpected_keys )
347422
@@ -382,7 +457,7 @@ def check_compaction(self, context: ValidationContext) -> bool:
382457 # Check if k is a term or a URI
383458 if k .startswith ("http" ):
384459 context .result .add_issue (
385- 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 } "'
386461 'because the compacted format requires simple terms as keys '
387462 '(see https://www.w3.org/TR/json-ld-api/#compaction for more details).' , self )
388463 else :
0 commit comments