
com.metreeca.rdf4j.handlers.Graphs Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of metreeca-rdf4j Show documentation
Show all versions of metreeca-rdf4j Show documentation
Connector kit for RDF4J-based SPARQL repositories.
The newest version!
/*
* Copyright © 2013-2022 Metreeca srl
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.metreeca.rdf4j.handlers;
import com.metreeca.json.Shape;
import com.metreeca.json.Values;
import com.metreeca.rdf4j.services.Graph;
import com.metreeca.rest.*;
import com.metreeca.rest.formats.JSONLDFormat;
import org.eclipse.rdf4j.model.*;
import org.eclipse.rdf4j.model.vocabulary.RDF;
import org.eclipse.rdf4j.model.vocabulary.VOID;
import org.eclipse.rdf4j.repository.*;
import org.eclipse.rdf4j.rio.*;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.util.function.Supplier;
import static com.metreeca.json.Shape.exactly;
import static com.metreeca.json.Values.iri;
import static com.metreeca.json.Values.statement;
import static com.metreeca.json.shapes.Field.field;
import static com.metreeca.rdf.formats.RDFFormat.rdf;
import static com.metreeca.rest.Format.mimes;
import static com.metreeca.rest.MessageException.status;
import static com.metreeca.rest.Response.BadRequest;
import static com.metreeca.rest.Response.InternalServerError;
import static com.metreeca.rest.Xtream.task;
import static com.metreeca.rest.formats.InputFormat.input;
import static com.metreeca.rest.formats.OutputFormat.output;
import static com.metreeca.rest.handlers.Router.router;
import static java.lang.String.format;
/**
* SPARQL 1.1 Graph Store endpoint handler.
*
* Provides a standard SPARQL 1.1 Graph Store endpoint exposing the contents of the shared {@linkplain Graph
* graph}.
*
* Both {@linkplain #query(Collection) query} and {@linkplain #update(Collection) update} operations are disabled,
* unless otherwise specified.
*
* @see SPARQL 1.1 Graph Store HTTP Protocol
*/
public final class Graphs extends Endpoint {
private static final Shape GraphsShape=field(RDF.VALUE,
field(RDF.TYPE), exactly(VOID.DATASET)
);
/**
* Creates a graph store endpoint
*
* @return a new graph store endpoint
*/
public static Graphs graphs() {
return new Graphs();
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
private Graphs() {
delegate(router()
.get(this::get)
.put(this::put)
.delete(this::delete)
.post(this::post)
);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/*
* https://www.w3.org/TR/sparql11-http-rdf-update/#http-get
*/
private Future get(final Request request) {
return consumer -> {
final boolean catalog=request.parameters().isEmpty();
final String target=graph(request);
final String accept=request.header("Accept").orElse("");
if ( target == null && !catalog ) {
request.reply(status(BadRequest, "missing target graph parameter")).accept(consumer);
} else if ( !queryable(request.roles()) ) {
request.reply(response -> response.status(Response.Unauthorized)).accept(consumer);
} else if ( catalog ) { // graph catalog
final IRI focus=iri(request.item());
final Collection model=new ArrayList<>();
graph().query(task(connection -> {
try ( final RepositoryResult contexts=connection.getContextIDs() ) {
while ( contexts.hasNext() ) {
final Resource context=contexts.next();
model.add(statement(focus, RDF.VALUE, context));
model.add(statement(context, RDF.TYPE, VOID.DATASET));
}
}
}));
request.reply(response -> response.status(Response.OK)
.set(JSONLDFormat.shape(), GraphsShape)
.body(rdf(), model)
).accept(consumer);
} else {
final RDFWriterFactory factory=com.metreeca.rdf.formats.RDFFormat.service(
RDFWriterRegistry.getInstance(), RDFFormat.TURTLE, mimes(accept)
);
final RDFFormat format=factory.getRDFFormat();
final Resource context=target.isEmpty() ? null : iri(target);
graph().query(task(connection -> {
request.reply(response -> response.status(Response.OK)
.header("Content-Type", format.getDefaultMIMEType())
.header("Content-Disposition", format("attachment; filename=\"%s.%s\"",
target.isEmpty() ? "default" : target, format.getDefaultFileExtension()
))
.body(output(), output -> { connection.export(factory.getWriter(output), context); })
).accept(consumer);
}));
}
};
}
/*
* https://www.w3.org/TR/sparql11-http-rdf-update/#http-put
*/
private Future put(final Request request) {
return consumer -> {
final String target=graph(request);
if ( target == null ) {
request.reply(status(BadRequest, "missing target graph parameter")).accept(consumer);
} else if ( !updatable(request.roles()) ) {
request.reply(response -> response.status(Response.Unauthorized)).accept(consumer);
} else {
final Resource context=target.isEmpty() ? null : iri(target);
final String content=request.header("Content-Type").orElse("");
// !!! If a clients issues a POST or PUT with a content type that is not understood by the
// !!! graph store, the implementation MUST respond with 415 Unsupported Media Type.
final RDFParserFactory factory=com.metreeca.rdf.formats.RDFFormat.service(
RDFParserRegistry.getInstance(), RDFFormat.TURTLE, mimes(content) // !!! review fallback
// handling
);
graph().update(task(connection -> { // binary format >> no rewriting
try ( final InputStream input=request.body(input()).fold(e -> Xtream.input(), Supplier::get) ) {
final boolean exists=exists(connection, context);
connection.clear(context);
connection.add(input, request.base(), factory.getRDFFormat(), context);
request.reply(response ->
response.status(exists ? Response.NoContent :
Response.Created)
).accept(consumer);
} catch ( final IOException e ) {
logger().warning(this, "unable to read RDF payload", e);
request.reply(status(InternalServerError, e)).accept(consumer);
} catch ( final RDFParseException e ) {
logger().warning(this, "malformed RDF payload", e);
request.reply(status(BadRequest, e)).accept(consumer);
} catch ( final RepositoryException e ) {
logger().warning(this, "unable to update graph "+context, e);
request.reply(status(InternalServerError, e)).accept(consumer);
}
}));
}
};
}
/*
* https://www.w3.org/TR/sparql11-http-rdf-update/#http-delete
*/
private Future delete(final Request request) {
return consumer -> {
final String target=graph(request);
if ( target == null ) {
request.reply(status(BadRequest, "missing target graph parameter")).accept(consumer);
} else if ( !updatable(request.roles()) ) {
request.reply(response -> response.status(Response.Unauthorized)).accept(consumer);
} else {
final Resource context=target.isEmpty() ? null : iri(target);
graph().update(task(connection -> {
try {
final boolean exists=exists(connection, context);
connection.clear(context);
request.reply(response ->
response.status(exists ? Response.NoContent :
Response.NotFound)
).accept(consumer);
} catch ( final RepositoryException e ) {
logger().warning(this, "unable to update graph "+context, e);
request.reply(status(InternalServerError, e)).accept(consumer);
}
}));
}
};
}
/*
* https://www.w3.org/TR/sparql11-http-rdf-update/#http-post
*/
private Future post(final Request request) {
return consumer -> {
// !!! support "multipart/form-data"
// !!! support graph creation with IRI identifying the underlying Graph Store
final String target=graph(request);
if ( target == null ) {
request.reply(status(BadRequest, "missing target graph parameter")).accept(consumer);
} else if ( !updatable(request.roles()) ) {
request.reply(response -> response.status(Response.Unauthorized)).accept(consumer);
} else {
final Resource context=target.isEmpty() ? null : iri(target);
final String content=request.header("Content-Type").orElse("");
// !!! If a clients issues a POST or PUT with a content type that is not understood by the
// !!! graph store, the implementation MUST respond with 415 Unsupported Media Type.
final RDFParserFactory factory=com.metreeca.rdf.formats.RDFFormat.service(
RDFParserRegistry.getInstance(), RDFFormat.TURTLE, mimes(content) // !!! review fallback
);
graph().update(task(connection -> { // binary format >> no rewriting
try ( final InputStream input=request.body(input()).fold(e -> Xtream.input(),
Supplier::get) ) {
final boolean exists=exists(connection, context);
connection.add(input, request.base(), factory.getRDFFormat(), context);
request.reply(response ->
response.status(exists ? Response.NoContent : Response.Created)
).accept(consumer);
} catch ( final IOException e ) {
logger().warning(this, "unable to read RDF payload", e);
request.reply(status(InternalServerError, e)).accept(consumer);
} catch ( final RDFParseException e ) {
logger().warning(this, "malformed RDF payload", e);
request.reply(status(BadRequest, e)).accept(consumer);
} catch ( final RepositoryException e ) {
logger().warning(this, "unable to update graph "+context, e);
request.reply(status(InternalServerError, e)).accept(consumer);
}
}));
}
};
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
private String graph(final Request request) {
final List defaults=request.parameters("default");
final List nameds=request.parameters("graph");
final boolean dflt=defaults.size() == 1 && defaults.get(0).isEmpty();
final boolean named=nameds.size() == 1 && Values.AbsoluteIRIPattern.matcher(nameds.get(0)).matches();
return dflt && named ? null : dflt ? "" : named ? nameds.get(0) : null;
}
private boolean exists(final RepositoryConnection connection, final Resource context) {
try ( final RepositoryResult contexts=connection.getContextIDs() ) {
while ( contexts.hasNext() ) {
if ( contexts.next().equals(context) ) { return true; }
}
}
return connection.hasStatement(null, null, null, true, context);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy