class SPARQL::Client
A SPARQL
1.0/1.1 client for RDF.rb.
@see www.w3.org/TR/sparql11-query/ @see www.w3.org/TR/sparql11-protocol/ @see www.w3.org/TR/sparql11-results-json/ @see www.w3.org/TR/sparql11-results-csv-tsv/
Constants
- ACCEPT_BRTR
- ACCEPT_CSV
- ACCEPT_GRAPH
- ACCEPT_JSON
- ACCEPT_RESULTS
- ACCEPT_TSV
- ACCEPT_XML
- DEFAULT_METHOD
- DEFAULT_PROTOCOL
- GRAPH_ALL
- RESULT_ALL
- RESULT_BOOL
- RESULT_BRTR
- RESULT_CSV
- RESULT_JSON
- RESULT_TSV
- RESULT_XML
- XMLNS
Attributes
The HTTP headers that will be sent in requests to the endpoint.
@return [Hash{String => String}]
Any miscellaneous configuration.
@return [Hash{Symbol => Object}]
Public Class Methods
Close the http connection when object is deallocated
# File lib/sparql/client.rb, line 117 def self.finalize(klass) proc do klass.shutdown if klass.respond_to?(:shutdown) end end
Initialize a new sparql client, either using the URL of a SPARQL
endpoint or an `RDF::Queryable` instance to use the native SPARQL
gem.
@param [String, RDF::Queryable, to_s] url
URL of endpoint, or queryable object.
@param [Hash{Symbol => Object}] options @option options [Symbol] :method (DEFAULT_METHOD
) @option options [Number] :protocol (DEFAULT_PROTOCOL
) @option options [Hash] :headers
HTTP Request headers Defaults `Accept` header based on available reader content types if triples are expected and to SPARQL result types otherwise, to allow for content negotiation based on available readers. Defaults `User-Agent` header, unless one is specified.
@option options [Hash] :read_timeout
# File lib/sparql/client.rb, line 95 def initialize(url, **options, &block) case url when RDF::Queryable @url, @options = url, options.dup else @url, @options = RDF::URI.new(url.to_s), options.dup @headers = @options.delete(:headers) || {} @http = http_klass(@url.scheme) # Close the http connection when object is deallocated ObjectSpace.define_finalizer(self, self.class.finalize(@http)) end if block_given? case block.arity when 1 then block.call(self) else instance_eval(&block) end end end
@param [String, Array<Array<String>>] csv @return [<RDF::Query::Solutions>] @see www.w3.org/TR/sparql11-results-csv-tsv/
# File lib/sparql/client.rb, line 453 def self.parse_csv_bindings(csv, nodes = {}) require 'csv' unless defined?(::CSV) csv = CSV.parse(csv.to_s) unless csv.is_a?(Array) vars = csv.shift solutions = RDF::Query::Solutions.new csv.each do |row| solution = RDF::Query::Solution.new row.each_with_index do |v, i| term = case v when /^_:(.*)$/ then nodes[$1] ||= RDF::Node($1) when /^\w+:.*$/ then RDF::URI(v) else RDF::Literal(v) end solution[vars[i].to_sym] = term end solutions << solution end solutions end
@param [String, Hash] json @return [<RDF::Query::Solutions>] @see www.w3.org/TR/rdf-sparql-json-res/#results
# File lib/sparql/client.rb, line 413 def self.parse_json_bindings(json, nodes = {}) require 'json' unless defined?(::JSON) json = JSON.parse(json.to_s) unless json.is_a?(Hash) case when json.has_key?('boolean') json['boolean'] when json.has_key?('results') solutions = json['results']['bindings'].map do |row| row = row.inject({}) do |cols, (name, value)| cols.merge(name.to_sym => parse_json_value(value, nodes)) end RDF::Query::Solution.new(row) end RDF::Query::Solutions.new(solutions) end end
@param [Hash{String => String}] value @return [RDF::Value] @see www.w3.org/TR/sparql11-results-json/#select-encode-terms @see www.w3.org/TR/rdf-sparql-json-res/#variable-binding-results
# File lib/sparql/client.rb, line 435 def self.parse_json_value(value, nodes = {}) case value['type'].to_sym when :bnode nodes[id = value['value']] ||= RDF::Node.new(id) when :uri RDF::URI.new(value['value']) when :literal RDF::Literal.new(value['value'], datatype: value['datatype'], language: value['xml:lang']) when :'typed-literal' RDF::Literal.new(value['value'], datatype: value['datatype']) else nil end end
@param [String, Array<Array<String>>] tsv @return [<RDF::Query::Solutions>] @see www.w3.org/TR/sparql11-results-csv-tsv/
# File lib/sparql/client.rb, line 477 def self.parse_tsv_bindings(tsv, nodes = {}) tsv = tsv.lines.map {|l| l.chomp.split("\t")} unless tsv.is_a?(Array) vars = tsv.shift.map {|h| h.sub(/^\?/, '')} solutions = RDF::Query::Solutions.new tsv.each do |row| solution = RDF::Query::Solution.new row.each_with_index do |v, i| if !v.empty? term = RDF::NTriples.unserialize(v) || case v when /^\d+\.\d*[eE][+-]?[0-9]+$/ then RDF::Literal::Double.new(v) when /^\d*\.\d+[eE][+-]?[0-9]+$/ then RDF::Literal::Double.new(v) when /^\d*\.\d+$/ then RDF::Literal::Decimal.new(v) when /^\d+$/ then RDF::Literal::Integer.new(v) else RDF::Literal(v) end nodes[term.id] = term if term.is_a? RDF::Node solution[vars[i].to_sym] = term end end solutions << solution end solutions end
@param [String, IO, Nokogiri::XML::Node, REXML::Element] xml @return [<RDF::Query::Solutions>] @see www.w3.org/TR/rdf-sparql-json-res/#results
# File lib/sparql/client.rb, line 506 def self.parse_xml_bindings(xml, nodes = {}) xml.force_encoding(::Encoding::UTF_8) if xml.respond_to?(:force_encoding) if defined?(::Nokogiri) xml = Nokogiri::XML(xml).root unless xml.is_a?(Nokogiri::XML::Document) case when boolean = xml.xpath("//sparql:boolean", XMLNS)[0] boolean.text == 'true' when results = xml.xpath("//sparql:results", XMLNS)[0] solutions = results.elements.map do |result| row = {} result.elements.each do |binding| name = binding.attr('name').to_sym value = binding.elements.first row[name] = parse_xml_value(value, nodes) end RDF::Query::Solution.new(row) end RDF::Query::Solutions.new(solutions) end else # REXML xml = REXML::Document.new(xml).root unless xml.is_a?(REXML::Element) case when boolean = xml.elements['boolean'] boolean.text == 'true' when results = xml.elements['results'] solutions = results.elements.map do |result| row = {} result.elements.each do |binding| name = binding.attributes['name'].to_sym value = binding.select { |node| node.kind_of?(::REXML::Element) }.first row[name] = parse_xml_value(value, nodes) end RDF::Query::Solution.new(row) end RDF::Query::Solutions.new(solutions) end end end
@param [Nokogiri::XML::Element, REXML::Element] value @return [RDF::Value] @see www.w3.org/TR/rdf-sparql-json-res/#variable-binding-results
# File lib/sparql/client.rb, line 551 def self.parse_xml_value(value, nodes = {}) case value.name.to_sym when :bnode nodes[id = value.text] ||= RDF::Node.new(id) when :uri RDF::URI.new(value.text) when :literal lang = value.respond_to?(:attr) ? value.attr('xml:lang') : value.attributes['xml:lang'] datatype = value.respond_to?(:attr) ? value.attr('datatype') : value.attributes['datatype'] RDF::Literal.new(value.text, language: lang, datatype: datatype) else nil end end
Serializes a SPARQL
graph
@param [RDF::Enumerable] patterns @param [Boolean] use_vars (false) Use variables in place of BNodes @return [String] @private
# File lib/sparql/client.rb, line 638 def self.serialize_patterns(patterns, use_vars = false) patterns.map do |pattern| serialized_pattern = case pattern when SPARQL::Client::QueryElement then [pattern.to_s] else RDF::Statement.from(pattern).to_triple.each_with_index.map do |v, i| if i == 1 SPARQL::Client.serialize_predicate(v) else SPARQL::Client.serialize_value(v, use_vars) end end end serialized_pattern.join(' ') + ' .' end end
Serializes a SPARQL
predicate
@param [RDF::Value, Array, String] value @param [Fixnum] rdepth @return [String] @private
# File lib/sparql/client.rb, line 617 def self.serialize_predicate(value,rdepth=0) case value when nil RDF::Query::Variable.new.to_s when String then value when Array s = value.map{|v|serialize_predicate(v,rdepth+1)}.join rdepth > 0 ? "(#{s})" : s when RDF::Value # abbreviate RDF.type in the predicate position per SPARQL grammar value.equal?(RDF.type) ? 'a' : serialize_value(value) end end
Serializes a URI or URI string into SPARQL
syntax.
@param [RDF::URI, String] uri @return [String] @private
# File lib/sparql/client.rb, line 584 def self.serialize_uri(uri) case uri when String then RDF::NTriples.serialize(RDF::URI(uri)) when RDF::URI then RDF::NTriples.serialize(uri) else raise ArgumentError, "expected the graph URI to be a String or RDF::URI, but got #{uri.inspect}" end end
Serializes an `RDF::Value` into SPARQL
syntax.
@param [RDF::Value] value @param [Boolean] use_vars (false) Use variables in place of BNodes @return [String] @private
# File lib/sparql/client.rb, line 599 def self.serialize_value(value, use_vars = false) # SPARQL queries are UTF-8, but support ASCII-style Unicode escapes, so # the N-Triples serializer is fine unless it's a variable: case when value.nil? then RDF::Query::Variable.new.to_s when value.variable? then value.to_s when value.node? then (use_vars ? RDF::Query::Variable.new(value.id) : value) else RDF::NTriples.serialize(value) end end
Public Instance Methods
Executes a boolean `ASK` query.
@param (see Query.ask
) @return [Query]
# File lib/sparql/client.rb, line 138 def ask(*args, **options) call_query_method(:ask, *args, **options) end
@private
# File lib/sparql/client.rb, line 291 def call_query_method(meth, *args, **options) client = self result = Query.send(meth, *args, **options) (class << result; self; end).send(:define_method, :execute) do client.query(self) end result end
Executes a `CLEAR` operation.
This requires that the endpoint support SPARQL
1.1 Update
.
@example `CLEAR GRAPH <example.org/>`
client.clear(:graph, RDF::URI("http://example.org/"))
@example `CLEAR DEFAULT`
client.clear(:default)
@example `CLEAR NAMED`
client.clear(:named)
@example `CLEAR ALL`
client.clear(:all)
@overload clear(what, *arguments)
@param [Symbol, #to_sym] what @param [Array] arguments splat of other arguments to {Update::Clear}. @option options [Boolean] :silent @return [void] `self`
@overload clear(what, *arguments, **options)
@param [Symbol, #to_sym] what @param [Array] arguments splat of other arguments to {Update::Clear}. @param [Hash{Symbol => Object}] options @option options [Boolean] :silent @return [void] `self`
@see www.w3.org/TR/sparql11-update/#clear
# File lib/sparql/client.rb, line 285 def clear(what, *arguments) self.update(Update::Clear.new(what, *arguments)) end
Executes a `CLEAR GRAPH` operation.
This is a convenience wrapper for the {#clear} method.
@example `CLEAR GRAPH <example.org/>`
client.clear_graph("http://example.org/")
@param [RDF::URI, String] graph_uri @param [Hash{Symbol => Object}] options @option options [Boolean] :silent @return [void] `self` @see www.w3.org/TR/sparql11-update/#clear
# File lib/sparql/client.rb, line 250 def clear_graph(graph_uri, **options) self.clear(:graph, graph_uri, **options) end
Closes a client instance by finishing the connection. The client is unavailable for any further data operations; an IOError is raised if such an attempt is made. I/O streams are automatically closed when they are claimed by the garbage collector. @return [void] `self`
# File lib/sparql/client.rb, line 127 def close @http.shutdown if @http @http = nil self end
Executes a graph `CONSTRUCT` query.
@param (see Query.construct
) @return [Query]
# File lib/sparql/client.rb, line 165 def construct(*args, **options) call_query_method(:construct, *args, **options) end
Executes a `DELETE DATA` operation.
This requires that the endpoint support SPARQL
1.1 Update
.
@example Deleting data sourced from a file or URL
data = RDF::Graph.load("https://raw.githubusercontent.com/ruby-rdf/rdf/develop/etc/doap.nt") client.delete_data(data)
@example Deleting data from a named graph
client.delete_data(data, graph: "http://example.org/")
@param [RDF::Enumerable] data @param [Hash{Symbol => Object}] options @option options [RDF::URI, String] :graph @return [void] `self` @see www.w3.org/TR/sparql11-update/#deleteData
# File lib/sparql/client.rb, line 217 def delete_data(data, **options) self.update(Update::DeleteData.new(data, **options)) end
Executes a `DELETE/INSERT` operation.
This requires that the endpoint support SPARQL
1.1 Update
.
@param [RDF::Enumerable] delete_graph @param [RDF::Enumerable] insert_graph @param [RDF::Enumerable] where_graph @param [Hash{Symbol => Object}] options @option options [RDF::URI, String] :graph @return [void] `self` @see www.w3.org/TR/sparql11-update/#deleteInsert
# File lib/sparql/client.rb, line 233 def delete_insert(delete_graph, insert_graph = nil, where_graph = nil, **options) self.update(Update::DeleteInsert.new(delete_graph, insert_graph, where_graph, **options)) end
Executes a `DESCRIBE` query.
@param (see Query.describe
) @return [Query]
# File lib/sparql/client.rb, line 156 def describe(*args, **options) call_query_method(:describe, *args, **options) end
Executes an `INSERT DATA` operation.
This requires that the endpoint support SPARQL
1.1 Update
.
Note that for inserting non-trivial amounts of data, you probably ought to consider using the RDF store's native bulk-loading facilities or APIs, as `INSERT DATA` operations entail comparably higher parsing overhead.
@example Inserting data constructed ad-hoc
client.insert_data(RDF::Graph.new { |graph| graph << [:jhacker, RDF::Vocab::FOAF.name, "J. Random Hacker"] })
@example Inserting data sourced from a file or URL
data = RDF::Graph.load("https://raw.githubusercontent.com/ruby-rdf/rdf/develop/etc/doap.nt") client.insert_data(data)
@example Inserting data into a named graph
client.insert_data(data, graph: "http://example.org/")
@param [RDF::Enumerable] data @param [Hash{Symbol => Object}] options @option options [RDF::URI, String] :graph @return [void] `self` @see www.w3.org/TR/sparql11-update/#insertData
# File lib/sparql/client.rb, line 196 def insert_data(data, **options) self.update(Update::InsertData.new(data, **options)) end
Returns a developer-friendly representation of this object.
@return [String]
# File lib/sparql/client.rb, line 667 def inspect sprintf("#<%s:%#0x(%s)>", self.class.name, __id__, url.to_s) end
Outputs a developer-friendly representation of this object to `stderr`.
@return [void]
# File lib/sparql/client.rb, line 659 def inspect! warn(inspect) end
Returns a mapping of blank node results for this client.
@private
# File lib/sparql/client.rb, line 304 def nodes @nodes ||= {} end
@param [Net::HTTPSuccess] response @param [Hash{Symbol => Object}] options @return [RDF::Enumerable]
# File lib/sparql/client.rb, line 569 def parse_rdf_serialization(response, **options) options = {content_type: response.content_type} unless options[:content_type] if reader = RDF::Reader.for(options) reader.new(response.body) else raise RDF::ReaderError, "no RDF reader was found for #{options}." end end
@param [Net::HTTPSuccess] response @param [Hash{Symbol => Object}] options @return [Object]
# File lib/sparql/client.rb, line 390 def parse_response(response, **options) case options[:content_type] || response.content_type when NilClass response.body when RESULT_BOOL # Sesame-specific response.body == 'true' when RESULT_JSON self.class.parse_json_bindings(response.body, nodes) when RESULT_XML self.class.parse_xml_bindings(response.body, nodes) when RESULT_CSV self.class.parse_csv_bindings(response.body, nodes) when RESULT_TSV self.class.parse_tsv_bindings(response.body, nodes) else parse_rdf_serialization(response, **options) end end
Executes a SPARQL
query and returns the parsed results.
@param [String, to_s] query @param [Hash{Symbol => Object}] options @option options [String] :content_type @option options [Hash] :headers @return [Array<RDF::Query::Solution>] @raise [IOError] if connection is closed @see www.w3.org/TR/sparql11-protocol/#query-operation
# File lib/sparql/client.rb, line 318 def query(query, **options) @op = :query @alt_endpoint = options[:endpoint] case @url when RDF::Queryable require 'sparql' unless defined?(::SPARQL::Grammar) begin SPARQL.execute(query, @url, optimize: true, **options) rescue SPARQL::MalformedQuery $stderr.puts "error running #{query}: #{$!}" raise end else parse_response(response(query, **options), **options) end end
Executes a SPARQL
query and returns the Net::HTTP::Response of the result.
@param [String, to_s] query @param [Hash{Symbol => Object}] options @option options [String] :content_type @option options [Hash] :headers @return [String] @raise [IOError] if connection is closed
# File lib/sparql/client.rb, line 369 def response(query, **options) headers = options[:headers] || @headers headers['Accept'] = options[:content_type] if options[:content_type] request(query, headers) do |response| case response when Net::HTTPBadRequest # 400 Bad Request raise MalformedQuery.new(response.body + " Processing query #{query}") when Net::HTTPClientError # 4xx raise ClientError.new(response.body + " Processing query #{query}") when Net::HTTPServerError # 5xx raise ServerError.new(response.body + " Processing query #{query}") when Net::HTTPSuccess # 2xx response end end end
Executes a tuple `SELECT` query.
@param (see Query.select
) @return [Query]
# File lib/sparql/client.rb, line 147 def select(*args, **options) call_query_method(:select, *args, **options) end
Executes a SPARQL
update operation.
@param [String, to_s] query @param [Hash{Symbol => Object}] options @option options [String] :endpoint @option options [String] :content_type @option options [Hash] :headers @return [void] `self` @raise [IOError] if connection is closed @see www.w3.org/TR/sparql11-protocol/#update-operation
# File lib/sparql/client.rb, line 346 def update(query, **options) @op = :update @alt_endpoint = options[:endpoint] case @url when RDF::Queryable require 'sparql' unless defined?(::SPARQL::Grammar) SPARQL.execute(query, @url, update: true, optimize: true, **options) else response(query, **options) end self end
Protected Instance Methods
Returns an HTTP class or HTTP proxy class based on the `http_proxy` and `https_proxy` environment variables.
@param [String] scheme @return [Net::HTTP::Proxy]
# File lib/sparql/client.rb, line 679 def http_klass(scheme) proxy_url = nil case scheme when 'http' value = ENV['http_proxy'] proxy_url = URI.parse(value) unless value.nil? || value.empty? when 'https' value = ENV['https_proxy'] proxy_url = URI.parse(value) unless value.nil? || value.empty? end klass = Net::HTTP::Persistent.new(name: self.class.to_s, proxy: proxy_url) klass.keep_alive = @options[:keep_alive] || 120 klass.read_timeout = @options[:read_timeout] || 60 klass end
Constructs an HTTP GET request according to the SPARQL
Protocol.
@param [String, to_s] query @param [Hash{String => String}] headers @return [Net::HTTPRequest] @see www.w3.org/TR/sparql11-protocol/#query-via-get
# File lib/sparql/client.rb, line 758 def make_get_request(query, headers = {}) url = self.url.dup url.query_values = (url.query_values || {}).merge(query: query.to_s) set_url_default_graph url unless @options[:graph].nil? request = Net::HTTP::Get.new(url.request_uri, self.headers.merge(headers)) request end
Constructs an HTTP POST request according to the SPARQL
Protocol.
@param [String, to_s] query @param [Hash{String => String}] headers @return [Net::HTTPRequest] @see www.w3.org/TR/sparql11-protocol/#query-via-post-direct @see www.w3.org/TR/sparql11-protocol/#query-via-post-urlencoded
# File lib/sparql/client.rb, line 774 def make_post_request(query, headers = {}) if @alt_endpoint.nil? url = self.url.dup set_url_default_graph url unless @options[:graph].nil? endpoint = url.request_uri else endpoint = @alt_endpoint end request = Net::HTTP::Post.new(endpoint, self.headers.merge(headers)) case (self.options[:protocol] || DEFAULT_PROTOCOL).to_s when '1.1' request['Content-Type'] = 'application/sparql-' + (@op || :query).to_s request.body = query.to_s when '1.0' form_data = {(@op || :query) => query.to_s} form_data.merge!( {:'default-graph-uri' => @options[:graph]} ) if !@options[:graph].nil? && (@op.eql? :query) form_data.merge!( {:'using-graph-uri' => @options[:graph]} ) if !@options[:graph].nil? && (@op.eql? :update) request.set_form_data(form_data) else raise ArgumentError, "unknown SPARQL protocol version: #{self.options[:protocol].inspect}" end request end
Performs an HTTP request against the SPARQL
endpoint.
@param [String, to_s] query @param [Hash{String => String}] headers
HTTP Request headers Defaults `Accept` header based on available reader content types if triples are expected and to SPARQL result types otherwise, to allow for content negotiation based on available readers. Defaults `User-Agent` header, unless one is specified.
@yield [response] @yieldparam [Net::HTTPResponse] response @return [Net::HTTPResponse] @raise [IOError] if connection is closed @see www.w3.org/TR/sparql11-protocol/#query-operation
# File lib/sparql/client.rb, line 710 def request(query, headers = {}, &block) # Make sure an appropriate Accept header is present headers['Accept'] ||= if (query.respond_to?(:expects_statements?) ? query.expects_statements? : (query =~ /CONSTRUCT|DESCRIBE|DELETE|CLEAR/)) GRAPH_ALL else RESULT_ALL end headers['User-Agent'] ||= "Ruby SPARQL::Client/#{SPARQL::Client::VERSION}" request = send("make_#{request_method(query)}_request", query, headers) request.basic_auth(url.user, url.password) if url.user && !url.user.empty? pre_http_hook(request) if respond_to?(:pre_http_hook) raise IOError, "Client has been closed" unless @http response = @http.request(::URI.parse(url.to_s), request) post_http_hook(response) if respond_to?(:post_http_hook) 10.times do if response.kind_of? Net::HTTPRedirection response = @http.request(::URI.parse(response['location']), request) else return block_given? ? block.call(response) : response end end raise ServerError, "Infinite redirect at #{url}. Redirected more than 10 times." end
Return the HTTP verb for posting this request. this is useful if you need to override the HTTP verb based on the request being made. (e.g. Marmotta 3.3.0 requires GET for DELETE requests, but can accept POST for INSERT)
# File lib/sparql/client.rb, line 746 def request_method(query) (options[:method] || DEFAULT_METHOD).to_sym end
Setup url query parameter to use a specified default graph
@see www.w3.org/TR/sparql11-protocol/#query-operation @see www.w3.org/TR/sparql11-protocol/#update-operation
# File lib/sparql/client.rb, line 808 def set_url_default_graph url if @options[:graph].is_a? Array graphs = @options[:graph].map {|graph| CGI::escape(graph) } else graphs = CGI::escape(@options[:graph]) end case @op when :query url.query_values = (url.query_values || {}) .merge(:'default-graph-uri' => graphs) when :update url.query_values = (url.query_values || {}) .merge(:'using-graph-uri' => graphs) end end