This generic REST API allows developers direct access to a SPARQL endpoint via
/class
or /resource
entry points. From there on the developer can navigate
the graph by intuitive path queries (alternating between resource and
property). The generic REST API transforms the RDF triple world into an
object-centric view (an object is represented by all outgoing edges). It
however does not hide the fact that the interlinked objects form a graph.
/class
- class (type) driven query/resource
- REST resource (object) driven query/batch
- batch processing (you have to use POST
method, has semantic of GET
for given ids/values)A GET
on a class will return an array of ids
.
A GET
on a single object will return its properties and values.
A GET
on a single property will return its values (or ids).
GET /bosch/class/mr:Milkrun
{
"ids": [":m1", ":m2", ":m3"]
}
GET /bosch/class/foaf:Person/:bert/rdfs:label
{
"values": [
{"value": "Bert Treb", "datatype": "rdf:langString", "language": "en"},
{"value": "Bert", "datatype": "rdf:langString", "language": "en"},
{"value": "Mr. T", "datatype": "rdf:langString", "language": "en"}
]
}
GET /bosch/resource/:c3/mr:crate_weight
{
"values": [
{"value": "123.4", "datatype": "weight:kg", "language": ""}
]
}
For consistency reasons the result of ids
and values
is always a list.
The result can contain a single element [":m1"]
or multiple [":m1", ":m3"]
.
In order to describe the value's datatype and language, each value is represented by a JSON object.
This will become also necessary for correctly POST
values.
"Give me everything about Bert":
GET /bosch/resource/:bert
{
"id-map": {
"foaf:name": {
"values": [{"value": "Bert Treb", "datatype": "xsd:string", "language": ""}]
},
"job:drives": {
"ids": [":m1", ":m2"]
}
// ...
}
}
A POST
message appends (never overwrites) (to) objects.
We use the same syntax for the body of POST
messages, as returned in GET
messages.
We will show the changes in the knowledge base for each example with an inline diff . At the beginning the knowledge base is empty (no triples exist).
POST /bosch/class/foaf:Person/
{
"id-map": {
"foaf:name": {
"values": [{"value": "Bert Treb", "datatype": "rdf:langString", "language": "en"}]
},
"job:drives": {
"ids": [":m1", ":m2"]
}
}
}
{
"ids": ["uuid:1234"],
}
POST /bosch/class/foaf:Person/uuid:1234/foaf:name
{
"values": [
{"value": "Bertos Trebos", "datatype": "rdf:langString", "language": "es"},
{"value": "Berto Trebinski", "datatype": "rdf:langString", "language": "pl"}
]
}
{}
POST /bosch/class/foaf:Person/uuid:1234/job:drives
{
"ids": [":m3"]
}
{}
POST /bosch/resource/:c3/mr:crate_weight
{
"values": [
{"value": "432.1", "datatype": "weight:kg", "language": ""}
]
}
{}
A PUT
, in contrast, replaces and overwrites objects / facts in the knowledge base.
PUT /bosch/class/foaf:Person/uuid:1234/foaf:name
{
"values": [
{"value": "Herr Treb", "datatype": "rdf:langString", "language": "de"}
]
}
{}
Statements which are not re-written are deleted.
Notice however, that statements can be implied by the path (here rdf:type foaf:Person
).
PUT /bosch/class/foaf:Person/uuid:1234
{
"id-map": {
"job:drives": {
"ids": [":m4"]
},
"foaf:knows": {
"ids": [":peter", ":marry"]
}
}
}
{}
PUT /bosch/class/mr:Milkrun/:a_new_uri
{
"id-map": {
"rdfs:comment": {
"values": [{ "value": "Das ist eine deutsche Erklärung.", "datatype": "rdf:langString", "language": "de" }]
}
}
}
{}
PUT /bosch/resource/:610e19ad-e1b6-436f-8c91-f9ba1398364d
{
"id-map": {
":someproperty": {
"values":[
{"value": "2016-11-02T12:00:00Z", "datatype": "gtrace:Date", "language":""}
]
}
}
}
{}
Assume the following knowledge base for the next examples:
DELETE /bosch/class/foaf:Person/uuid:1234/job:drives/:m4
{}
DELETE /bosch/class/foaf:Person/uuid:1234/mr:knows
{}
DELETE /bosch/class/foaf:Person/uuid:1234
{}
Assume the following knowledge base for the next example:
DELETE /bosch/resource/:delbert
{}
In this section we introduce more complex query capabilities: property path and wildcards.
We allow the usage of property paths. They can only be used to replace a property id.
We indicate the use of a property path by round brackets ( )
.
GET /bosch/class/foaf:Person/:bert/(foaf:name|job:drives)
{
"ids": [":m1", ":m2"],
"values": [
{"value": "Bert Treb", "datatype": "xsd:string", "language": ""}
]
}
GET /bosch/class/foaf:Person/:bert/(foaf:child+)
{
"ids": [":peter", ":marry", ":paul"]
}
GET /bosch/class/foaf:Person/:bert/(foaf:child*)
{
"ids": [":bert", ":peter", ":marry", ":paul"]
}
GET /bosch/class/foaf:Person/:bert/(foaf:hasFriend/foaf:name)
{
"values": [
{"value": "Peter", "datatype": "rdf:langString", "language": "en"},
{"value": "Paul", "datatype": "rdf:langString", "language": "en"},
{"value": "marry", "datatype": "rdf:langString", "language": "en"}
]
}
GET /bosch/class/mr:Milkrun/:m1/(^mr:isTransportedBy)
{
"ids": [":c2", ":c6", ":c8"]
}
The wildcard *
can be seen as an unnamed variable which iterates over all possibilities depending on the
location in the URI.
As a rule of thumb each *
will cause the result to contain an additional nesting layer, mostly using the id-map
JSON object. Its keys are the ids the *
iterated over, its values are nested results for that key.
An example:
GET /bosch/resource/:bert/foaf:knows/*/foaf:name
{
"id-map": {
":peter": {
"values": [{"value": "Peter Retep", "datatype": "xsd:string", "language": ""}]
},
":hans": {
"values": [{"value": "Hans Snah", "datatype": "xsd:string", "language": ""}]
}
}
}
A nested example:
GET /bosch/resource/*/job:drives/*/rdfs:label
{
"id-map": {
":bert": {
"id-map": {
":m1": {
"values": [{"value": "Milkrun 1", "datatype": "xsd:string", "language": ""}]
}
}
},
":hans": {
"id-map": {
":m2": {
"values": [
{"value": "Milkrun 2", "datatype": "xsd:string", "language": ""},
{"value": "Milkrun Old", "datatype": "xsd:string", "language": ""}
]
}
}
}
}
}
Wildcard example with respect to final *
that matches objects and literals
GET /bosch/resource/:bert/*/*
{
"id-map": {
"foaf:name": {
"values": [{"value": "Bert Treb", "datatype": "xsd:string", "language": ""}]
},
"rdfs:label": {
"values": [ // values can't be further expanded by final *
{"value": "Bert", "datatype": "rdf:langString", "language": "en"},
{"value": "Bert Treb", "datatype": "rdf:langString", "language": "en"}
]
},
"job:drives": {
"id-map": { // the final * leads to the expansion of all objects
":m1": {
"id-map": {
"mr:drives_to": { "ids": [":hall2"] }
// ...
}
}
}
}
}
}
By default, wildcard expansion will stop at Literals as they don't have outgoing edges.
If desired, you can use a Property Path to traverse against the edge direction, which then creates a value-map
.
Nested id- and value-maps example:
GET /bosch/resource/*/foaf:name/*/(^rdfs:label)
{
"id-map": {
":bert": {
"value-map": [
[
{"value": "Bert Treb", "datatype": "xsd:string", "language": ""},
{"ids": [":bert", ":bert2"]}
],
[
{"value": "Bert", "datatype": "xsd:string", "language": ""},
{"ids": [":bert"]}
]
]
}
}
}
This section shows how the query can be manipulated in terms of filtering, sorting and paging. We use RQL to express the manipulation. Like stated above in uri syntax RQL is only usable at the end of the path query.
For filtering we introduce regex
which is used on string data type properties to search for objects which have values like the given regular expression.
In places where usually ids are returned, regex
can filter them by constraining a properties value.
GET /bosch/class/foaf:Person?regex(foaf:name,^Bert.*)
{
"ids": [":berthold", ":bert", ":berta"]
}
GET /bosch/class/foaf:Person/:bert/foaf:knows?regex(foaf:name,^.*ter$)
{
"ids": [":peter", ":walter"]
}
We sort with the sort
RQL statement. You have to define ascending +
or descending -
order.
The sort
statement needs a data type property with comparable value.
GET /bosch/class/mr:Crate?sort(+mr:crate_cost)
{
"ids": [":c3", ":c4", ":c5", ":c6", ":c1", ":c2"]
}
GET /bosch/class/foaf:Person/:bert/foaf:knows?sort(-foaf:name)
{
"ids": [":peter", ":marry", ":hans"]
}
We page the result with the limit
RQL statement. It has an optional offset value and requires a limit value how many results will be returned.
To make sure that the result is deterministic, you should use a sort
statement before.
GET /bosch/class/foaf:Person?sort(+foaf:name)
{
"ids": [":alfred", ":bert", ":clara", ":daniel"]
}
GET /bosch/class/foaf:Person?sort(+foaf:name),limit(0,2)
{
"ids": [":alfred", ":bert"]
}
GET /bosch/class/foaf:Person?sort(+foaf:name),limit(2,2)
{
"ids": [":berta", ":berthold"]
}
GET /bosch/class/foaf:Person?sort(+foaf:name),limit(20,2)
{}
GET /bosch/class/foaf:Person?sort(+foaf:name),limit(3)
{
"ids": [":alfred", ":bert", ":berta"]
}
GET /bosch/class/foaf:Person?limit(0,2)
{
"ids": [":bert", ":daniel"]
}
GET /bosch/class/foaf:Person?limit(0,2)
{
"ids": [":clara", ":bert"]
}
We allow aggregation of values with the RQL statements count
, sum
and avg
.
These statements always operate on the last leaf type.
sum
and avg
can only be applied to numeric values (we throw an error if
they are not numeric).
GET /bosch/class/foaf:Person
{
"ids": [":bert", ":peter", ":marry"]
}
GET /bosch/class/foaf:Person?count
{
"count": 11
}
GET /bosch/resource/:bert
{
"id-map": {
"foaf:name": {
"values": [{"value": "Bert Treb", "datatype": "rdf:langString", "language": "en"}]
},
"job:drives": {
"ids": [":m1", ":m2"]
}
}
}
GET /bosch/resource/:bert?count
{
"id-map": {
"foaf:name": {
"count": 2
},
"job:drives": {
"count": 2
}
}
}
GET /bosch/resource/foaf:Person/(^rdf:type/foaf:age)?avg
{
"avg": 33.5
}
GET /bosch/class/mr:Milkrun/*/(mr:vehicle_contains/^mr:transported_by/mr:contains_mat_pieces)?sum
{
"id-map": {
":m1": {
"sum": 1000
},
":m2": {
"sum": 111
},
":m3": {
"sum": 12
}
}
}
You can use the /batch
prefix to post a list of ids or values (entities) and process them in batch mode.
Every given id or value is treated as if you use the /resource/<entity>
URI. The /batch...
path postfix is treated similarly to /resource/<entity>...
.
Consequently this means that if /batch
is followed by some path expression, the first component will be a property (e.g., /batch/foaf:name
).
/resource/<entity>
on every given entityGET /bosch/batch
{
"ids": [":bert", ":peter", ":marry"]
}
{
"id-map": {
":bert": {
"id-map": {
"foaf:knows": { "ids": [":peter"] },
"foaf:drives": { "ids": [":m1"] },
"foaf:name": { "values": [{"value": "Bert Treb", "datatype": "rdf:langString", "language": "en"}]}
}
},
":peter": {
"id-map": {
"foaf:knows": { "ids": [":marry"] },
"foaf:drives": { "ids": [":m3"] },
"foaf:name": { "values": [{"value": "Peter Retep", "datatype": "rdf:langString", "language": "en"}]}
}
},
":marry": {
"id-map": {
"foaf:knows": { "ids": [":bert"] },
"foaf:name": { "values": [{"value": "Marry Yrram", "datatype": "rdf:langString", "language": "en"}]}
}
}
}
}
GET /bosch/batch/foaf:firstname
{
"ids": [":bert", ":peter", ":marry"]
}
{
"id-map": {
":bert": {
"values": [{ "value": "Bert", "datatype": "rdf:langString", "language": "en"}]
},
":peter": {
"values": [{ "value": "Peter", "datatype": "rdf:langString", "language": "en"}]
},
":marry": {
"values": [{ "value": "Marry", "datatype": "rdf:langString", "language": "en"}]
}
}
}
GET /bosch/batch/(^rdfs:label)
{
"values": [
{"value": "CX0023", "datatype": "xsd:string", "language": ""},
{"value": "EA8815", "datatype": "xsd:string", "language": ""}
]
}
{
"value-map": [
[
{"value": "CX0023", "datatype": "xsd:string", "language": ""},
{"ids": [":c23"]}
], [
{"value": "EA8815", "datatype": "xsd:string", "language": ""},
{"ids": [":c15"]}
]
]
}
For convenience /batch
is able to process the output of previous operations as input "as-is".
You can use the output of a GET
...
GET /bosch/class/foaf:Person
{
"ids": [":peter", ":marry", ":albert"]
}
... to POST
it to the batch processing.
GET /bosch/batch/(foaf:knows/foaf:knows)
{
"ids": [":berta", ":marry", ":albert"]
}
{
"id-map": {
":marry": { "ids": [":albert"]}
}
}
GET /bosch/class/foaf:Person/*/foaf:knows/*/foaf:name
{
"id-map": {
":bert": {
"id-map": {
":marry": {
"values": [{ "value": "Marry", "datatype": "rdf:langString", "language": "en"}]
}
// ...
}
},
":peter": {
"id-map": {
":marry": {
"values": [{ "value": "Marry", "datatype": "rdf:langString", "language": "en"}]
}
// ...
}
}
// ...
}
}
Two wildcards create a two-folded id-map
in this case.
For convenience, the post to /batch
also supports such mapping results as input.
In those cases the first layer of the map, so the keys of the outermost id-map
or value-map
will be used as batch-entities:
GET /bosch/batch/job:drives
{
"id-map": {
":bert": {
"id-map": {
":marry": {
"values": [{ "value": "Marry", "datatype": "rdf:langString", "language": "en"}]
}
}
},
":peter": {
"id-map": {
":marry": {
"values": [{ "value": "Marry", "datatype": "rdf:langString", "language": "en"}]
}
}
}
}
}
{
"id-map": {
":bert": {
"ids": [":m1"]
},
":peter": {
"ids": [":m2"]
}
// ...
}
}
Batch path expressions can be complex themselves. However, there is no attempt to match any levels of the maps together, as this would lead to inconsistent behavior and be quite confusing. Batch queries are independent of the previous query. If the convenience of posting the previous query's result is used, the batch query will behave as above: The given ids
/values
or the outermost map's keys will be used as entities. The /batch/...xyz...
query is then performed for each entity as if it was written like this: /resource/<entity>...xyz...
.
So with the GET results from the previous section you can use batch:
GET /bosch/batch/job:drives/*/rdfs:label
{
"id-map": {
":bert": {
"id-map": {
":marry": {
"values": [{ "value": "Marry", "datatype": "rdf:langString", "language": "en"}]
}
}
},
":peter": {
"id-map": {
":marry": {
"values": [{ "value": "Marry", "datatype": "rdf:langString", "language": "en"}]
}
}
}
}
}
{
"id-map": {
":bert": {
"id-map": {
":m1": {
"values": [{ "value": "Milkrun 1", "datatype": "rdf:langString", "language": "en"}]
}
// ...
}
},
":peter": {
"id-map": {
":m2": {
"values": [{ "value": "Milkrun 2", "datatype": "rdf:langString", "language": "en"}]
}
// ...
}
}
// ...
}
}
The next section covers advanced topics which require more knowledge about semantic web technologies.
Blank nodes are handled in the API like you would use normal URIs.
They are indicated with the underscore namespace _
.
GET /bosch/resource/:bert/foaf:address/_:bertsaddress
{
"id-map": {
"foaf:street": {
"values": [{"value": "bert-street", "datatype": "xsd:string", "language": ""}]
},
"foaf:streetnr": {
"values": [{"value": "42", "datatype": "xsd:string", "language": ""}]
}
}
}
PUT /bosch/resource/:peter/foaf:address
{
"ids": [ "_:petersaddress" ]
}
{}
DELETE /bosch/resource/:peter/foaf:address/_:petersaddress
{}
In this section we list additional features besides the core feature mentioned above.
This feature uses the /namespace
prefix.
To lists all mappings invoke:
GET /bosch/namespace
{
"":"urn:bosch_data:",
"date":"urn:bosch_vocabs:date#",
"stardog":"tag:stardog:api:",
"mr":"urn:bosch_vocabs:milkrun#",
"org":"urn:bosch_vocabs:org#",
"owl":"http://www.w3.org/2002/07/owl#",
"xsd":"http://www.w3.org/2001/XMLSchema#",
"weight":"urn:bosch_vocabs:weight#",
"rdfs":"http://www.w3.org/2000/01/rdf-schema#",
"rdf":"http://www.w3.org/1999/02/22-rdf-syntax-ns#",
"currency":"urn:bosch_vocabs:currency#",
"job":"urn:bosch_vocabs:job#",
"foaf":"http://xmlns.com/foaf/0.1/"
}
An additional list selects namespaces (comma-separated). Encoded URIs are allowed.
default
is the standard namespace "" (empty).
GET /bosch/namespace/http%3A%2F%2Fwww.w3.org%2F2002%2F07%2Fowl%23,default,foaf
{
"":"urn:bosch_data:",
"owl":"http://www.w3.org/2002/07/owl#",
"foaf":"http://xmlns.com/foaf/0.1/"
}
Mappings are given in body of the request to add them with POST.
POST /bosch/namespace
{
"new":"http://xmlns.com/new/2.0/",
"new2":"http://xmlns.com/new2/3.0/"
}
{ }
DELETE removes mappings but only prefixes are possible. Multiple removal with given list.
DELETE /bosch/namespace/new,new2
{ }