Skip to content

Commit

Permalink
Release 1.1.3.
Browse files Browse the repository at this point in the history
Be more descriminating on Accept headers sent based on different queries. Queries build using the DSL use either RDF content types, or SPARQL Results content types, not both. This includes adding */*;q=0.1 to all requests.
Those using RDF content types:

 * CONSTRUCT, DESCRIBE, DELETE DATA, LOAD, CREATE

Those using SPARQL Results content types:

  * ASK, SELECT, INSERT DATA, CLEAR, DROP

Hopefully, this makes issues such as come up in #51 less likely to happen.

Only use DELETE DATA for #delete_statements if the statement is both constant, and contains no BNodes, otherwise, it falls back to DELETE/INSERT.

Check error response includes query in exception.
When doing updates, change BNodes to Variables.
  • Loading branch information
gkellogg committed Aug 25, 2014
2 parents dccb8da + 2d7f1ac commit 89f12f9
Show file tree
Hide file tree
Showing 11 changed files with 319 additions and 56 deletions.
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.1.2
1.1.3
70 changes: 53 additions & 17 deletions lib/sparql/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,26 @@ class ServerError < StandardError; end
RESULT_TSV = 'text/tab-separated-values'.freeze
RESULT_BOOL = 'text/boolean'.freeze # Sesame-specific
RESULT_BRTR = 'application/x-binary-rdf-results-table'.freeze # Sesame-specific
ACCEPT_JSON = {'Accept' => RESULT_JSON}.freeze
ACCEPT_XML = {'Accept' => RESULT_XML}.freeze
ACCEPT_CSV = {'Accept' => RESULT_CSV}.freeze
ACCEPT_TSV = {'Accept' => RESULT_TSV}.freeze
ACCEPT_BRTR = {'Accept' => RESULT_BRTR}.freeze
RESULT_ALL = [
RESULT_JSON,
RESULT_XML,
RESULT_BOOL,
"#{RESULT_TSV};p=0.8",
"#{RESULT_CSV};p=0.2",
'*/*;p=0.1'
].join(', ').freeze
GRAPH_ALL = (
RDF::Format.content_types.keys +
['*/*;p=0.1']
).join(', ').freeze

ACCEPT_JSON = {'Accept' => RESULT_JSON}.freeze
ACCEPT_XML = {'Accept' => RESULT_XML}.freeze
ACCEPT_CSV = {'Accept' => RESULT_CSV}.freeze
ACCEPT_TSV = {'Accept' => RESULT_TSV}.freeze
ACCEPT_BRTR = {'Accept' => RESULT_BRTR}.freeze
ACCEPT_RESULTS = {'Accept' => RESULT_ALL}.freeze
ACCEPT_GRAPH = {'Accept' => GRAPH_ALL}.freeze

DEFAULT_PROTOCOL = 1.0
DEFAULT_METHOD = :post
Expand Down Expand Up @@ -78,9 +93,7 @@ def initialize(url, options = {}, &block)
@url, @options = url, options.dup
else
@url, @options = RDF::URI.new(url.to_s), options.dup
@headers = {
'Accept' => [RESULT_JSON, RESULT_XML, "#{RESULT_TSV};p=0.8", "#{RESULT_CSV};p=0.2", RDF::Format.content_types.keys.map(&:to_s)].join(', ')
}.merge(@options.delete(:headers) || {})
@headers = @options.delete(:headers) || {}
@http = http_klass(@url.scheme)
end

Expand Down Expand Up @@ -291,12 +304,14 @@ def query(query, options = {})
#
# @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`
# @see http://www.w3.org/TR/sparql11-protocol/#update-operation
def update(query, options = {})
@op = :update
@alt_endpoint = options[:endpoint] unless options[:endpoint].nil?
case @url
when RDF::Queryable
require 'sparql' unless defined?(::SPARQL::Grammar)
Expand All @@ -322,11 +337,11 @@ def response(query, options = {})
request(query, headers) do |response|
case response
when Net::HTTPBadRequest # 400 Bad Request
raise MalformedQuery.new(response.body)
raise MalformedQuery.new(response.body + " Processing query #{query}")
when Net::HTTPClientError # 4xx
raise ClientError.new(response.body)
raise ClientError.new(response.body + " Processing query #{query}")
when Net::HTTPServerError # 5xx
raise ServerError.new(response.body)
raise ServerError.new(response.body + " Processing query #{query}")
when Net::HTTPSuccess # 2xx
response
end
Expand Down Expand Up @@ -378,6 +393,7 @@ def self.parse_json_bindings(json, nodes = {})
##
# @param [Hash{String => String}] value
# @return [RDF::Value]
# @see http://www.w3.org/TR/sparql11-results-json/#select-encode-terms
# @see http://www.w3.org/TR/rdf-sparql-json-res/#variable-binding-results
def self.parse_json_value(value, nodes = {})
case value['type'].to_sym
Expand All @@ -386,7 +402,7 @@ def self.parse_json_value(value, nodes = {})
when :uri
RDF::URI.new(value['value'])
when :literal
RDF::Literal.new(value['value'], :language => value['xml:lang'])
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
Expand Down Expand Up @@ -538,14 +554,16 @@ def self.serialize_uri(uri)
# 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
def self.serialize_value(value)
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.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
Expand All @@ -559,6 +577,8 @@ def self.serialize_value(value)
# @private
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
Expand All @@ -573,15 +593,16 @@ def self.serialize_predicate(value,rdepth=0)
# Serializes a SPARQL graph
#
# @param [RDF::Enumerable] patterns
# @param [Boolean] use_vars (false) Use variables in place of BNodes
# @return [String]
# @private
def self.serialize_patterns(patterns)
def self.serialize_patterns(patterns, use_vars = false)
patterns.map do |pattern|
serialized_pattern = 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)
SPARQL::Client.serialize_value(v, use_vars)
end
end
serialized_pattern.join(' ') + ' .'
Expand Down Expand Up @@ -639,6 +660,16 @@ def http_klass(scheme)
# @see http://www.w3.org/TR/sparql11-protocol/#query-operation
def request(query, headers = {}, &block)
method = (self.options[:method] || DEFAULT_METHOD).to_sym

# 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

request = send("make_#{method}_request", query, headers)

request.basic_auth(url.user, url.password) if url.user && !url.user.empty?
Expand Down Expand Up @@ -674,7 +705,12 @@ def make_get_request(query, headers = {})
# @see http://www.w3.org/TR/sparql11-protocol/#query-via-post-direct
# @see http://www.w3.org/TR/sparql11-protocol/#query-via-post-urlencoded
def make_post_request(query, headers = {})
request = Net::HTTP::Post.new(self.url.request_uri, self.headers.merge(headers))
if @alt_endpoint.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
Expand Down
6 changes: 6 additions & 0 deletions lib/sparql/client/query.rb
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,12 @@ def optional(*patterns)
self
end

##
# @return expects_statements?
def expects_statements?
[:construct, :describe].include?(form)
end

##
# @private
def build_patterns(patterns)
Expand Down
18 changes: 7 additions & 11 deletions lib/sparql/client/repository.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ def initialize(endpoint, options = {})
# @see RDF::Queryable#query
# @see RDF::Query#execute
def query_execute(query, options = {}, &block)
return nil unless block_given?
q = SPARQL::Client::Query.select(query.variables).where(*query.patterns)
client.query(q, options).each do |solution|
client.query(q, options).each do |solution|
yield solution
end
end
Expand Down Expand Up @@ -252,16 +253,11 @@ def delete(*statements)
# @return [void]
def delete_statements(statements)

constant = true
statements.each do |value|
case
when value.respond_to?(:each_statement)
# needs to be flattened... urgh
nil
when (statement = RDF::Statement.from(value)).constant?
# constant
else
constant = false
constant = statements.all? do |value|
# needs to be flattened... urgh
!value.respond_to?(:each_statement) && begin
statement = RDF::Statement.from(value)
statement.constant? && !statement.has_blank_nodes?
end
end

Expand Down
30 changes: 27 additions & 3 deletions lib/sparql/client/update.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ def initialize(*arguments)
end
end

##
# Generic Update always returns statements
#
# @return expects_statements?
def expects_statements?
true
end

def silent
self.options[:silent] = true
self
Expand All @@ -57,6 +65,14 @@ def graph(uri)
self
end

##
# InsertData always returns result set
#
# @return expects_statements?
def expects_statements?
false
end

def to_s
query_text = 'INSERT DATA {'
query_text += ' GRAPH ' + SPARQL::Client.serialize_uri(self.options[:graph]) + ' {' if self.options[:graph]
Expand Down Expand Up @@ -119,19 +135,19 @@ def to_s
buffer << SPARQL::Client.serialize_uri(self.options[:graph])
end
if delete_graph and !delete_graph.empty?
serialized_delete = SPARQL::Client.serialize_patterns delete_graph
serialized_delete = SPARQL::Client.serialize_patterns delete_graph, true
buffer << "DELETE {\n"
buffer += serialized_delete
buffer << "}\n"
end
if insert_graph and !insert_graph.empty?
buffer << "INSERT {\n"
buffer += SPARQL::Client.serialize_patterns insert_graph
buffer += SPARQL::Client.serialize_patterns insert_graph, true
buffer << "}\n"
end
buffer << "WHERE {\n"
if where_graph
buffer += SPARQL::Client.serialize_patterns where_graph
buffer += SPARQL::Client.serialize_patterns where_graph, true
elsif serialized_delete
buffer += serialized_delete
end
Expand Down Expand Up @@ -193,6 +209,14 @@ def all
self
end

##
# Clear always returns statements
#
# @return expects_statements?
def expects_statements?
false
end

def to_s
query_text = 'CLEAR '
query_text += 'SILENT ' if self.options[:silent]
Expand Down
62 changes: 59 additions & 3 deletions spec/client_spec.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
# -*- coding: utf-8 -*-
require File.join(File.dirname(__FILE__), 'spec_helper')
require 'webmock/rspec'
require 'json'

describe SPARQL::Client do
let(:query) {'DESCRIBE ?kb WHERE { ?kb <http://data.linkedmdb.org/resource/movie/actor_name> "Kevin Bacon" . }'}
let(:construct_query) {'CONSTRUCT {?kb <http://data.linkedmdb.org/resource/movie/actor_name> "Kevin Bacon" . } WHERE { ?kb <http://data.linkedmdb.org/resource/movie/actor_name> "Kevin Bacon" . }'}
let(:select_query) {'SELECT ?kb WHERE { ?kb <http://data.linkedmdb.org/resource/movie/actor_name> "Kevin Bacon" . }'}
let(:ask_query) {'ASK WHERE { ?kb <http://data.linkedmdb.org/resource/movie/actor_name> "Kevin Bacon" . }'}
context "when querying a remote endpoint" do
subject {SPARQL::Client.new('http://data.linkedmdb.org/sparql')}

Expand Down Expand Up @@ -80,9 +85,7 @@ def response(header)
client.query(query)
end

it "should support international characters in response body", skip: ENV['CI'].nil? do
require 'webmock/rspec'
require 'json'
it "should support international characters in response body" do
client = SPARQL::Client.new('http://dbpedia.org/sparql')
json = {
:results => {
Expand All @@ -97,6 +100,59 @@ def response(header)
result = client.query(query, :content_type => SPARQL::Client::RESULT_JSON).first
expect(result[:name].to_s).to eq "東京"
end

context "Accept Header" do
it "should use application/sparql-results+json for ASK" do
WebMock.stub_request(:any, 'http://data.linkedmdb.org/sparql').
to_return(:body => '{}', :status => 200, :headers => { 'Content-Type' => 'application/sparql-results+json'})
subject.query(ask_query)
expect(WebMock).to have_requested(:post, "http://data.linkedmdb.org/sparql").
with(:headers => {'Accept'=>'application/sparql-results+json, application/sparql-results+xml, text/boolean, text/tab-separated-values;p=0.8, text/csv;p=0.2, */*;p=0.1'})
end

it "should use application/n-triples for CONSTRUCT" do
WebMock.stub_request(:any, 'http://data.linkedmdb.org/sparql').
to_return(:body => '', :status => 200, :headers => { 'Content-Type' => 'application/n-triples'})
subject.query(construct_query)
expect(WebMock).to have_requested(:post, "http://data.linkedmdb.org/sparql").
with(:headers => {'Accept'=>'application/n-triples, text/plain, */*;p=0.1'})
end

it "should use application/n-triples for DESCRIBE" do
WebMock.stub_request(:any, 'http://data.linkedmdb.org/sparql').
to_return(:body => '', :status => 200, :headers => { 'Content-Type' => 'application/n-triples'})
subject.query(query)
expect(WebMock).to have_requested(:post, "http://data.linkedmdb.org/sparql").
with(:headers => {'Accept'=>'application/n-triples, text/plain, */*;p=0.1'})
end

it "should use application/sparql-results+json for SELECT" do
WebMock.stub_request(:any, 'http://data.linkedmdb.org/sparql').
to_return(:body => '{}', :status => 200, :headers => { 'Content-Type' => 'application/sparql-results+json'})
subject.query(select_query)
expect(WebMock).to have_requested(:post, "http://data.linkedmdb.org/sparql").
with(:headers => {'Accept'=>'application/sparql-results+json, application/sparql-results+xml, text/boolean, text/tab-separated-values;p=0.8, text/csv;p=0.2, */*;p=0.1'})
end
end

context "Error response" do
{
"bad request" => {status: 400, error: SPARQL::Client::MalformedQuery },
"unauthorized" => {status: 401, error: SPARQL::Client::ClientError },
"not found" => {status: 404, error: SPARQL::Client::ClientError },
"internal server error" => {status: 500, error: SPARQL::Client::ServerError },
"not implemented" => {status: 501, error: SPARQL::Client::ServerError },
"service unavailable" => {status: 503, error: SPARQL::Client::ServerError },
}.each do |test, params|
it "detects #{test}" do
WebMock.stub_request(:any, 'http://data.linkedmdb.org/sparql').
to_return(:body => 'the body', :status => params[:status], headers: {'Content-Type' => 'text/plain'})
expect {
subject.query(select_query)
}.to raise_error(params[:error], "the body Processing query #{select_query}")
end
end
end
end

context "when querying an RDF::Repository" do
Expand Down
Loading

0 comments on commit 89f12f9

Please sign in to comment.