Current section

Files

Jump to
sparql_client lib sparql_client.ex
Raw

lib/sparql_client.ex

defmodule SPARQL.Client do
@moduledoc """
A SPARQL protocol client.
The [SPARQL Protocol](https://www.w3.org/TR/sparql11-protocol/) consists of
two HTTP operations:
- a query operation for performing SPARQL 1.0 and 1.1 Query Language queries
- an update operation for performing SPARQL Update Language requests, which is
not implemented yet
"""
use Tesla, docs: false
import ContentType
@default_request_method :post
@default_protocol_version "1.0"
@default_select_accept_header [
SPARQL.Query.Result.JSON.media_type,
SPARQL.Query.Result.XML.media_type,
"#{SPARQL.Query.Result.TSV.media_type};p=0.8",
"#{SPARQL.Query.Result.CSV.media_type};p=0.2",
"*/*;p=0.1"
] |> Enum.join(", ")
@default_ask_accept_header [
SPARQL.Query.Result.JSON.media_type,
SPARQL.Query.Result.XML.media_type,
"*/*;p=0.1"
] |> Enum.join(", ")
@default_rdf_accept_header [
RDF.Turtle.media_type,
RDF.NTriples.media_type,
RDF.NQuads.media_type,
JSON.LD.media_type,
"*/*;p=0.1"
] |> Enum.join(", ")
@doc """
The query operation is used to send a SPARQL query to a service endpoint and receive the results of the query.
The query can either be given as string or as an already parsed `SPARQL.Query`.
with %SPARQL.Query{} = query <- SPARQL.Query.new("SELECT * WHERE { ?s ?p ?o }") do
SPARQL.Client.query(query, "http://dbpedia.org/sparql")
end
The type of the result returned depends on the query form:
- `SELECT` queries will return a `SPARQL.Query.ResultSet` struct with a list of
`SPARQL.Query.Result` structs in the `results` field.
- `ASK` queries will return a `SPARQL.Query.ResultSet` struct with the boolean
result in the `results` field
- `CONSTRUCT` and `DESCRIBE` queries will return an RDF data structure
## Specifying the request method
The SPARQL 1.1 protocol spec defines [three methods](https://www.w3.org/TR/sparql11-protocol/#query-operation)
to perform a SPARQL query operation via HTTP, which can be specified via the
`request_method` and `protocol_version` options:
1. query via GET: by setting the options as `request_method: :get` and `protocol_version: "1.1"`
2. query via URL-encoded POST: by setting the options as `request_method: :post` and `protocol_version: "1.0"`
3. query via POST directly: by setting the options as `request_method: :post` and `protocol_version: "1.1"`
In order to work with SPARQL 1.0 services out-of-the-box the second method,
query via URL-encoded POST, is the default.
To perform previous query via GET, you would have to call it like this:
SPARQL.Client.query(query, "http://dbpedia.org/sparql",
request_method: :get, protocol_version: "1.1")
## Specifying custom headers
You can specify custom headers for the HTTP request to the SPARQL service with
the `headers` option and a map.
SPARQL.Client.query(query, "http://some.company.org/private/sparql",
headers: %{"Authorization" => "Basic XXX=="})
## Specifying the response format
The `SPARQL.Client` can handle all of the specified result formats for SPARQL
tuple results (JSON, XML, CSV and TSV) and for `CONSTRUCT` and `DESCRIBE` queries
all RDF serialization formats supported by [RDF.ex](https://github.com/marcelotto/rdf-ex)
can be handled.
If no custom `Accept` header is specified, all accepted formats for the resp.
query form will be set automatically, with
- JSON being the preferred format for `SELECT` and `ASK` queries
- Turtle being the preferred format for `CONSTRUCT` and `DESCRIBE` queries
Although the returned result is mostly independent from the actually returned
response format from the service, you might want to set it manually with the
`result_format` and the name of the format
SPARQL.Client.query(query, "http://some.company.org/private/sparql",
result_format: :xml)
These are the names of the supported formats:
- tuple result formats: `:json, :xml, :csv, :tsv`
- RDF result formats: `:turtle, :ntriples, :nquads, :jsonld`
When a `result_format` is specified the `Accept` header is set to the corresponding
media type. You might however still want to overwrite the `Accept` header, for
example when a SPARQL service uses a non-standard media type for a format.
Note that, when providing a custom non-standard `Accept` header the `result_format`
option is mandatory.
## Specifying an RDF Dataset
The RDF dataset to be queried can be specified [as described in the spec](https://www.w3.org/TR/sparql11-protocol/#dataset)
via the the `default_graph` and `named_graph` options and either a single dataset
names or lists of datasets.
SPARQL.Client.query(query, "http://some.company.org/private/sparql",
default_graph: "http://www.example/sparql/",
named_graph: [
"http://www.other.example/sparql/",
"http://www.another.example/sparql/"
])
## Other options
- `max_redirects`: the number of redirects to follow before the operation fails (default: `5`)
"""
def query(query, endpoint, options \\ %{})
def query(query, endpoint, options) when is_list(options),
do: query(query, endpoint, Map.new(options))
def query(%SPARQL.Query{} = query, endpoint, options) do
with {:ok, client} <- client(query, endpoint, options),
{:ok, response} <- http_request(client, endpoint, query, options)
do
evaluate_response(query, response, options)
end
end
def query(query_string, endpoint, options) do
with %SPARQL.Query{} = query <- SPARQL.Query.new(query_string) do
query(query, endpoint, options)
end
end
############################################################################
# Configuration
defp request_method(%{request_method: request_method}), do: request_method
defp request_method(_), do: @default_request_method
defp protocol_version(%{protocol_version: protocol_version}), do: protocol_version
defp protocol_version(_), do: @default_protocol_version
defp result_format(query_form, %{result_format: result_format})
when query_form in ~w[select ask]a,
do: SPARQL.result_format(result_format)
defp result_format(query_form, %{result_format: result_format})
when query_form in ~w[construct describe]a,
do: RDF.Serialization.format(result_format)
defp result_format(_, _), do: nil
@doc false
def default_accept_header(query_form)
def default_accept_header(:select), do: @default_select_accept_header
def default_accept_header(:ask), do: @default_ask_accept_header
def default_accept_header(:describe), do: @default_rdf_accept_header
def default_accept_header(:construct), do: @default_rdf_accept_header
def default_accept_header(%SPARQL.Query{form: form}), do: default_accept_header(form)
############################################################################
# HTTP Request building
defp client(query, _endpoint, options) do
with {:ok, headers} <- request_headers(query, options) do
{:ok,
Tesla.build_client [
{Tesla.Middleware.Tuples, rescue_errors: :all},
{Tesla.Middleware.Headers, headers},
{Tesla.Middleware.FollowRedirects,
max_redirects: Map.get(options, :max_redirects, 5)}
]
}
end
end
defp request_headers(query, options) do
{:ok,
options
|> Map.get(:headers, %{})
|> add_content_type(request_method(options), protocol_version(options))
|> add_accept_header(query, result_format(query.form, options))
}
end
defp add_content_type(headers, :post, "1.1"),
do: Map.put(headers, "Content-Type", "application/sparql-query")
defp add_content_type(headers, :post, "1.0"),
do: Map.put(headers, "Content-Type", "application/x-www-form-urlencoded")
defp add_content_type(headers, _, _), do: headers
defp add_accept_header(headers, query, nil),
do: Map.put_new(headers, "Accept", default_accept_header(query))
defp add_accept_header(headers, _query, result_format),
do: Map.put_new(headers, "Accept", result_format.media_type)
defp graph_params(options) do
options
|> Enum.reduce([], fn
{:default_graph, graph_uris}, acc when is_list(graph_uris) ->
Enum.reduce graph_uris, acc, fn graph_uri, acc ->
[{"default-graph-uri", graph_uri} | acc]
end
{:default_graph, graph_uri}, acc ->
[{"default-graph-uri", graph_uri} | acc]
{:named_graph, graph_uris}, acc when is_list(graph_uris) ->
Enum.reduce graph_uris, acc, fn graph_uri, acc ->
[{"named-graph-uri", graph_uri} | acc]
end
{:named_graph, graph_uri}, acc ->
[{"named-graph-uri", graph_uri} | acc]
_, acc ->
acc
end)
|> Enum.reverse()
end
############################################################################
# HTTP Request execution
defp http_request(client, endpoint, query, options) do
do_http_request(client, request_method(options), protocol_version(options), endpoint, query, options)
end
defp do_http_request(client, :get, "1.1", endpoint, query, options) do
client
|> get(endpoint <> "?" <> URI.encode_query(
[{"query", query.query_string} | graph_params(options)]))
end
defp do_http_request(client, :post, "1.1", endpoint, query, options) do
url =
case graph_params(options) do
[] -> endpoint
graph_params -> endpoint <> "?" <> URI.encode_query(graph_params)
end
client
|> post(url, query.query_string)
end
defp do_http_request(client, :post, "1.0", endpoint, query, options) do
client
|> post(endpoint, URI.encode_query(
[{"query", query.query_string} | graph_params(options)]))
end
defp do_http_request(_, request_method, protocol_version, _, _, _),
do: {:error, "unknown request method: #{inspect request_method} with SPARQL protocol version #{protocol_version}"}
############################################################################
# HTTP Response evaluation
defp evaluate_response(query, %Tesla.Env{status: status} = response, options)
when status in 200..299
do
with result_format when is_atom(result_format) <-
response_result_format(query.form, response, options) do
if query.form in ~w[construct describe]a or
query.form in result_format.supported_query_forms do
result_format.read_string(response.body)
else
{:error, "unsupported result format for #{query.form} query: #{inspect result_format.media_type}"}
end
end
end
defp evaluate_response(_, response, _), do: {:error, response}
defp response_result_format(query_form, %Tesla.Env{headers: %{"content-type" => content_type}}, options)
do
( content_type
|> parse_content_type()
|> result_format_by_media_type(query_form)
) || result_format(query_form, options)
|| {:error, "unsupported result format for #{query_form} query: #{inspect content_type}"}
end
defp result_format_by_media_type(media_type, query_form)
when query_form in ~w[select ask]a,
do: SPARQL.result_format_by_media_type(media_type)
defp result_format_by_media_type(media_type, query_form)
when query_form in ~w[construct describe]a,
do: RDF.Serialization.format_by_media_type(media_type)
defp parse_content_type(content_type) do
with {:ok, type, subtype, _params} <- content_type(content_type) do
type <> "/" <> subtype
end
end
end