/*
* Copyright 2008 Fedora Commons, Inc.
*
* 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 org.mulgara.protocol.http;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.activation.MimeType;
import javax.activation.MimeTypeParseException;
import javax.mail.BodyPart;
import javax.mail.MessagingException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
import static javax.servlet.http.HttpServletResponse.SC_CREATED;
import org.apache.log4j.Logger;
import org.jrdf.graph.ObjectNode;
import org.jrdf.graph.URIReference;
import org.mulgara.connection.Connection;
import org.mulgara.parser.Interpreter;
import org.mulgara.protocol.StreamedAnswer;
import org.mulgara.query.Answer;
import org.mulgara.query.ConstructQuery;
import org.mulgara.query.Query;
import org.mulgara.query.QueryException;
import org.mulgara.query.TuplesException;
import org.mulgara.query.operation.Command;
import org.mulgara.query.operation.CreateGraph;
import org.mulgara.query.operation.Deletion;
import org.mulgara.query.operation.DropGraph;
import org.mulgara.query.operation.ExecuteScript;
import org.mulgara.query.operation.Insertion;
import org.mulgara.query.operation.Load;
import org.mulgara.query.operation.Rollback;
import org.mulgara.query.operation.ServerCommand;
import org.mulgara.query.operation.SetAutoCommit;
import org.mulgara.server.ServerInfo;
import org.mulgara.server.SessionFactoryProvider;
import org.mulgara.util.StringUtil;
import org.mulgara.util.functional.C;
import org.mulgara.util.functional.Fn1E;
import org.mulgara.util.functional.Fn2;
import org.mulgara.util.functional.Pair;
/**
* A query gateway for query languages.
*
* @created Sep 7, 2008
* @author Paula Gearon
* @copyright © 2008 <a href="http://www.fedora-commons.org/">Fedora Commons</a>
*/
public abstract class ProtocolServlet extends MulgaraServlet {
/** Generated serialization ID. */
private static final long serialVersionUID = -6510062000251611536L;
/** The logger. */
final static Logger logger = Logger.getLogger(ProtocolServlet.class.getName());
/**
* Internal type definition of a function that takes "something" and an output stream,
* and returns a {@link StreamedAnswer}
*/
protected interface StreamConstructor<T> extends Fn2<T,OutputStream,StreamedAnswer> { }
/**
* Internal type definition of a function that takes an Answer and an output stream,
* and returns a {@link StreamedAnswer}
*/
protected interface AnswerStreamConstructor extends StreamConstructor<Answer> { }
/**
* Internal type definition of a function that takes an Object and an output stream,
* and returns a {@link StreamedAnswer}
*/
protected interface ObjectStreamConstructor extends StreamConstructor<Object> { }
/**
* Identifies the HTTP PATCH method. Need to explicitly handle this as it is not yet in the API.
*/
private static final String METHOD_PATCH = "PATCH";
/** Local access to the name of the HTTP POST method. */
private static final String METHOD_POST = "POST";
/** The parameter identifying the query. */
private static final String QUERY_ARG = "query";
/** The parameter identifying the output type. */
protected static final String OUTPUT_ARG = "format";
/** The header name for accepted mime types. */
protected static final String ACCEPT_HEADER = "Accept";
/** The default output type to use. */
protected static final Output DEFAULT_OUTPUT_TYPE = Output.XML;
/** The default output type to use for queries that return graph results. */
protected static final Output DEFAULT_GRAPH_OUTPUT_TYPE = Output.RDFXML;
/** The parameter identifying the graph. */
protected static final String DEFAULT_GRAPH_ARG = "default-graph-uri";
/** The parameter identifying the graph. We don't set these in SPARQL yet. */
protected static final String NAMED_GRAPH_ARG = "named-graph-uri";
/** The content type of the results. */
protected static final String CONTENT_TYPE = "application/sparql-results+xml";
/** Session value for interpreter. */
protected static final String INTERPRETER = "session.interpreter";
/** Posted RDF data content type. */
protected static final String POSTED_DATA_TYPE = "multipart/form-data;";
/** The name of the posted data. */
protected static final String GRAPH_DATA = "graph";
/** The header used to indicate a statement count. */
protected static final String HDR_STMT_COUNT = "Statements-Loaded";
/** The header used to indicate a part that couldn't be loaded. */
protected static final String HDR_CANNOT_LOAD = "Cannot-Load";
/** A made-up scheme for data uploaded through http-put, since http means "downloaded". */
protected static final String HTTP_PUT_NS = "http-put://upload/";
/** The various parameter names used to identify a graph in a request */
private static final String[] GRAPH_PARAM_NAMES = { DEFAULT_GRAPH_ARG, GRAPH_DATA };
/** The various parameter names used to identify a subject in a request */
private static final String[] SUBJECT_PARAM_NAMES = { "subject", "subj", "s" };
/** The various parameter names used to identify a predicate in a request */
private static final String[] PREDICATE_PARAM_NAMES = { "predicate", "pred", "p" };
/** The various parameter names used to identify an object in a request */
private static final String[] OBJECT_PARAM_NAMES = { "object", "obj", "o" };
/** A query to get the entire contents of a graph */
protected static final String CONSTRUCT_ALL_QUERY = "CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }";
/** This object maps request types to the constructors for that output. */
protected final Map<Output,AnswerStreamConstructor> streamBuilders = new EnumMap<Output,AnswerStreamConstructor>(Output.class);
/** This object maps request types to the constructors for sending objects to that output. */
protected final Map<Output,ObjectStreamConstructor> objectStreamBuilders = new EnumMap<Output,ObjectStreamConstructor>(Output.class);
/**
* Creates the servlet for communicating with the given server.
* @param server The server that provides access to the database.
*/
public ProtocolServlet(SessionFactoryProvider server) throws IOException {
super(server);
initializeBuilders();
}
/**
* Creates the servlet for communicating with the given server. Default construction,
* meaning that the connectionFactory will be used for establishing a connection.
*/
public ProtocolServlet() throws IOException {
initializeBuilders();
}
/**
* Initialize the functional mappings of output types to the objects that manage them.
*/
abstract protected void initializeBuilders();
protected void service(HttpServletRequest req, HttpServletResponse resp) throws javax.servlet.ServletException, IOException {
if (METHOD_PATCH.equals(req.getMethod())) doPatch(req, resp);
else super.service(req, resp);
}
/**
* Respond to a request for the servlet.
* @see javax.servlet.http.HttpServlet#doGet(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
*/
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
try {
RestParams params = new RestParams(req);
doResponse(req, resp, params);
} catch (ServletException e) {
e.sendResponseTo(resp);
}
}
/**
* Respond to a request for the servlet. This may handle update queries.
* @see javax.servlet.http.HttpServlet#doPost(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
*/
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String type = req.getContentType();
try {
if (type != null && type.startsWith(POSTED_DATA_TYPE)) handleDataUpload(req, resp);
else handleUpdateQuery(req, resp);
} catch (ServletException e) {
e.sendResponseTo(resp);
}
}
/**
* Responds to requests to create graphs or triples.
*/
protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IOException {
try {
Pair<URI,LocalTriple> params = getModifyParams(req);
URI graph = params.first();
LocalTriple triple = params.second();
Connection conn = getConnection(req);
if (triple == null) createGraph(conn, graph);
else createTriple(conn, graph, triple);
resp.setStatus(SC_CREATED);
} catch (ServletException e) {
e.sendResponseTo(resp);
}
}
/**
* Responds to requests to create graphs or triples.
*/
protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws IOException {
try {
Pair<URI,LocalTriple> params = getModifyParams(req);
URI graph = params.first();
LocalTriple triple = params.second();
Connection conn = getConnection(req);
if (triple == null) deleteGraph(conn, graph);
else deleteTriple(conn, graph, triple);
resp.setStatus(SC_NO_CONTENT);
} catch (ServletException e) {
e.sendResponseTo(resp);
}
}
/**
* Responds to an HTTP PATCH request.
* This is a default implementation for the moment and will be overridden or replaced.
* @param req an {@link javax.servlet.http.HttpServletRequest} object that contains the request the client has made of the servlet
* @param resp an {@link javax.servlet.http.HttpServletResponse} object that contains the response the servlet sends to the client
* @throws ServletException if the request for a PATCH could not be handled.
* @throws IOException If an I/O error occurs while handling the request.
*/
protected void doPatch(HttpServletRequest req, HttpServletResponse resp) throws javax.servlet.ServletException, IOException {
String protocol = req.getProtocol();
String msg = "PATCH Method not supported";
if (protocol.endsWith("1.1")) {
resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg);
} else {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
}
}
/**
* Provide a description for the servlet.
* @see javax.servlet.GenericServlet#getServletInfo()
*/
public abstract String getServletInfo();
/**
* Respond to a parameterized request. The parameters have already been extracted.
* @param req The initial HTTP request.
* @param resp The response object to send data on.
* @param params The parameters already extracted from the request.
* @throws ServletException If there was an error in the servlet.
* @throws IOException If there is an error responding to the requesting client.
*/
protected void doResponse(HttpServletRequest req, HttpServletResponse resp, RestParams params) throws ServletException, IOException {
Answer result = null;
try {
RestParams.ResourceType type = params.getType();
// build a query based on either the resource being requested, or an explicit query
Query query = null;
if (type == RestParams.ResourceType.QUERY) {
query = getQuery(params.getQuery(), req);
} else if (type == RestParams.ResourceType.GRAPH) {
query = getQuery(CONSTRUCT_ALL_QUERY, req);
} else if (type == RestParams.ResourceType.STATEMENT) {
query = getQuery(createAskQuery(params.getTriple()), req);
} else {
throw new InternalErrorException("Unknown request type");
}
result = executeQuery(query, req);
Output outputType = getOutputType(req, query);
sendAnswer(result, outputType, resp);
} finally {
try {
if (result != null) result.close();
} catch (TuplesException e) {
logger.warn("Error closing: " + e.getMessage(), e);
}
}
}
/**
* Converts a SPARQL query string into a Query object. This uses extra parameters from the
* client where appropriate, such as the default graph.
* @param query The query string issued by the client.
* @param req The request from the client.
* @return A new Query object, built from the query string.
* @throws BadRequestException Due to an invalid command string.
*/
Query getQuery(String query, HttpServletRequest req) throws BadRequestException {
if (query == null) throw new BadRequestException("Query must be supplied");
try {
Interpreter interpreter = getInterpreter(req);
return interpreter.parseQuery(query);
} catch (Exception e) {
throw new BadRequestException(e.getMessage());
}
}
/**
* Converts a SPARQL query to a Command. For normal SPARQL this will be a Query,
* but SPARQL Update may create other command types.
* @param cmd The command string.
* @param req The client request object.
* @return The Command object specified by the cmd string.
* @throws BadRequestException Due to an invalid command string.
*/
List<Command> getCommand(String cmd, HttpServletRequest req) throws BadRequestException {
if (cmd == null) throw new BadRequestException("Command must be supplied");
try {
Interpreter interpreter = getInterpreter(req);
return interpreter.parseCommands(cmd);
} catch (Exception e) {
throw new BadRequestException(e.getMessage());
}
}
/**
* Execute a query on the database, and return the {@link Answer}.
* @param query The query to run.
* @param req The client request object.
* @return An Answer containing the results of the query.
* @throws ServletException Due to an error executing the query.
* @throws IOException If there was an error establishing a connection.
*/
Answer executeQuery(Query query, HttpServletRequest req) throws ServletException, IOException {
try {
return query.execute(getConnection(req));
} catch (IllegalStateException e) {
throw new ServiceUnavailableException(e.getMessage());
} catch (QueryException e) {
throw new InternalErrorException(e.getMessage());
} catch (TuplesException e) {
throw new InternalErrorException(e.getMessage());
}
}
/**
* Execute a query on the database, and return the {@link Answer}.
* @param query The query to run.
* @param req The client request object.
* @return An Answer containing the results of the query.
* @throws ServletException Due to an error executing the query.
* @throws IOException If there was an error establishing a connection.
*/
List<Answer> executeQuery(List<Query> queries, HttpServletRequest req) throws ServletException, IOException {
try {
final Connection connection = getConnection(req);
return C.map(queries,
new Fn1E<Query,Answer,Exception>() { public Answer fn(Query q) throws Exception { return q.execute(connection); } }
);
} catch (IllegalStateException e) {
throw new ServiceUnavailableException(e.getMessage());
} catch (QueryException e) {
throw new InternalErrorException(e.getMessage());
} catch (TuplesException e) {
throw new InternalErrorException(e.getMessage());
} catch (Exception e) {
throw new InternalErrorException("Unexpected error type: " + e.getMessage());
}
}
/**
* Execute a command on the database, and return whatever the result is.
* @param cmd The command to run.
* @param req The client request object.
* @return An Object containing the results of the query.
* @throws ServletException Due to an error executing the query.
* @throws IOException If there was an error establishing a connection.
*/
Object executeCommand(Command cmd, HttpServletRequest req) throws ServletException, IOException {
try {
return cmd.execute(getConnection(req));
} catch (IllegalStateException e) {
throw new ServiceUnavailableException(e.getMessage());
} catch (Exception e) {
throw new InternalErrorException(e.getMessage());
}
}
/**
* Sends an Answer back to a client, using the request protocol.
* @param answer The answer to send to the client.
* @param outputType The protocol requested by the client.
* @param resp The response object for communicating with the client.
* @throws IOException Due to a communications error with the client.
* @throws BadRequestException Due to a bad protocol type.
* @throws InternalErrorException Due to an error accessing the answer.
*/
void sendAnswer(Answer answer, Output outputType, HttpServletResponse resp) throws IOException, BadRequestException, InternalErrorException {
send(streamBuilders, answer, outputType, resp);
}
/**
* Writes information to the client stream. This is a general catch-all for non-answer
* information.
* @param result The data to return to the client.
* @param outputType The requested format for the response.
* @param resp The object for responding to a client.
* @throws IOException Due to an error communicating with the client.
* @throws BadRequestException Due to a bad protocol type.
* @throws InternalErrorException Due to an error accessing the result.
*/
void sendStatus(Object result, Output outputType, HttpServletResponse resp) throws IOException, BadRequestException, InternalErrorException {
send(objectStreamBuilders, result, outputType, resp);
}
/**
* Sends an result back to a client, using the requested protocol.
* @param <T> The type of the data that is to be streamed to the client.
* @param builders The map of protocol types to the objects that implement streaming for
* that protocol.
* @param data The result to send to the client.
* @param type The protocol type to use when talking to the client.
* @param resp The respons object for talking to the client.
* @throws IOException Due to a communications error with the client.
* @throws BadRequestException Due to a bad protocol type.
* @throws InternalErrorException Due to an error accessing the answer.
*/
<T> void send(Map<Output,? extends StreamConstructor<T>> builders, T data, Output type, HttpServletResponse resp) throws IOException, BadRequestException, InternalErrorException {
// establish the output type
if (type == null) type = DEFAULT_OUTPUT_TYPE;
resp.setContentType(type.mimeText);
resp.setHeader("pragma", "no-cache");
// get the constructor for the stream outputter
StreamConstructor<T> constructor = builders.get(type);
if (constructor == null) throw new BadRequestException("Unknown result type: " + type);
try {
OutputStream out = resp.getOutputStream();
constructor.fn(data, out).emit();
out.close();
} catch (IOException ioe) {
// There's no point in telling the client if we can't talk to the client
throw ioe;
} catch (Exception e) {
throw new InternalErrorException(e.getMessage());
}
}
/**
* Uploads data into a graph.
* @param req The object containing the client request to upload data.
* @param resp The object to respond to the client with.
* @throws IOException If an error occurs when communicating with the client.
*/
protected void handleDataUpload(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
try {
// parse in the data to be uploaded
MimeMultiNamedPart mime = new MimeMultiNamedPart(new ServletDataSource(req, GRAPH_DATA));
// validate the request
if (mime.getCount() == 0) throw new BadRequestException("Request claims to have posted data, but none was supplied.");
// Get the destination graph, and ensure it exists
URI destGraph = getRequestedDefaultGraph(req, mime);
Connection conn = getConnection(req);
try {
new CreateGraph(destGraph).execute(conn);
} catch (QueryException e) {
throw new InternalErrorException("Unable to create graph: " + e.getMessage());
}
// upload the data
int attempts = 0;
int failed = 0;
StringBuilder errorBuffer = new StringBuilder();
for (int partNr = 0; partNr < mime.getCount(); partNr++) {
BodyPart part = mime.getBodyPart(partNr);
String partName = mime.getPartName(partNr);
try {
if (!knownParam(partName)) {
attempts++;
resp.addHeader(HDR_STMT_COUNT, Long.toString(loadData(destGraph, part, conn)));
}
} catch (QueryException e) {
resp.addHeader(HDR_CANNOT_LOAD, partName);
errorBuffer.append("\n").append(partName).append(": ").append(e.getMessage());
failed++;
}
}
if (failed == attempts) {
throw new BadRequestException("Unable to load data from " + failed + " file" + (failed == 1 ? "" : "s") + errorBuffer);
}
if (failed > 0) throw new PartialFailureException("Failed to load " + failed + "/" + attempts + " files" + errorBuffer);
} catch (MessagingException e) {
throw new BadRequestException("Unable to process received MIME data: " + e.getMessage());
}
resp.setStatus(SC_NO_CONTENT);
}
/**
* Respond to a request for a query that may update the data.
* @param req The query request object.
* @param resp The HTTP response object.
* @throws IOException If an error occurs when communicating with the client.
* @throws ServletException
*/
protected void handleUpdateQuery(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
String queryStr = req.getParameter(QUERY_ARG);
// check to see if this was a large query that couldn't fit into a GET
if (queryStr == null) {
// a GET-style query that put the query into the body
doGet(req, resp);
return;
}
List<Command> cmds = getCommand(queryStr, req);
List<Pair<Answer,Output>> results = new ArrayList<Pair<Answer,Output>>();
// a flag to indicate that transactions will occur during this operation
boolean tx = cmds.size() > 1;
// locations in the list of operations for starting and ending transactions
int txOn = -1;
int txOff = -1;
if (tx) {
// look for the boundaries of write operations
for (int i = 0; i < cmds.size(); i++) {
Command c = cmds.get(i);
if (c instanceof ServerCommand || c instanceof ExecuteScript) {
if (txOn < 0) txOn = i;
txOff = i;
}
}
// if there is a single write operation, then no transaction is needed
if (txOn == txOff) {
txOn = -1;
txOff = -1;
}
// if there is no need to turn transaction on, then turn off the transaction flag
tx = txOn >= 0;
}
Object finalResult = null;
Output finalOutputType = null;
Object tmpResult = null;
Output tmpOutputType = null;
try {
try {
for (int i = 0; i < cmds.size(); i++) {
Command cmd = cmds.get(i);
// if a transaction is being created, we need to know about it
if (!tx && cmd instanceof SetAutoCommit) {
tx = !((SetAutoCommit)cmd).isOn();
}
// test if we need to start our own transaction wrapper here
if (i == txOn) {
executeCommand(new SetAutoCommit(false), req);
tx = true; // make sure this is set in case another operation reset it
}
tmpResult = executeCommand(cmd, req);
tmpOutputType = getOutputType(req, cmd);
// remember the last answer we see
if (tmpResult instanceof Answer) {
results.add(Pair.p((Answer)tmpResult, tmpOutputType));
finalResult = tmpResult;
finalOutputType = tmpOutputType;
}
// test if we need to end our own transaction wrapper here
if (i == txOff) {
executeCommand(new SetAutoCommit(true), req);
tx = false; // we can now avoid transactions since we've explicitly closed it
}
}
} catch (ServletException e) {
if (tx) executeCommand(new Rollback(), req);
throw e;
}
// if there is no result, then take the last one
if (finalResult == null) {
finalResult = tmpResult;
finalOutputType = tmpOutputType;
}
if (!results.isEmpty()) {
doResponseList(results, resp);
} else {
assert !(finalResult instanceof Answer);
sendStatus(finalResult, finalOutputType, resp);
}
} finally {
// always turn on autocommit since we can't leave a transaction running in HTTP
try {
if (tx) executeCommand(new SetAutoCommit(true), req);
} catch (Exception e) {
// throw away
logger.error("Unable to close transaction", e);
}
}
}
/**
* Sends back a response when a list has been requested. In the normal case,
* this is just the final response. All results are also closed.
* @param results The list of Answer/Output pairs.
* @param resp The response object to send the data on.
* @throws BadRequestException More than one type appeared in the request.
* @throws InternalErrorException There was a problem processing the request.
* @throws IOException There was an error responding to the client.
*/
protected void doResponseList(List<Pair<Answer,Output>> results, HttpServletResponse resp) throws BadRequestException, InternalErrorException, IOException {
Pair<Answer,Output> lastResult = C.last(results);
sendAnswer(lastResult.first(), lastResult.second(), resp);
// now clean it all up
for (Pair<Answer,Output> result: results) {
try {
result.first().close();
} catch (TuplesException e) {
logger.warn("Error closing resources: " + e.getMessage());
}
}
}
/**
* Load MIME data into a graph.
* @param graph The graph to load into.
* @param data The data to be loaded, with associated meta-data.
* @param cxt The connection to the database.
* @return The number of statements loaded.
* @throws IOException error reading from the client.
* @throws BadRequestException Bad data passed to the load request.
* @throws InternalErrorException A query exception occurred during the load operation.
*/
protected long loadData(URI graph, BodyPart data, Connection cxt) throws IOException, ServletException, QueryException {
String contentType = "";
InputStream dataStream = null;
try {
contentType = data.getContentType();
dataStream = data.getInputStream();
Load loadCmd = new Load(graph, dataStream, new MimeType(contentType), data.getFileName());
return (Long)loadCmd.execute(cxt);
} catch (MessagingException e) {
throw new BadRequestException("Unable to process data for loading: " + e.getMessage());
} catch (MimeTypeParseException e) {
throw new BadRequestException("Bad Content Type in request: " + contentType + " (" + e.getMessage() + ")");
} finally {
if (dataStream != null) dataStream.close();
}
}
/**
* Gets the SPARQL interpreter for the current session,
* creating it if it doesn't exist yet.
* @param req The current request environment.
* @return A connection that is tied to this HTTP session.
*/
abstract protected Interpreter getInterpreter(HttpServletRequest req) throws BadRequestException;
/**
* Gets the default graphs the user requested.
* @param req The request object from the user.
* @return A list of URIs for graphs. This may be null if no URIs were requested.
* @throws BadRequestException If a graph name was an invalid URI.
*/
protected List<URI> getRequestedDefaultGraphs(HttpServletRequest req) throws BadRequestException {
String[] defaults = req.getParameterValues(DEFAULT_GRAPH_ARG);
if (defaults == null) return null;
try {
return C.map(defaults, new Fn1E<String,URI,URISyntaxException>(){public URI fn(String s)throws URISyntaxException{return new URI(s);}});
} catch (URISyntaxException e) {
throw new BadRequestException("Invalid URI. " + e.getMessage());
}
}
/**
* Gets the default graphs the user requested.
* @param req The request object from the user.
* @return A list of URIs for graphs. This may be null if no URIs were requested.
* @throws BadRequestException If a graph name was an invalid URI.
*/
protected URI getRequestedDefaultGraph(HttpServletRequest req, MimeMultiNamedPart mime) throws ServletException {
// look in the parameters
String[] defaults = req.getParameterValues(DEFAULT_GRAPH_ARG);
if (defaults != null) {
if (defaults.length != 1) throw new BadRequestException("Multiple graphs requested.");
try {
return new URI(defaults[0]);
} catch (URISyntaxException e) {
throw new BadRequestException("Invalid URI. " + e.getInput());
}
}
// look in the mime data
if (mime != null) {
try {
String result = mime.getParameterString(DEFAULT_GRAPH_ARG);
if (result != null) {
try {
return new URI(result);
} catch (URISyntaxException e) {
throw new BadRequestException("Bad graph URI: " + result);
}
}
} catch (Exception e) {
throw new BadRequestException("Bad MIME data: " + e.getMessage());
}
}
return ServerInfo.getDefaultGraphURI();
}
/**
* Creates a new graph.
* @param conn A connection to the database.
* @param graph The graph to create.
* @throws ServletException If there was an error creating the graph.
*/
protected void createGraph(Connection conn, URI graph) throws ServletException {
try {
new CreateGraph(graph).execute(conn);
} catch (QueryException e) {
throw new InternalErrorException("Unable to create graph: " + e.getMessage());
}
}
/**
* Creates a triple in a graph.
* @param conn A connection to the database.
* @param graph The graph to create the triple in.
* @param triple The triple to create.
* @throws ServletException If there was an error creating the triple, or the graph does not exist.
*/
protected void createTriple(Connection conn, URI graph, LocalTriple triple) throws ServletException {
try {
new Insertion(graph, triple.toSet()).execute(conn);
} catch (QueryException e) {
throw new InternalErrorException("Unable to create triple: " + e.getMessage());
}
}
/**
* Deletes a graph.
* @param conn A connection to the database.
* @param graph The graph to delete.
* @throws ServletException If there was an error removing the graph.
*/
protected void deleteGraph(Connection conn, URI graph) throws ServletException {
try {
new DropGraph(graph).execute(conn);
} catch (QueryException e) {
throw new InternalErrorException("Unable to drop graph: " + e.getMessage());
}
}
/**
* Deletes a triple in a graph.
* @param conn A connection to the database.
* @param graph The graph to delete the triple from.
* @param triple The triple to delete.
* @throws ServletException If there was an error deleting the triple.
*/
protected void deleteTriple(Connection conn, URI graph, LocalTriple triple) throws ServletException {
try {
new Deletion(graph, triple.toSet()).execute(conn);
} catch (QueryException e) {
throw new InternalErrorException("Unable to delete triple: " + e.getMessage());
}
}
/**
* Creates a string query asking if a given triple exists.
* @param triple The triple to ask the existence of.
* @return A string containing an ASK query.
*/
String createAskQuery(LocalTriple triple) {
StringBuffer query = new StringBuffer("ASK ?s ?p ?o { ?s ?p ?o ");
query.append(". ?s <http://mulgara.org/is> <").append(triple.getSubject()).append(">");
query.append(". ?p <http://mulgara.org/is> <").append(triple.getPredicate()).append(">");
ObjectNode o = triple.getObject();
query.append(". ?o <http://mulgara.org/is> ");
if (o instanceof URIReference) query.append("<");
query.append(triple.getObject());
if (o instanceof URIReference) query.append(">");
query.append(" }");
return query.toString();
}
/**
* Compare a parameter name to a set of known parameter names.
* @param name The name to check.
* @return <code>true</code> if the name is known. <code>false</code> if not known or <code>null</code>.
*/
private boolean knownParam(String name) {
final String[] knownParams = new String[] { DEFAULT_GRAPH_ARG, NAMED_GRAPH_ARG, GRAPH_DATA };
for (String p: knownParams) if (p.equalsIgnoreCase(name)) return true;
return false;
}
/**
* Get the parameters of a request to modify the store.
* @param req The HTTP request.
* @return A Pair containing the graph for the modification, and an optional triple to be
* modified in the graph.
* @throws ServletException If the request was improperly formed.
*/
@SuppressWarnings("unchecked")
private Pair<URI,LocalTriple> getModifyParams(HttpServletRequest req) throws ServletException {
Map<String,String[]> params = req.getParameterMap();
String graphParam = getParam(GRAPH_PARAM_NAMES, params);
if (graphParam == null) throw new BadRequestException("No graph parameter defined.");
URI g;
try {
g = new URI(graphParam);
} catch (URISyntaxException e) {
throw new BadRequestException("Invalid graph name: " + graphParam);
}
String s = getParam(SUBJECT_PARAM_NAMES, params);
String p = getParam(PREDICATE_PARAM_NAMES, params);
String o = getParam(OBJECT_PARAM_NAMES, params);
if (s == null && p == null && o == null) return new Pair<URI,LocalTriple>(g, null);
if (s == null || p == null || o == null) throw new BadRequestException("Incomplete triple specified");
return new Pair<URI,LocalTriple>(g, new LocalTriple(s, p, o, true));
}
/**
* Get the value of a parameter from a map. The parameter is identified by one of the elements
* found in the <var>keyNames</var> parameter.
* @param keyNames An array of alternative names for the one parameter.
* @param params A map of parameter names to values.
* @return The value for the parameter, or <code>null</code> if not found.
* @throws ServletException If the parameter appear more than once.
*/
private static String getParam(String[] keyNames, Map<String,String[]> params) throws ServletException {
boolean found = false;
String result = null;
for (String k: keyNames) {
if (params.containsKey(k)) {
if (found) throw new BadRequestException("Duplicate parameter: " + k);
String[] values = params.get(k);
if (values.length == 0) throw new BadRequestException("Unsupplied parameter: " + k);
if (values.length > 1) throw new BadRequestException("Duplicate values for " + k + ": " + Arrays.asList(values));
result = values[0];
found = true;
}
}
return result;
}
/**
* Determine the type of response we need.
* @param req The request object for the servlet connection.
* @return xml, json, rdfXml or rdfN3.
*/
protected Output getOutputType(HttpServletRequest req, Command cmd) {
Output type = DEFAULT_OUTPUT_TYPE;
// get the accepted types
String accepted = req.getHeader(ACCEPT_HEADER);
if (accepted != null) {
// if this is a known type, then return it
Output t = Output.forMime(accepted);
if (t != null) type = t;
}
// check the URI parameters
String reqOutputName = req.getParameter(OUTPUT_ARG);
if (reqOutputName != null) {
try {
type = Output.valueOf(reqOutputName.toUpperCase());
} catch (IllegalArgumentException e) {
// no-op: ignore unknown enumeration values.
}
}
// need graph types if constructing a graph
if (cmd instanceof ConstructQuery) {
if (!type.isGraphType) type = DEFAULT_GRAPH_OUTPUT_TYPE;
} else {
if (!type.isBindingType) type = DEFAULT_OUTPUT_TYPE;
}
return type;
}
/**
* Enumeration of the various output types, depending on mime type.
*/
enum Output {
XML("application/sparql-results+xml", false, true),
JSON("application/sparql-results+json", true, true),
RDFXML("application/rdf+xml", true, false),
N3("text/rdf+n3", true, false);
final String mimeText;
final boolean isGraphType;
final boolean isBindingType;
private Output(String mimeText, boolean isGraphType, boolean isBindingType) {
this.mimeText = mimeText;
this.isGraphType = isGraphType;
this.isBindingType = isBindingType;
}
static private Map<String,Output> outputs = new HashMap<String,Output>();
static {
for (Output o: Output.values()) outputs.put(o.mimeText, o);
}
static Output forMime(String mimeText) { return outputs.get(mimeText); }
}
/**
* A structure containing the possible params for a read request.
*/
static class RestParams {
/** Character encoding to use. */
private static final String UTF8 = "UTF-8";
/** The default graph URIs for use in the SPARQL protocol. */
List<URI> defaultGraphUris = null;
/** The named graph URIs for use in the SPARQL protocol. */
List<URI> namedGraphUris = null;
/** The query or command, for use in the SPARQL protocol. */
String query = null;
/** A graph, for use in defining a resource. */
URI graph = null;
/** A triple, for use in defining a resource. */
LocalTriple triple = null;
/** The type of resource. */
final ResourceType type;
@SuppressWarnings("unchecked")
public RestParams(HttpServletRequest req) throws ServletException {
Map<String,String[]> params;
if (METHOD_POST.equals(req.getMethod())) {
params = bodyParamParse(req);
} else {
params = req.getParameterMap();
}
defaultGraphUris = getUriParamList(DEFAULT_GRAPH_ARG, params);
namedGraphUris = getUriParamList(NAMED_GRAPH_ARG, params);
graph = getUriParam(GRAPH_DATA, params);
// a single default-graph-uri is equivalent to a graph
if (graph == null && defaultGraphUris != null && defaultGraphUris.size() == 1) {
graph = defaultGraphUris.get(0);
}
// a graph is equivalent to a single default-graph-uri
if (graph != null && defaultGraphUris == null) {
defaultGraphUris = Collections.singletonList(graph);
}
query = getStringParam(QUERY_ARG, params);
String s = getParam(SUBJECT_PARAM_NAMES, params);
String p = getParam(PREDICATE_PARAM_NAMES, params);
String o = getParam(OBJECT_PARAM_NAMES, params);
if (s != null || p != null || o != null) triple = new LocalTriple(s, p, o, true);
type = testForType();
}
/**
* @return the default-graph-uri parameters in a list, or <code>null</code> if not set.
*/
public List<URI> getDefaultGraphUris() {
return defaultGraphUris;
}
/**
* @return the named-graph-uri parameters in a list, or <code>null</code> if not set.
*/
public List<URI> getNamedGraphUris() {
return namedGraphUris;
}
/**
* @return the query parameter, or <code>null</code> if not set.
*/
public String getQuery() {
return query;
}
/**
* @return the graph parameter, or <code>null</code> if not set.
*/
public URI getGraph() {
return graph;
}
/**
* @return the triple, or <code>null</code> if not set.
*/
public LocalTriple getTriple() {
return triple;
}
/**
* @return the type of resource represented by these parameters.
*/
public ResourceType getType() {
return type;
}
/**
* Test that all the parameters are as expected, and determine the resource type.
* @return The type of resource represented with these parameters.
* @throws ServletException If the combination of parameters is not valid for a resource.
*/
private ResourceType testForType() throws ServletException {
if (triple != null) {
// if a triple is defined, then a graph must be defined
if (graph == null) throw new BadRequestException("No graph parameter defined for triple.");
// no query allowed
if (query != null) throw new BadRequestException("Cannot define a statement resource with a query.");
return ResourceType.STATEMENT;
}
// If a query, then the only parameters we may not have are the triple,
// which has already been tested for.
if (query != null) return ResourceType.QUERY;
// So this is a graph resource
// May not have named graphs, or more than one default graph
if (defaultGraphUris != null && defaultGraphUris.size() != 1) {
throw new BadRequestException("Multiple graph resources not permitted.");
}
if (namedGraphUris != null && !namedGraphUris.isEmpty()) {
throw new BadRequestException("Named graphs not valid with a graph resource.");
}
return ResourceType.GRAPH;
}
/**
* Retrieves all parameters with a given name, and convert to a list of URIs
* @param paramName The name of the parameter to retrieve.
* @param params The full set of parameters to retrieve.
* @return A List of URIs, or <code>null</code> if there are no parameters for <var>paramName</var>.
* @throws ServletException If one of the parameter strings is not a valid URI.
*/
static List<URI> getUriParamList(String paramName, Map<String,String[]> params) throws ServletException {
String[] uris = params.get(paramName);
if (uris == null || uris.length == 0) return null;
// convert the default graph strings to URIs
// report an error if an invalid URI is found
return C.map(uris,
new Fn1E<String,URI,ServletException>() {
public URI fn(String u) throws ServletException {
try {
return new URI(u);
} catch (URISyntaxException e) {
throw new BadRequestException("Bad graph URI: " + e.getMessage());
}
}
}
);
}
/**
* Retrieves a parameter with a given name, and converts it into a URI.
* @param paramName The name of the parameter to retrieve.
* @param params The full set of parameters to retrieve.
* @return A URI, or <code>null</code> if there are no parameters for <var>paramName</var>.
* @throws ServletException If the parameter string is not a valid URI, or if there is more than one value.
*/
static URI getUriParam(String paramName, Map<String,String[]> params) throws ServletException {
try {
String p = getStringParam(paramName, params);
return p == null ? null : new URI(p);
} catch (URISyntaxException e) {
throw new BadRequestException("Bad graph URI: " + e.getMessage());
}
}
/**
* Retrieves a parameter with a given name.
* @param paramName The name of the parameter to retrieve.
* @param params The full set of parameters to retrieve.
* @return A string value, or <code>null</code> if there are no parameters for <var>paramName</var>.
* @throws ServletException If there is more than one value.
*/
static String getStringParam(String paramName, Map<String,String[]> params) throws ServletException {
String[] vals = params.get(paramName);
if (vals == null || vals.length == 0) return null;
if (vals.length > 1) throw new BadRequestException("More that one value for: " + paramName);
return vals[0];
}
/**
* Reads parameters out of the body of a query. This is only expected from a POST, not a GET.
* @param data The data from the body of the request.
* @return A map of parameters to an array of the strings containing the values.
* @throws ServletException If there was bad data in the body.
*/
private static Map<String,String[]> bodyParamParse(HttpServletRequest req) throws ServletException {
Map<String,List<String>> params = new HashMap<String,List<String>>();
try {
String bodyData = StringUtil.toString(req.getReader());
for (String entry: bodyData.split("&")) {
String[] parts = entry.split("=");
if (parts.length != 2) throw new BadRequestException("parameters can only have a single = parts");
List<String> values = params.get(parts[0]);
if (values == null) {
values = new ArrayList<String>();
params.put(parts[0], values);
}
values.add(URLDecoder.decode(parts[1], UTF8));
}
} catch (UnsupportedEncodingException e) {
throw new InternalErrorException("Bad data encoding in " + UTF8 + ": " + e.getMessage());
} catch (IOException e) {
throw new InternalErrorException("Unable to read request: " + e.getMessage());
}
Map<String,String[]> paramMap = new HashMap<String,String[]>();
for (Map.Entry<String,List<String>> p: params.entrySet()) {
List<String> vals = p.getValue();
paramMap.put(p.getKey(), vals.toArray(new String[vals.size()]));
}
return paramMap;
}
/**
* Resources being referenced by an HTTP method.
*/
enum ResourceType {
STATEMENT,
GRAPH,
QUERY
}
}
}