begin require 'xml' $libxml_enabled = true rescue LoadError end require 'parsedate' require File.join(File.dirname(__FILE__), 'duration') module RdfContext # An RDF Literal, with value, encoding and language elements. class Literal class Encoding attr_reader :value # New Encoding for a literal, typed, untyped or XMLLiteral def initialize(value) @value = URIRef.new(value.to_s) if value end # Shortcut for Literal::Encoding.new("http://www.w3.org/2001/XMLSchema#boolean") def self.boolean @boolean ||= coerce XSD_NS.boolean end # Shortcut for Literal::Encoding.new("http://www.w3.org/2001/XMLSchema#double") def self.double @double ||= coerce XSD_NS.double end # Shortcut for Literal::Encoding.new("http://www.w3.org/2001/XMLSchema#float") def self.float @float ||= coerce XSD_NS.float end # Shortcut for Literal::Encoding.new("http://www.w3.org/2001/XMLSchema#int") def self.integer @integer ||= coerce XSD_NS.int end # Shortcut for Literal::Encoding.new("http://www.w3.org/2001/XMLSchema#date") def self.date @date ||= coerce XSD_NS.date end # Shortcut for Literal::Encoding.new("http://www.w3.org/2001/XMLSchema#dateTime") def self.datetime @datetime ||= coerce XSD_NS.dateTime end # Shortcut for Literal::Encoding.new("http://www.w3.org/2001/XMLSchema#duration") def self.duration @duration ||= coerce XSD_NS.duration end # Shortcut for Literal::Encoding.new("http://www.w3.org/2001/XMLSchema#string") def self.string @string ||= coerce XSD_NS.string end # Shortcut for Literal::Encoding.new("http://www.w3.org/2001/XMLSchema#time") def self.time @time ||= coerce XSD_NS.time end # Create from URI, empty or nil string def self.coerce(string_or_nil) if string_or_nil.nil? || string_or_nil == '' the_null_encoding elsif xmlliteral == string_or_nil.to_s xmlliteral else new string_or_nil end end def self.the_null_encoding @the_null_encoding ||= Null.new(nil) end def self.xmlliteral @xmlliteral ||= XMLLiteral.new("http://www.w3.org/1999/02/22-rdf-syntax-ns#XMLLiteral") end # Compare against another encoding, or a URI of a literal type def ==(other) case other when String other == @value.to_s when self.class other.value.to_s == @value.to_s else false end end # Generate hash of type to determine uniqueness def hash @value.hash end def to_s @value.to_s end # Serialize literal, adding datatype and language elements, if present. # XMLLiteral and String values are RDF-escaped. def format_as_n3(content, lang) quoted_content = "\"#{content.to_s.rdf_escape}\"^^<#{value}>" end # Serialize literal to TriX def format_as_trix(content, lang) lang = " xml:lang=\"#{lang}\"" if lang "#{content}" end # Return content and hash appropriate for encoding in XML # # ==== Example # Encoding.string.xml_args("foo", "en-US") => ["foo", {"rdf:datatype" => "xs:string"}] def xml_args(content, lang) hash = {"rdf:datatype" => @value.to_s} [content.to_s, hash] end # Compare literal contents, ignore language def compare_contents(a, b, same_lang) a == b end # Encode literal contents def encode_contents(contents, options) case @value when XSD_NS.boolean then %w(1 true).include?(contents.to_s) ? "true" : "false" when XSD_NS.time then contents.is_a?(Time) ? contents.strftime("%H:%M:%S%Z").sub(/\+00:00|UTC/, "Z") : contents.to_s when XSD_NS.dateTime then contents.is_a?(DateTime) ? contents.strftime("%Y-%m-%dT%H:%M:%S%Z").sub(/\+00:00|UTC/, "Z") : contents.to_s when XSD_NS.date then contents.is_a?(Date) ? contents.strftime("%Y-%m-%d%Z").sub(/\+00:00|UTC/, "Z") : contents.to_s when XSD_NS.duration then contents.is_a?(Duration) ? contents.to_s(:xml) : contents.to_s else contents.to_s end end end # The null encoding class Null < Encoding def to_s '' end # Format content for n3/N-Triples. Quote an RDF-escape and include language def format_as_n3(content, lang) "\"#{content.to_s.rdf_escape}\"" + (lang ? "@#{lang}" : "") end # Format content for TriX def format_as_trix(content, lang) if lang "#{content}" else "#{content}" end end # Return content and hash appropriate for encoding in XML # # ==== Example # Encoding.the_null_encoding.xml_args("foo", "en-US") => ["foo", {"xml:lang" => "en-US"}] def xml_args(content, lang) hash = {} hash["xml:lang"] = lang if lang [content, hash] end # Compare literal contents, requiring languages to match def compare_contents(a, b, same_lang) a == b && same_lang end def inspect "" end end class XMLLiteral < Encoding # Compare XMLLiterals # # Nokogiri doesn't do a deep compare of elements # # Convert node-sets to hash using ActiveSupport::XmlMini and compare hashes. def compare_contents(a, b, same_lang) begin a_hash = ActiveSupport::XmlMini.parse("#{a}") b_hash = ActiveSupport::XmlMini.parse("#{b}") a_hash == b_hash rescue super end end def format_as_n3(content, lang) "\"#{content.to_s.rdf_escape}\"^^<#{value}>" end def format_as_trix(content, lang) "#{content}" end def xml_args(content, lang) hash = {"rdf:parseType" => "Literal"} [content, hash] end # Map namespaces from context to each top-level element found within node-set def encode_contents(contents, options) #puts "encode_contents: '#{contents}'" if contents.is_a?(String) ns_hash = options[:namespaces].values.inject({}) {|h, ns| h.merge(ns.xmlns_hash)} ns_strs = [] ns_hash.each_pair {|a, u| ns_strs << "#{a}=\"#{u}\""} # Add inherited namespaces to created root element so that they're inherited to sub-elements contents = Nokogiri::XML::Document.parse("#{contents}").root.children end # Add already mapped namespaces and language @contents = contents.map do |c| if $libxml_enabled c = Nokogiri::XML.parse(c.copy(true).to_s) if c.is_a?(LibXML::XML::Node) end if c.is_a?(Nokogiri::XML::Element) # Gather namespaces from self and decendant nodes c.traverse do |n| ns = n.namespace next unless ns prefix = ns.prefix ? "xmlns:#{ns.prefix}" : "xmlns" c[prefix] = ns.href unless c.namespaces[prefix] end # Add lanuage if options[:language] && c["lang"].to_s.empty? c["xml:lang"] = options[:language] end end c.to_html end.join("") end end class Language attr_accessor :value def initialize(string) @value = string.to_s.downcase end def clean(string) case string when "eng"; "en" else string end end def == (other) case other when String other == @value when self.class other.value == @value end end def to_s; @value; end end attr_accessor :contents, :encoding, :lang # Create a new Literal. Optinally pass a namespaces hash # for use in applying to rdf::XMLLiteral values. def initialize(contents, encoding, options = {}) unless encoding.is_a?(Encoding) raise TypeError, "#{encoding.inspect} should be an instance of Encoding" end @encoding = encoding lang = options[:language] @lang = Language.new(lang) if lang options = {:namespaces => {}}.merge(options) @contents = @encoding.encode_contents(contents, options) end # Create literal from a string that is already N3 encoded. def self.n3_encoded(contents, language, encoding = nil) encoding = encoding.nil? ? Encoding.the_null_encoding : Encoding.coerce(encoding) options = {} options[:language] = language if language #puts "encoded: #{contents.dump}" contents = contents.rdf_unescape #puts "unencoded: #{contents.dump}" new(contents, encoding, options) end # Create an un-typed literal with a language def self.untyped(contents, language = nil) options = {} options[:language] = language if language new(contents, Encoding.the_null_encoding, options) end # Create a typed literal # Options include: # _namespaces_:: A hash of namespace entries (for XMLLiteral) def self.typed(contents, encoding, options = {}) encoding = Encoding.coerce(encoding) new(contents, encoding, options) end # Create a literal appropriate for type of object by datatype introspection def self.build_from(object) new(object.to_s, infer_encoding_for(object)) end # Infer the proper XML datatype for the given object def self.infer_encoding_for(object) case object when TrueClass then Encoding.boolean when FalseClass then Encoding.boolean when Integer then Encoding.integer when Float then Encoding.float when Time then Encoding.time when DateTime then Encoding.datetime when Date then Encoding.date when Duration then Encoding.duration else Encoding.string end end class << self protected :new end # Compare literal with another literal or a string. # If a string is passed, only contents must match. # Otherwise, compare encoding types, contents and languages. def ==(other) case other when String then other == self.contents when self.class other.encoding == @encoding && @encoding.compare_contents(self.contents, other.contents, other.lang == @lang) else false end end def hash [@contents, @encoding, @lang].hash end # Output literal in N3 format def to_n3 encoding.format_as_n3(self.contents, @lang) end alias_method :to_ntriples, :to_n3 def inspect "#{self.class}[#{self.to_n3}]" end # Output literal in TriX format def to_trix encoding.format_as_trix(@contents, @lang) end # Create native representation for value def to_native case encoding when Encoding.boolean then @contents.to_s == "true" when Encoding.double then @contents.to_s.to_f when Encoding.integer then @contents.to_s.to_i when Encoding.float then @contents.to_s.to_f when Encoding.time then Time.parse(@contents.to_s) when Encoding.datetime then DateTime.parse(@contents.to_s) when Encoding.date then Date.parse(@contents.to_s) when Encoding.duration then Duration.parse(@contents.to_s) else @contents.to_s end end # Return content and hash appropriate for encoding in XML # # ==== Example # Encoding.the_null_encoding.xml_args("foo", "en-US") => ["foo", {"xml:lang" => "en-US"}] def xml_args encoding.xml_args(@contents, @lang) end # Is this an XMLLiteral? def xmlliteral? encoding.is_a?(XMLLiteral) end # Output literal contents as a string def to_s self.contents.to_s end end end