@@ -21,6 +21,11 @@ class Context
2121 # @return [Hash{Symbol => Context}]
2222 PRELOADED = { }
2323
24+ ##
25+ # Defines the maximum number of interned URI references that can be held
26+ # cached in memory at any one time.
27+ CACHE_SIZE = -1 # unlimited by default
28+
2429 class << self
2530 ##
2631 # Add preloaded context. In the block form, the context is lazy evaulated on first use.
@@ -335,6 +340,16 @@ def self.parse(local_context, protected: false, override_protected: false, propa
335340 self . new ( **options ) . parse ( local_context , protected : false , override_protected : override_protected , propagate : propagate )
336341 end
337342
343+ ##
344+ # Class-level cache used for retaining parsed remote contexts.
345+ #
346+ # @return [RDF::Util::Cache]
347+ # @private
348+ def self . cache
349+ require 'rdf/util/cache' unless defined? ( ::RDF ::Util ::Cache )
350+ @cache ||= RDF ::Util ::Cache . new ( CACHE_SIZE )
351+ end
352+
338353 ##
339354 # Create new evaluation context
340355 # @param [Hash] options
@@ -561,7 +576,7 @@ def parse(local_context,
561576 end
562577 when Context
563578 #log_debug("parse") {"context: #{context.inspect}"}
564- result = context . dup
579+ result = result . merge ( context )
565580 when IO , StringIO
566581 #log_debug("parse") {"io: #{context}"}
567582 # Load context document, if it is an open file
@@ -570,7 +585,6 @@ def parse(local_context,
570585 raise JSON ::LD ::JsonLdError ::InvalidRemoteContext , "Context missing @context key" if @options [ :validate ] && ctx [ '@context' ] . nil?
571586 result = result . dup . parse ( ctx [ "@context" ] ? ctx [ "@context" ] . dup : { } )
572587 result . provided_context = ctx [ "@context" ] if [ context ] == local_context
573- result
574588 rescue JSON ::ParserError => e
575589 #log_debug("parse") {"Failed to parse @context from remote document at #{context}: #{e.message}"}
576590 raise JSON ::LD ::JsonLdError ::InvalidRemoteContext , "Failed to parse remote context at #{ context } : #{ e . message } " if @options [ :validate ]
@@ -582,19 +596,15 @@ def parse(local_context,
582596 # 3.2.1) Set context to the result of resolving value against the base IRI which is established as specified in section 5.1 Establishing a Base URI of [RFC3986]. Only the basic algorithm in section 5.2 of [RFC3986] is used; neither Syntax-Based Normalization nor Scheme-Based Normalization are performed. Characters additionally allowed in IRI references are treated in the same way that unreserved characters are treated in URI references, per section 6.5 of [RFC3987].
583597 context = RDF ::URI ( result . context_base || options [ :base ] ) . join ( context )
584598 context_canon = context . canonicalize
585- context_canon . scheme == 'http' if context_canon . scheme == 'https'
599+ context_canon . scheme = 'http' if context_canon . scheme == 'https'
586600
587601 # If validating a scoped context which has already been loaded, skip to the next one
588602 next if !validate_scoped && remote_contexts . include? ( context . to_s )
589603
590604 remote_contexts << context . to_s
591605 raise JsonLdError ::ContextOverflow , "#{ context } " if remote_contexts . length >= MAX_CONTEXTS_LOADED
592606
593- context_no_base = result . dup
594- context_no_base . base = nil
595- context_no_base . context_base = context . to_s
596-
597- if PRELOADED [ context_canon . to_s ]
607+ cached_context = if PRELOADED [ context_canon . to_s ]
598608 # If we have a cached context, merge it into the current context (result) and use as the new context
599609 #log_debug("parse") {"=> cached_context: #{context_canon.to_s.inspect}"}
600610
@@ -603,10 +613,10 @@ def parse(local_context,
603613 #log_debug("parse") {"=> (call)"}
604614 PRELOADED [ context_canon . to_s ] = PRELOADED [ context_canon . to_s ] . call
605615 end
606- context = context_no_base . merge! ( PRELOADED [ context_canon . to_s ] )
616+ PRELOADED [ context_canon . to_s ]
607617 else
608618 # Load context document, if it is a string
609- begin
619+ Context . cache [ context_canon . to_s ] ||= begin
610620 context_opts = @options . merge (
611621 profile : 'http://www.w3.org/ns/json-ld#context' ,
612622 requestProfile : 'http://www.w3.org/ns/json-ld#context' ,
@@ -615,29 +625,33 @@ def parse(local_context,
615625 JSON ::LD ::API . loadRemoteDocument ( context . to_s , **context_opts ) do |remote_doc |
616626 # 3.2.5) Dereference context. If the dereferenced document has no top-level JSON object with an @context member, an invalid remote context has been detected and processing is aborted; otherwise, set context to the value of that member.
617627 raise JsonLdError ::InvalidRemoteContext , "#{ context } " unless remote_doc . document . is_a? ( Hash ) && remote_doc . document . has_key? ( '@context' )
618- context = remote_doc . document [ '@context' ]
628+
629+ # Parse stand-alone
630+ ctx = Context . new ( **options )
631+ ctx . context_base = context . to_s
632+ ctx . parse ( remote_doc . document [ '@context' ] , remote_contexts : remote_contexts . dup )
619633 end
620634 rescue JsonLdError ::LoadingDocumentFailed => e
621635 #log_debug("parse") {"Failed to retrieve @context from remote document at #{context_no_base.context_base.inspect}: #{e.message}"}
622- raise JsonLdError ::LoadingRemoteContextFailed , "#{ context_no_base . context_base } : #{ e . message } " , e . backtrace
636+ raise JsonLdError ::LoadingRemoteContextFailed , "#{ context } : #{ e . message } " , e . backtrace
623637 rescue JsonLdError
624638 raise
625639 rescue StandardError => e
626640 #log_debug("parse") {"Failed to retrieve @context from remote document at #{context_no_base.context_base.inspect}: #{e.message}"}
627- raise JsonLdError ::LoadingRemoteContextFailed , "#{ context_no_base . context_base } : #{ e . message } " , e . backtrace
641+ raise JsonLdError ::LoadingRemoteContextFailed , "#{ context } : #{ e . message } " , e . backtrace
628642 end
643+ end . dup ( )
629644
630- # 3.2.6) Set context to the result of recursively calling this algorithm, passing context no base for active context, context for local context, and remote contexts.
631- context = context_no_base . parse ( context ,
632- remote_contexts : remote_contexts . dup ,
633- protected : protected ,
634- override_protected : override_protected ,
635- propagate : propagate ,
636- validate_scoped : validate_scoped )
637- PRELOADED [ context_canon . to_s ] = context . dup
638- context . provided_context = result . provided_context
645+ # if `protected` is true, update term definitions to be protected
646+ # FIXME: if they were explicitly marked not protected, then this will fail
647+ if protected
648+ cached_context . term_definitions . each { |td | td . protected = true }
639649 end
640- context . base ||= result . base
650+
651+ # Merge loaded context noting protected term overriding
652+ context = result . merge ( cached_context , override_protected : override_protected )
653+
654+ context . previous_context = result unless propagate
641655 result = context
642656 #log_debug("parse") {"=> provided_context: #{context.inspect}"}
643657 when Hash
@@ -711,28 +725,31 @@ def parse(local_context,
711725 # Merge in a context, creating a new context with updates from `context`
712726 #
713727 # @param [Context] context
728+ # @param [Boolean] protected mark resulting context as protected
729+ # @param [Boolean] override_protected Allow or disallow protected terms to be changed
730+ # @param [Boolean]
714731 # @return [Context]
715- def merge ( context )
716- c = self . dup . merge! ( context )
717- c . instance_variable_set ( :@term_definitions , context . term_definitions . dup )
718- c
719- end
732+ def merge ( context , override_protected : false )
733+ ctx = self . dup
734+ ctx . context_base = context . context_base if context . context_base
735+ ctx . default_language = context . default_language if context . default_language
736+ ctx . default_direction = context . default_direction if context . default_direction
737+ ctx . vocab = context . vocab if context . vocab
738+ ctx . base = context . base if context . base
739+ if !override_protected
740+ ctx . term_definitions . each do |term , definition |
741+ next unless definition . protected? && ( other = context . term_definitions [ term ] )
742+ unless definition == other
743+ raise JSON ::LD ::JsonLdError ::ProtectedTermRedefinition , "Attempt to redefine protected term #{ term } "
744+ end
745+ end
746+ end
720747
721- ##
722- # Update context with definitions from `context`
723- #
724- # @param [Context] context
725- # @return [self]
726- def merge! ( context )
727- # FIXME: if new context removes the default language, this won't do anything
728- self . default_language = context . default_language if context . default_language
729- self . vocab = context . vocab if context . vocab
730- self . base = context . base if context . base
731-
732- # Merge in Term Definitions
733- term_definitions . merge! ( context . term_definitions )
734- @inverse_context = nil # Re-build after term definitions set
735- self
748+ # Add term definitions
749+ context . term_definitions . each do |term , definition |
750+ ctx . term_definitions [ term ] = definition
751+ end
752+ ctx
736753 end
737754
738755 # The following constants are used to reduce object allocations in #create_term_definition below
@@ -985,7 +1002,10 @@ def create_term_definition(local_context, term, defined,
9851002
9861003 if value . has_key? ( '@context' )
9871004 begin
988- new_ctx = self . parse ( value [ '@context' ] , override_protected : true , remote_contexts : remote_contexts , validate_scoped : false )
1005+ new_ctx = self . parse ( value [ '@context' ] ,
1006+ override_protected : true ,
1007+ remote_contexts : remote_contexts ,
1008+ validate_scoped : false )
9891009 # Record null context in array form
9901010 definition . context = case value [ '@context' ]
9911011 when String then new_ctx . context_base
@@ -1838,6 +1858,7 @@ def dup
18381858 ec . instance_eval do
18391859 @term_definitions = that . term_definitions . dup
18401860 @iri_to_term = that . iri_to_term . dup
1861+ @inverse_context = nil
18411862 end
18421863 ec
18431864 end
0 commit comments