module SPARQL # Generate SPARQL Results as Boolean, XML or JSON # # This module is a mixin for RDF::Query::Solutions module Results MIME_TYPES = { json: 'application/sparql-results+json', html: 'text/html', :xml => 'application/sparql-results+xml', csv: 'text/csv', tsv: 'text/tab-separated-values' } ## # Generate Solutions as JSON # @return [String] # @see https://www.w3.org/TR/rdf-sparql-json-res/ def to_json require 'json' unless defined?(::JSON) format = ->(value) do case value when RDF::URI then {type: "uri", value: value.to_s } when RDF::Node then {type: "bnode", value: value.id } when RDF::Literal if value.datatype? {type: "typed-literal", datatype: value.datatype.to_s, value: value.to_s } elsif value.language? {type: "literal", "xml:lang" => value.language.to_s, value: value.to_s } else {type: "literal", value: value.to_s } end when RDF::Statement { type: 'triple', value: { subject: format.call(value.subject), predicate: format.call(value.predicate), object: format.call(value.object) } } end end bindings = self.map do |solution| variable_names.inject({}) do |memo, n| rep = format.call(solution[n]) rep ? memo.merge(n => rep) : memo end end { :head => { vars: variable_names }, :results => { bindings: bindings} }.to_json end ## # Generate Solutions as XML # @return [String] # @see https://www.w3.org/TR/rdf-sparql-XMLres/ def to_xml require 'builder' unless defined?(::Builder) xml = ::Builder::XmlMarkup.new(indent: 2) xml.instruct! format = ->(s) do case s when RDF::URI xml.uri(s.to_s) when RDF::Node xml.bnode(s.id) when RDF::Literal if s.datatype? xml.literal(s.to_s, "datatype" => s.datatype.to_s) elsif s.language xml.literal(s.to_s, "xml:lang" => s.language.to_s) else xml.literal(s.to_s) end when RDF::Statement xml.triple do xml.subject {format.call(s.subject)} xml.predicate {format.call(s.predicate)} xml.object {format.call(s.object)} end end end xml.sparql(xmlns: "http://www.w3.org/2005/sparql-results#") do xml.head do variable_names.each do |n| xml.variable(name: n) end end xml.results do self.each do |solution| xml.result do variable_names.each do |n| s = solution[n] next unless s xml.binding(name: n) do format.call(s) end end end end end end end ## # Generate Solutions as HTML # @return [String] # @see http://www.w3.org/TR/rdf-sparql-XMLres/ def to_html require 'builder' unless defined?(::Builder) xml = ::Builder::XmlMarkup.new(indent: 2) xml.table(class: "sparql") do xml.tbody do xml.tr do variable_names.each do |n| xml.th(n.to_s) end end self.each do |solution| xml.tr do variable_names.each do |n| xml.td(RDF::NTriples.serialize(solution[n])) end end end end end end ## # Generate Solutions as CSV # @return [String] # @see https://www.w3.org/TR/2013/REC-sparql11-results-csv-tsv-20130321/ def to_csv(bnode_map: {}) require 'csv' unless defined?(::CSV) bnode_gen = "_:a" CSV.generate(row_sep: "\r\n") do |csv| csv << variable_names.to_a self.each do |solution| csv << variable_names.map do |n| case term = solution[n] when RDF::Node then bnode_map[term] ||= begin this = bnode_gen bnode_gen = bnode_gen.succ this end when RDF::Statement RDF::Query::Solutions( RDF::Query::Solution.new(subject: term.subject, predicate: term.predicate, object: term.object) ).to_csv(bnode_map: bnode_map).split.last.strip else solution[n].to_s.strip end end end end end ## # Generate Solutions as TSV # @return [String] # @see https://www.w3.org/TR/2013/REC-sparql11-results-csv-tsv-20130321/ def to_tsv require 'csv' unless defined?(::CSV) results = [ variable_names.map {|v| "?#{v}"}.join("\t") ] + self.map do |solution| variable_names.map do |n| case term = solution[n] when RDF::Literal::Integer, RDF::Literal::Decimal, RDF::Literal::Double term.canonicalize.to_s when RDF::Statement emb_stmt = RDF::Query::Solutions( RDF::Query::Solution.new(subject: term.subject, predicate: term.predicate, object: term.object) ).to_tsv.split("\n").last.strip emb_stmt.gsub(/[\t\n\r]/, "\t" => '\t', "\n" => '\n', "\r" => '\r') when nil "" else RDF::NTriples.serialize(term).strip.gsub(/[\t\n\r]/, "\t" => '\t', "\n" => '\n', "\r" => '\r') end end.join("\t") end results.join("\n") + "\n" end end ## # Serialize solutions using the determined format # # @param [RDF::Query::Solutions, RDF::Queryable, Boolean] solutions # Solutions as either a solution set, a Queryable object (such as a graph) or a Boolean value # @param [Hash{Symbol => Object}] options # @option options [#to_sym] :format # Format of results, one of :html, :json or :xml. # May also be an RDF::Writer format to serialize DESCRIBE or CONSTRUCT results # @option options [String] :content_type # Format of results, one of 'application/sparql-results+json' or 'application/sparql-results+xml' # May also be an RDF::Writer content_type to serialize DESCRIBE or CONSTRUCT results # @option options [Array] :content_types # Similar to :content_type, but takes an ordered array of appropriate content types, # and serializes using the first appropriate type, including wild-cards. # @return [String] # String with serialized results and `#content_type` # @raise [RDF::WriterError] when inappropriate formatting options are used def serialize_results(solutions, **options) format = options[:format].to_sym if options[:format] content_type = options[:content_type].to_s.split(';').first content_types = Array(options[:content_types] || '*/*') if !format && !content_type case solutions when RDF::Queryable content_type = first_content_type(content_types, RDF::Format.content_types.keys) || 'text/plain' format = RDF::Writer.for(content_type: content_type).to_sym else content_type = first_content_type(content_types, SPARQL::Results::MIME_TYPES.values) || 'application/sparql-results+xml' format = SPARQL::Results::MIME_TYPES.invert[content_type] end end serialization = case solutions when TrueClass, FalseClass, RDF::Literal::TRUE, RDF::Literal::FALSE solutions = solutions.object if solutions.is_a?(RDF::Literal) case format when :json require 'json' unless defined?(::JSON) {head: {}, boolean: solutions}.to_json when :xml require 'builder' unless defined?(::Builder) xml = ::Builder::XmlMarkup.new(indent: 2) xml.instruct! xml.sparql(xmlns: "http://www.w3.org/2005/sparql-results#") do xml.head xml.boolean(solutions.to_s) end when :html require 'builder' unless defined?(::Builder) content_type = "text/html" xml = ::Builder::XmlMarkup.new(indent: 2) xml.div(solutions.to_s, class: "sparql") else raise RDF::WriterError, "Unknown format #{(format || content_type).inspect} for #{solutions.class}" end when RDF::Queryable begin require 'linkeddata' rescue LoadError require 'rdf/ntriples' end fmt = RDF::Format.for(format ? format.to_sym : {content_type: content_type}) unless fmt fmt = RDF::Format.for(file_extension: format.to_sym) || RDF::NTriples::Format format = fmt.to_sym end format ||= fmt.to_sym content_type ||= fmt.content_type.first results = solutions.dump(format, **options) raise RDF::WriterError, "Unknown format #{fmt.inspect} for #{solutions.class}" unless results results when RDF::Query::Solutions case format when :json then solutions.to_json when :xml then solutions.to_xml when :html then solutions.to_html when :csv then solutions.to_csv when :tsv then solutions.to_tsv else raise RDF::WriterError, "Unknown format #{(format || content_type).inspect} for #{solutions.class}" end end content_type ||= SPARQL::Results::MIME_TYPES[format] if format serialization = serialization.dup if serialization.frozen? serialization.instance_eval do define_singleton_method(:content_type) { content_type } end serialization end module_function :serialize_results ERROR_MESSAGE = %q( SPARQL Processing Service: %s

%s: %s

).freeze ## # Find a content_type from a list using an ordered list of acceptable content types # using wildcard matching # # @param [Array] acceptable # @param [Array] available # @return [String] # # @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1 def first_content_type(acceptable, available) return acceptable.first if available.empty? available.flatten! acceptable.each do |pattern| type = available.detect { |t| File.fnmatch(pattern, t) } return type if type end nil end module_function :first_content_type ## # Serialize error results # # Returns appropriate content based upon an execution exception # @param [Exception] exception # @param [Hash{Symbol => Object}] options # @option options [:format] # Format of results, one of :html, :json or :xml. # May also be an RDF::Writer format to serialize DESCRIBE or CONSTRUCT results # @option options [:content_type] # Format of results, one of 'application/sparql-results+json' or 'application/sparql-results+xml' # May also be an RDF::Writer content_type to serialize DESCRIBE or CONSTRUCT results # @return [String] # String with serialized results and #content_type def serialize_exception(exception, **options) format = options[:format] content_type = options[:content_type] content_type ||= SPARQL::Results::MIME_TYPES[format] serialization = case content_type when 'text/html' title = exception.respond_to?(:title) ? exception.title : exception.class.to_s ERROR_MESSAGE % [title, title, exception.message] else content_type = "text/plain" exception.message end serialization.instance_eval do define_singleton_method(:content_type) { content_type } end serialization end module_function :serialize_exception end