require 'addressable/uri' module RDF ## # A Uniform Resource Identifier (URI). # # `RDF::URI` supports all the instance methods of `Addressable::URI`. # # @example Creating a URI reference (1) # uri = RDF::URI.new("http://rdf.rubyforge.org/") # # @example Creating a URI reference (2) # uri = RDF::URI.new(:scheme => 'http', :host => 'rdf.rubyforge.org', :path => '/') # # @example Creating an interned URI reference # uri = RDF::URI.intern("http://rdf.rubyforge.org/") # # @example Getting the string representation of a URI # uri.to_s #=> "http://rdf.rubyforge.org/" # # @see http://en.wikipedia.org/wiki/Uniform_Resource_Identifier # @see http://addressable.rubyforge.org/ class URI include RDF::Resource ## # Defines the maximum number of interned URI references that can be held # cached in memory at any one time. CACHE_SIZE = -1 # unlimited by default ## # @return [RDF::Util::Cache] # @private def self.cache require 'rdf/util/cache' unless defined?(::RDF::Util::Cache) @cache ||= RDF::Util::Cache.new(CACHE_SIZE) end ## # Returns an interned `RDF::URI` instance based on the given `uri` # string. # # The maximum number of cached interned URI references is given by the # `CACHE_SIZE` constant. This value is unlimited by default, in which # case an interned URI object will be purged only when the last strong # reference to it is garbage collected (i.e., when its finalizer runs). # # Excepting special memory-limited circumstances, it should always be # safe and preferred to construct new URI references using # `RDF::URI.intern` instead of `RDF::URI.new`, since if an interned # object can't be returned for some reason, this method will fall back # to returning a freshly-allocated one. # # @param [String, #to_s] str # @return [RDF::URI] an immutable, frozen URI object def self.intern(str) (cache[str = str.to_s] ||= self.new(str)).freeze end ## # Creates a new `RDF::URI` instance based on the given `uri` string. # # This is just an alias for {#initialize RDF::URI.new} for compatibity # with `Addressable::URI.parse`. # # @param [String, #to_s] str # @return [RDF::URI] def self.parse(str) self.new(str) end ## # @overload URI.new(uri) # @param [RDF::URI, String, #to_s] uri # # @overload URI.new(options = {}) # @param [Hash{Symbol => Object} options def initialize(uri_or_options) case uri_or_options when Hash @uri = Addressable::URI.new(uri_or_options) when Addressable::URI @uri = uri_or_options else @uri = Addressable::URI.parse(uri_or_options.to_s) end end ## # Returns `false`. # # @return [Boolean] `true` or `false` def anonymous? false end ## # Returns `true`. # # @return [Boolean] `true` or `false` # @see http://en.wikipedia.org/wiki/Uniform_Resource_Identifier def uri? true end ## # Returns `true` if this URI is a URN. # # @example # RDF::URI('http://example.org/').urn? #=> false # # @return [Boolean] `true` or `false` # @see http://en.wikipedia.org/wiki/Uniform_Resource_Name # @since 0.2.0 def urn? self.start_with?('urn:') end ## # Returns `true` if this URI is a URL. # # @example # RDF::URI('http://example.org/').url? #=> true # # @return [Boolean] `true` or `false` # @see http://en.wikipedia.org/wiki/Uniform_Resource_Locator # @since 0.2.0 def url? !urn? end ## # Returns the string length of this URI. # # @example # RDF::URI('http://example.org/').length #=> 19 # # @return [Integer] # @since 0.3.0 def length to_s.length end alias_method :size, :length ## # Validates this URI, raising an error if it is invalid. # # @return [RDF::URI] `self` # @raise [ArgumentError] if the URI is invalid # @since 0.3.0 def validate! # TODO: raise error if the URI fails validation self end alias_method :validate, :validate! ## # Returns a copy of this URI converted into its canonical lexical # representation. # # @return [RDF::URI] # @since 0.3.0 def canonicalize self.dup.canonicalize! end ## # Converts this URI into its canonical lexical representation. # # @return [RDF::URI] `self` # @since 0.3.0 def canonicalize! # TODO: canonicalize this URI self end ## # Joins several URIs together. # # This method conforms to join normalization semantics as per RFC3986, # section 5.2. This method normalizes URIs, removes some duplicate path # information, such as double slashes, and other behavior specified in the # RFC. # # Other URI building methods are `#/` and `#+`. # # For an up-to-date list of edge case behavior, see the shared examples for # RDF::URI in the rdf-spec project. # # @example Joining two URIs # RDF::URI.new('http://example.org/foo/bar').join('/foo') # #=> RDF::URI('http://example.org/foo') # @see <http://github.com/ruby-rdf/rdf-spec/blob/master/lib/rdf/spec/uri.rb> # @see <http://tools.ietf.org/html/rfc3986#section-5.2> # @see RDF::URI#/ # @see RDF::URI#+ # @param [Array<String, RDF::URI, #to_s>] uris # @return [RDF::URI] def join(*uris) result = @uri.dup uris.each do |uri| result = result.join(uri) end self.class.new(result) end ## # 'Smart separator' URI builder # # This method attempts to use some understanding of the most common use # cases for URLs and URNs to create a simple method for building new URIs # from fragments. This means that it will always insert a separator of # some sort, will remove duplicate seperators, will always assume that a # fragment argument represents a relative and not absolute path, and throws # an exception when an absolute URI is received for a fragment argument. # # This is separate from the semantics for `#join`, which are well-defined by # RFC3986 section 5.2 as part of the merging and normalization process; # this method does not perform any normalization, removal of spurious # paths, or removal of parent directory references `(/../)`. # # See also `#+`, which concatenates the string forms of two URIs without # any sort of checking or processing. # # For an up-to-date list of edge case behavior, see the shared examples for # RDF::URI in the rdf-spec project. # # @param [Any] fragment A URI fragment to be appended to this URI # @return [RDF::URI] # @see RDF::URI#+ # @see RDF::URI#join # @see <http://tools.ietf.org/html/rfc3986#section-5.2> # @see <http://github.com/ruby-rdf/rdf-spec/blob/master/lib/rdf/spec/uri.rb> # @example Building a HTTP URL # RDF::URI.new('http://example.org') / 'jhacker' / 'foaf.ttl' # #=> RDF::URI('http://example.org/jhacker/foaf.ttl') # @example Building a HTTP URL # RDF::URI.new('http://example.org/') / '/jhacker/' / '/foaf.ttl' # #=> RDF::URI('http://example.org/jhacker/foaf.ttl') # @example Using an anchored base URI # RDF::URI.new('http://example.org/users#') / 'jhacker' # #=> RDF::URI('http://example.org/users#jhacker') # @example Building a URN # RDF::URI.new('urn:isbn') / 125235111 # #=> RDF::URI('urn:isbn:125235111') def /(fragment) frag = fragment.respond_to?(:to_uri) ? fragment.to_uri : RDF::URI(fragment.to_s) raise ArgumentError, "Non-absolute URI or string required, got #{frag}" unless frag.relative? if urn? RDF::URI.intern(to_s.sub(/:+$/,'') + ':' + fragment.to_s.sub(/^:+/,'')) else # !urn? case to_s[-1].chr when '#' case fragment.to_s[0].chr when '/' then # Base ending with '#', fragment beginning with '/'. The fragment wins, we use '/'. RDF::URI.intern(to_s.sub(/#+$/,'') + '/' + fragment.to_s.sub(/^\/+/,'')) else RDF::URI.intern(to_s.sub(/#+$/,'') + '#' + fragment.to_s.sub(/^#+/,'')) end else # includes '/'. Results from bases ending in '/' are the same as if there were no trailing slash. case fragment.to_s[0].chr when '#' then # Base ending with '/', fragment beginning with '#'. The fragment wins, we use '#'. RDF::URI.intern(to_s.sub(/\/+$/,'') + '#' + fragment.to_s.sub(/^#+/,'')) else RDF::URI.intern(to_s.sub(/\/+$/,'') + '/' + fragment.to_s.sub(/^\/+/,'')) end end end end ## # Simple concatenation operator. Returns a URI formed from concatenating # the string form of two elements. # # For building URIs from fragments, you may want to use the smart # separator, `#/`. `#join` implements another set of URI building # semantics. # # @example Concatenating a string to a URI # RDF::URI.new('http://example.org/test') + 'test' # #=> RDF::URI('http://example.org/testtest') # @example Concatenating two URIs # RDF::URI.new('http://example.org/test') + RDF::URI.new('test') # #=> RDF::URI('http://example.org/testtest') # @see RDF::URI#/ # @see RDF::URI#join # @param [Any] other # @return [RDF::URI] def +(other) RDF::URI.intern(self.to_s + other.to_s) end ## # Returns `true` if this URI's path component is equal to `/`. # # @example # RDF::URI('http://example.org/').root? #=> true # RDF::URI('http://example.org/path/').root? #=> false # # @return [Boolean] `true` or `false` def root? self.path == '/' || self.path.empty? end ## # Returns a copy of this URI with the path component set to `/`. # # @example # RDF::URI('http://example.org/').root #=> RDF::URI('http://example.org/') # RDF::URI('http://example.org/path/').root #=> RDF::URI('http://example.org/') # # @return [RDF::URI] def root if root? self else uri = self.dup uri.path = '/' uri end end ## # Returns `true` if this URI's path component isn't equal to `/`. # # @example # RDF::URI('http://example.org/').has_parent? #=> false # RDF::URI('http://example.org/path/').has_parent? #=> true # # @return [Boolean] `true` or `false` def has_parent? !root? end ## # Returns a copy of this URI with the path component ascended to the # parent directory, if any. # # @example # RDF::URI('http://example.org/').parent #=> nil # RDF::URI('http://example.org/path/').parent #=> RDF::URI('http://example.org/') # # @return [RDF::URI] def parent case when root? then nil else require 'pathname' unless defined?(Pathname) if path = Pathname.new(self.path).parent uri = self.dup uri.path = path.to_s uri.path << '/' unless uri.root? uri end end end ## # Returns a qualified name (QName) for this URI, if possible. # # @example # RDF::URI('http://purl.org/dc/terms/').qname #=> [:dc, nil] # RDF::URI('http://purl.org/dc/terms/title').qname #=> [:dc, :title] # RDF::DC.title.qname #=> [:dc, :title] # # @return [Array(Symbol, Symbol)] or `nil` if no QName found def qname if self.to_s =~ %r([:/#]([^:/#]*)$) local_name = $1 vocab_uri = local_name.empty? ? self.to_s : self.to_s[0...-(local_name.length)] Vocabulary.each do |vocab| if vocab.to_uri == vocab_uri prefix = vocab.equal?(RDF) ? :rdf : vocab.__prefix__ return [prefix, local_name.empty? ? nil : local_name.to_sym] end end else Vocabulary.each do |vocab| vocab_uri = vocab.to_uri if self.start_with?(vocab_uri) prefix = vocab.equal?(RDF) ? :rdf : vocab.__prefix__ local_name = self.to_s[vocab_uri.length..-1] return [prefix, local_name.empty? ? nil : local_name.to_sym] end end end return nil # no QName found end ## # Returns a duplicate copy of `self`. # # @return [RDF::URI] def dup self.class.new(@uri.dup) end ## # @private def freeze @uri.freeze super end ## # Returns `true` if this URI starts with the given `string`. # # @example # RDF::URI('http://example.org/').start_with?('http') #=> true # RDF::URI('http://example.org/').start_with?('ftp') #=> false # # @param [String, #to_s] string # @return [Boolean] `true` or `false` # @see String#start_with? # @since 0.3.0 def start_with?(string) to_s.start_with?(string.to_s) end alias_method :starts_with?, :start_with? ## # Returns `true` if this URI ends with the given `string`. # # @example # RDF::URI('http://example.org/').end_with?('/') #=> true # RDF::URI('http://example.org/').end_with?('#') #=> false # # @param [String, #to_s] string # @return [Boolean] `true` or `false` # @see String#end_with? # @since 0.3.0 def end_with?(string) to_s.end_with?(string.to_s) end alias_method :ends_with?, :end_with? ## # Checks whether this URI the same term as `other'. # # @example # RDF::URI('http://t.co/').eql?(RDF::URI('http://t.co/')) #=> true # RDF::URI('http://t.co/').eql?('http://t.co/') #=> false # RDF::URI('http://purl.org/dc/terms/').eql?(RDF::DC) #=> false # # @param [RDF::URI] other # @return [Boolean] `true` or `false` def eql?(other) other.is_a?(URI) && self == other end ## # Checks whether this URI is equal to `other` (type checking). # # Per SPARQL data-r2/expr-equal/eq-2-2, numeric can't be compared with other types # # @example # RDF::URI('http://t.co/') == RDF::URI('http://t.co/') #=> true # RDF::URI('http://t.co/') == 'http://t.co/' #=> true # RDF::URI('http://purl.org/dc/terms/') == RDF::DC #=> true # # @param [Object] other # @return [Boolean] `true` or `false` # @see http://www.w3.org/TR/rdf-sparql-query/#func-RDFterm-equal def ==(other) case other when Literal # If other is a Literal, reverse test to consolodate complex type checking logic other == self when String then to_s == other when URI, Addressable::URI then to_s == other.to_s else other.respond_to?(:to_uri) && to_s == other.to_uri.to_s end end ## # Checks for case equality to the given `other` object. # # @example # RDF::URI('http://example.org/') === /example/ #=> true # RDF::URI('http://example.org/') === /foobar/ #=> false # RDF::URI('http://t.co/') === RDF::URI('http://t.co/') #=> true # RDF::URI('http://t.co/') === 'http://t.co/' #=> true # RDF::URI('http://purl.org/dc/terms/') === RDF::DC #=> true # # @param [Object] other # @return [Boolean] `true` or `false` # @since 0.3.0 def ===(other) case other when Regexp then other === to_s else self == other end end ## # Performs a pattern match using the given regular expression. # # @example # RDF::URI('http://example.org/') =~ /example/ #=> 7 # RDF::URI('http://example.org/') =~ /foobar/ #=> nil # # @param [Regexp] pattern # @return [Integer] the position the match starts # @see String#=~ # @since 0.3.0 def =~(pattern) case pattern when Regexp then to_s =~ pattern else super # `Object#=~` returns `false` end end ## # Returns `self`. # # @return [RDF::URI] `self` def to_uri self end ## # Returns the string representation of this URI. # # @example # RDF::URI('http://example.org/').to_str #=> 'http://example.org/' # # @return [String] def to_str @uri.to_s end alias_method :to_s, :to_str ## # Returns a hash code for this URI. # # @return [Fixnum] def hash @uri.hash end ## # Returns `true` if this URI instance supports the `symbol` method. # # @param [Symbol, String, #to_s] symbol # @return [Boolean] `true` or `false` def respond_to?(symbol) @uri.respond_to?(symbol) || super end protected ## # @param [Symbol, String, #to_s] symbol # @param [Array<Object>] args # @yield # @return [Object] # @private def method_missing(symbol, *args, &block) if @uri.respond_to?(symbol) case result = @uri.send(symbol, *args, &block) when Addressable::URI self.class.new(result) else result end else super end end end # URI end # RDF