package de.unikiel.inf.comsys.neo4j.http;
/*
* #%L
* neo4j-sparql-extension
* %%
* Copyright (C) 2014 Niclas Hoyer
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program. If not, see
* <http://www.gnu.org/licenses/gpl-3.0.html>.
* #L%
*/
import de.unikiel.inf.comsys.neo4j.http.streams.ChunkedCommitHandler;
import de.unikiel.inf.comsys.neo4j.SPARQLExtensionProps;
import de.unikiel.inf.comsys.neo4j.http.streams.RDFStreamingOutput;
import de.unikiel.inf.comsys.neo4j.inference.QueryRewriterFactory;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Request;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.StreamingOutput;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.core.Variant;
import org.openrdf.model.Resource;
import org.openrdf.model.ValueFactory;
import org.openrdf.repository.RepositoryConnection;
import org.openrdf.repository.RepositoryException;
import org.openrdf.repository.sail.SailRepository;
import org.openrdf.repository.sail.SailRepositoryConnection;
import org.openrdf.rio.RDFFormat;
import org.openrdf.rio.RDFHandlerException;
import org.openrdf.rio.RDFParseException;
import org.openrdf.rio.RDFParser;
/**
* Implementation of the SPARQL 1.1 Graph Store HTTP Protocol.
*
* @see <a href="http://www.w3.org/TR/sparql11-http-rdf-update/">
* SPARQL 1.1 Graph Store HTTP Protocol
* </a>
*/
public class GraphStore extends AbstractSailResource {
private final ValueFactory vf;
private final long chunksize;
/**
* Create a new graph store management resource based on a repository.
*
* @param rep the repository this resources operates on
*/
public GraphStore(SailRepository rep) {
super(rep);
this.vf = rep.getValueFactory();
String chunksizeStr = SPARQLExtensionProps.getProperty("chunksize");
chunksize = Long.parseLong(chunksizeStr);
}
/**
* Indirect HTTP GET
*
* @see <a href="http://www.w3.org/TR/sparql11-http-rdf-update/#http-get">
* Section 5.2 "HTTP GET"
* </a>
* @param req JAX-RS {@link Request} object
* @param graphString the "graph" query parameter
* @param def the "default" query parameter
* @return the content of the request graph as HTTP response
*/
@GET
@Produces({
RDFMediaType.RDF_TURTLE,
RDFMediaType.RDF_XML,
RDFMediaType.RDF_NTRIPLES,
RDFMediaType.RDF_JSON
})
public Response graphIndirectGet(
@Context Request req,
@QueryParam("graph") String graphString,
@QueryParam("default") String def) {
return handleGet(req, def, graphString);
}
/**
* Indirect HTTP PUT
*
* @see <a href="http://www.w3.org/TR/sparql11-http-rdf-update/#http-put">
* Section 5.3 "HTTP PUT"
* </a>
* @param uriInfo JAX-RS {@link UriInfo} object
* @param type Content-Type HTTP header field
* @param graphString the "graph" query parameter
* @param def the "default" query parameter
* @param chunked the "chunked" query parameter
* @param in HTTP body as {@link InputStream}
* @return "204 No Content", if operation was successful
*/
@PUT
@Consumes({
RDFMediaType.RDF_TURTLE,
RDFMediaType.RDF_XML,
RDFMediaType.RDF_NTRIPLES,
RDFMediaType.RDF_XML
})
public Response graphIndirectPut(
@Context UriInfo uriInfo,
@HeaderParam("Content-Type") MediaType type,
@QueryParam("graph") String graphString,
@QueryParam("default") String def,
@QueryParam("chunked") String chunked,
InputStream in) {
return handleAdd(uriInfo, type, graphString, def, in, chunked, true);
}
/**
* Indirect HTTP DELETE
*
* @see <a
* href="http://www.w3.org/TR/sparql11-http-rdf-update/#http-delete">
* Section 5.4 "HTTP DELETE"
* </a>
* @param graphString the "graph" query parameter
* @param def the "default" query parameter
* @return "204 No Content", if operation was successful
*/
@DELETE
public Response graphIndirectDelete(
@QueryParam("graph") String graphString,
@QueryParam("default") String def) {
return handleClear(graphString);
}
/**
* Indirect HTTP POST
*
* @see <a href="http://www.w3.org/TR/sparql11-http-rdf-update/#http-post">
* Section 5.5 "HTTP POST"
* </a>
* @param uriInfo JAX-RS {@link UriInfo} object
* @param type Content-Type HTTP header field
* @param graphString the "graph" query parameter
* @param def the "default" query parameter
* @param chunked the "chunked" query parameter
* @param in HTTP body as {@link InputStream}
* @return "204 No Content", if operation was successful
*/
@POST
@Consumes({
RDFMediaType.RDF_TURTLE,
RDFMediaType.RDF_XML,
RDFMediaType.RDF_NTRIPLES,
RDFMediaType.RDF_XML
})
public Response graphIndirectPost(
@Context UriInfo uriInfo,
@HeaderParam("Content-Type") MediaType type,
@QueryParam("graph") String graphString,
@QueryParam("default") String def,
@QueryParam("chunked") String chunked,
InputStream in) {
return handleAdd(uriInfo, type, graphString, def, in, chunked, false);
}
/**
* Direct HTTP GET
*
* @see <a href="http://www.w3.org/TR/sparql11-http-rdf-update/#http-get">
* Section 5.2 "HTTP GET"
* </a>
* @param req JAX-RS {@link Request} object
* @param uriInfo JAX-RS {@link UriInfo} object
* @param graphString the "graph" query parameter
* @return the content of the request graph as HTTP response
*/
@GET
@Produces({
RDFMediaType.RDF_TURTLE,
RDFMediaType.RDF_XML,
RDFMediaType.RDF_NTRIPLES,
RDFMediaType.RDF_JSON
})
@Path("/{graph}")
public Response graphDirectGet(
@Context Request req,
@Context UriInfo uriInfo,
@PathParam("graph") String graphString) {
String graphuri = uriInfo.getAbsolutePath().toASCIIString();
return handleGet(req, null, graphuri);
}
/**
* Direct HTTP PUT
*
* @see <a href="http://www.w3.org/TR/sparql11-http-rdf-update/#http-put">
* Section 5.3 "HTTP PUT"
* </a>
* @param uriInfo JAX-RS {@link UriInfo} object
* @param type Content-Type HTTP header field
* @param chunked the "chunked" query parameter
* @param in HTTP body as {@link InputStream}
* @return "204 No Content", if operation was successful
*/
@PUT
@Consumes({
RDFMediaType.RDF_TURTLE,
RDFMediaType.RDF_XML,
RDFMediaType.RDF_NTRIPLES,
RDFMediaType.RDF_XML
})
@Path("/{graph}")
public Response graphDirectPut(
@Context UriInfo uriInfo,
@HeaderParam("Content-Type") MediaType type,
@QueryParam("chunked") String chunked,
InputStream in) {
String graphuri = uriInfo.getAbsolutePath().toASCIIString();
return handleAdd(uriInfo, type, graphuri, null, in, chunked, true);
}
/**
* Direct HTTP DELETE
*
* @see <a
* href="http://www.w3.org/TR/sparql11-http-rdf-update/#http-delete">
* Section 5.4 "HTTP DELETE"
* </a>
* @param uriInfo JAX-RS {@link UriInfo} object
* @return "204 No Content", if operation was successful
*/
@DELETE
@Path("/{graph}")
public Response graphDirectDelete(@Context UriInfo uriInfo) {
String graphuri = uriInfo.getAbsolutePath().toASCIIString();
return handleClear(graphuri);
}
/**
* Direct HTTP POST
*
* @see <a href="http://www.w3.org/TR/sparql11-http-rdf-update/#http-post">
* Section 5.5 "HTTP POST"
* </a>
* @param uriInfo JAX-RS {@link UriInfo} object
* @param type Content-Type HTTP header field
* @param chunked the "chunked" query parameter
* @param in HTTP body as {@link InputStream}
* @return "204 No Content", if operation was successful
*/
@POST
@Consumes({
RDFMediaType.RDF_TURTLE,
RDFMediaType.RDF_XML,
RDFMediaType.RDF_NTRIPLES,
RDFMediaType.RDF_XML
})
@Path("/{graph}")
public Response graphDirectPost(
@Context UriInfo uriInfo,
@HeaderParam("Content-Type") MediaType type,
@QueryParam("chunked") String chunked,
InputStream in) {
String graphuri = uriInfo.getAbsolutePath().toASCIIString();
return handleAdd(uriInfo, type, graphuri, null, in, chunked, false);
}
/**
* Adds RDF data to a graph in the repository.
*
* @param uriInfo JAX-RS {@link UriInfo} object
* @param type Content-Type HTTP header field
* @param graphString the "graph" query parameter
* @param def the "default" query parameter
* @param in RDF data
* @param chunkedStr the "chunked" query parameter
* @param clear true if the graph should be cleared before adding data
* @return "204 No Content", if operation was successful
*/
private Response handleAdd(
UriInfo uriInfo,
MediaType type,
String graphString,
String def,
InputStream in,
String chunkedStr,
boolean clear) {
SailRepositoryConnection conn;
try {
conn = getConnection();
} catch (RepositoryException ex) {
throw new WebApplicationException(ex);
}
try {
boolean chunked = chunkedStr != null && chunkedStr.equals("true");
Resource dctx = null;
// get the base URI by using direct or indirect graph reference
String base = uriInfo.getAbsolutePath().toASCIIString();
if (graphString != null) {
dctx = vf.createURI(graphString);
base = dctx.stringValue();
}
// check if a Content-Type header was set
if (type == null) {
return Response.status(Response.Status.BAD_REQUEST).build();
}
String typestr = type.getType() + "/" + type.getSubtype();
RDFFormat format = getRDFFormat(typestr);
// begin import
conn.begin();
if (dctx != null) {
if (clear) {
conn.clear(dctx);
}
addToGraphstore(conn, in, base, format, dctx, chunked);
} else {
if (clear) {
conn.clear();
}
addToGraphstore(conn, in, base, format, null, chunked);
}
conn.commit();
// check for modifications of TBox-graph and notify the query
// rewriting component
if (dctx != null) {
QueryRewriterFactory qr = QueryRewriterFactory.getInstance(rep);
if (dctx.stringValue().equals(qr.getOntologyContext())) {
qr.updateOntology(conn);
}
}
close(conn);
return Response.noContent().build();
} catch (RDFParseException ex) {
// rdf syntax error
String str = ex.getMessage();
close(conn, ex);
return Response.status(400).entity(
str.getBytes(Charset.forName("UTF-8"))).build();
} catch (IOException | RepositoryException | RDFHandlerException ex) {
// server error
close(conn, ex);
throw new WebApplicationException(ex);
}
}
/**
* Helper method for handleAdd.
*/
private void addToGraphstore(
RepositoryConnection conn,
InputStream in,
String base,
RDFFormat format,
Resource dctx,
boolean chunked) throws IOException, RDFParseException,
RDFHandlerException, RepositoryException {
if (chunked) {
RDFParser parser = getRDFParser(format);
parser.setRDFHandler(
new ChunkedCommitHandler(conn, chunksize, dctx));
parser.parse(in, base);
} else {
if (dctx != null) {
conn.add(in, base, format, dctx);
} else {
conn.add(in, base, format);
}
}
}
/**
* Deletes all data from a graph in the repository.
*
* @param graphString the graph to delete
* @return "204 No Content", if operation was successful
*/
private Response handleClear(String graphString) {
SailRepositoryConnection conn;
try {
conn = getConnection();
} catch (RepositoryException ex) {
throw new WebApplicationException(ex);
}
try {
conn.begin();
if (graphString != null) {
Resource ctx = vf.createURI(graphString);
conn.clear(ctx);
// check if TBox graph has been cleared and notify query
// rewriting component
QueryRewriterFactory qr = QueryRewriterFactory.getInstance(rep);
if (ctx.stringValue().equals(qr.getOntologyContext())) {
qr.updateOntology(conn);
}
} else {
conn.clear();
}
conn.commit();
close(conn);
} catch (RepositoryException ex) {
// server error
close(conn, ex);
throw new WebApplicationException(ex);
}
return Response.noContent().build();
}
/**
* Returns RDF data from a graph in the repository.
*
* @see RDFStreamingOutput
* @param req JAX-RS {@link Request} object
* @param def the "default" query parameter
* @param graphString the "graph" query parameter
* @return RDF data as HTTP response
*/
private Response handleGet(
Request req,
String def,
String graphString) {
// select matching MIME-Type for response based on HTTP headers
final Variant variant = req.selectVariant(rdfResultVariants);
final MediaType mt = variant.getMediaType();
final String mtstr = mt.getType() + "/" + mt.getSubtype();
final RDFFormat format = getRDFFormat(mtstr);
StreamingOutput stream;
RepositoryConnection conn = null;
try {
// return data as RDF stream
conn = getConnection();
if (graphString != null) {
Resource ctx = vf.createURI(graphString);
if (conn.size(ctx) == 0) {
return Response.status(Response.Status.NOT_FOUND).build();
}
stream = new RDFStreamingOutput(conn, format, ctx);
} else {
stream = new RDFStreamingOutput(conn, format);
}
} catch (RepositoryException ex) {
// server error
close(conn, ex);
throw new WebApplicationException(ex);
}
return Response.ok(stream).build();
}
}