QueryServlet.java
/*******************************************************************************
* Copyright (c) 2015 Eclipse RDF4J contributors, Aduna, and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Distribution License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*******************************************************************************/
package org.eclipse.rdf4j.workbench.commands;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.WeakHashMap;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.rdf4j.common.exception.RDF4JException;
import org.eclipse.rdf4j.common.iteration.Iterations;
import org.eclipse.rdf4j.model.IRI;
import org.eclipse.rdf4j.model.Literal;
import org.eclipse.rdf4j.model.Namespace;
import org.eclipse.rdf4j.model.base.CoreDatatype;
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
import org.eclipse.rdf4j.query.MalformedQueryException;
import org.eclipse.rdf4j.query.QueryLanguage;
import org.eclipse.rdf4j.query.QueryResultHandlerException;
import org.eclipse.rdf4j.query.resultio.QueryResultFormat;
import org.eclipse.rdf4j.query.resultio.QueryResultIO;
import org.eclipse.rdf4j.query.resultio.UnsupportedQueryResultFormatException;
import org.eclipse.rdf4j.repository.RepositoryConnection;
import org.eclipse.rdf4j.repository.RepositoryException;
import org.eclipse.rdf4j.repository.http.HTTPQueryEvaluationException;
import org.eclipse.rdf4j.repository.http.HTTPRepository;
import org.eclipse.rdf4j.rio.RDFFormat;
import org.eclipse.rdf4j.rio.Rio;
import org.eclipse.rdf4j.workbench.base.TransformationServlet;
import org.eclipse.rdf4j.workbench.exceptions.BadRequestException;
import org.eclipse.rdf4j.workbench.util.QueryEvaluator;
import org.eclipse.rdf4j.workbench.util.QueryStorage;
import org.eclipse.rdf4j.workbench.util.TupleResultBuilder;
import org.eclipse.rdf4j.workbench.util.WorkbenchRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
public class QueryServlet extends TransformationServlet {
protected static final String REF = "ref";
protected static final String LIMIT = "limit_query";
private static final String QUERY_LN = "queryLn";
private static final String INFER = "infer";
private static final String ACCEPT = "Accept";
protected static final String QUERY = "query";
private static final String[] EDIT_PARAMS = new String[] { QUERY_LN, QUERY, INFER, LIMIT };
private static final Logger LOGGER = LoggerFactory.getLogger(QueryServlet.class);
private static final QueryEvaluator EVAL = QueryEvaluator.INSTANCE;
private static final ObjectMapper mapper = new ObjectMapper();
private QueryStorage storage;
protected boolean writeQueryCookie;
// Poor Man's Cache: At the very least, garbage collection can clean up keys
// followed by values whenever the JVM faces memory pressure.
private static Map<String, String> queryCache = Collections.synchronizedMap(new WeakHashMap<>());
/**
* For testing purposes only.
*
* @param testQueryCache cache to use instead of the production cache instance
*/
protected static void substituteQueryCache(Map<String, String> testQueryCache) {
queryCache = testQueryCache;
}
protected void substituteQueryStorage(QueryStorage storage) {
this.storage = storage;
}
/**
* @return the names of the cookies that will be retrieved from the request, and returned in the response
*/
@Override
public String[] getCookieNames() {
String[] result;
if (writeQueryCookie) {
result = new String[] { QUERY, REF, LIMIT, QUERY_LN, INFER, "total_result_count", "show-datatypes" };
} else {
result = new String[] { REF, LIMIT, QUERY_LN, INFER, "total_result_count", "show-datatypes" };
}
return result;
}
/**
* Initialize this instance of the servlet.
*
* @param config configuration passed in by the application container
*/
@Override
public void init(final ServletConfig config) throws ServletException {
super.init(config);
try {
this.storage = QueryStorage.getSingletonInstance(this.appConfig);
} catch (RepositoryException e) {
throw new ServletException(e);
}
}
@Override
public void destroy() {
this.storage.shutdown();
super.destroy();
}
/**
* Long query strings could blow past the Tomcat default 8k HTTP header limit if stuffed into a cookie. In this
* case, we need to set a flag to avoid this happening before
* {@link TransformationServlet#service(HttpServletRequest, HttpServletResponse)} is called. A much lower limit on
* the size of the query text is used to stay well below the Tomcat limitation.
*/
@Override
public final void service(final HttpServletRequest req, final HttpServletResponse resp)
throws ServletException, IOException {
this.writeQueryCookie = shouldWriteQueryCookie(req.getParameter(QUERY));
super.service(req, resp);
}
/**
* <p>
* Determines if the servlet should write out the query text into a cookie as received, or write it's hash instead.
* </p>
* <p>
* Note: This is a separate method for testing purposes.
* </p>
*
* @param queryText the text received as the value for the parameter 'query'
*/
protected boolean shouldWriteQueryCookie(String queryText) {
return (null == queryText || queryText.length() <= 2048);
}
@Override
protected void service(final WorkbenchRequest req, final HttpServletResponse resp, final String xslPath)
throws IOException, RDF4JException, BadRequestException {
if (!writeQueryCookie) {
// If we suppressed putting the query text into the cookies before.
cookies.addCookie(req, resp, REF, "hash");
String queryValue = req.getParameter(QUERY);
String hash = String.valueOf(queryValue.hashCode());
queryCache.put(hash, queryValue);
cookies.addCookie(req, resp, QUERY, hash);
}
if ("get".equals(req.getParameter("action"))) {
ObjectNode jsonObject = mapper.createObjectNode();
jsonObject.put("queryText", getQueryText(req));
PrintWriter writer = new PrintWriter(new BufferedWriter(resp.getWriter()));
try {
writer.write(mapper.writeValueAsString(jsonObject));
} finally {
writer.flush();
}
} else {
handleStandardBrowserRequest(req, resp, xslPath);
}
}
private void handleStandardBrowserRequest(WorkbenchRequest req, HttpServletResponse resp, String xslPath)
throws IOException, RDF4JException, QueryResultHandlerException {
setContentType(req, resp);
OutputStream out = resp.getOutputStream();
try {
service(req, resp, out, xslPath);
} catch (BadRequestException | HTTPQueryEvaluationException exc) {
LOGGER.warn(exc.toString(), exc);
TupleResultBuilder builder = getTupleResultBuilder(req, resp, out);
builder.transform(xslPath, "query.xsl");
builder.start("error-message");
builder.link(Arrays.asList(INFO, "namespaces"));
builder.result(exc.getMessage());
builder.end();
} finally {
out.flush();
}
}
@Override
protected void doPost(final WorkbenchRequest req, final HttpServletResponse resp, final String xslPath)
throws IOException, BadRequestException, RDF4JException {
final String action = req.getParameter("action");
if ("save".equals(action)) {
saveQuery(req, resp);
} else if ("edit".equals(action)) {
if (canReadSavedQuery(req)) {
/*
* only need read access for edit action, since we are only reading the saved query text to present it
* in the editor
*/
final TupleResultBuilder builder = getTupleResultBuilder(req, resp, resp.getOutputStream());
builder.transform(xslPath, "query.xsl");
builder.start(EDIT_PARAMS);
builder.link(Arrays.asList(INFO, "namespaces"));
final String queryLn = req.getParameter(EDIT_PARAMS[0]);
final String query = getQueryText(req);
final Boolean infer = Boolean.valueOf(req.getParameter(EDIT_PARAMS[2]));
final Literal limit = SimpleValueFactory.getInstance()
.createLiteral(req.getParameter(EDIT_PARAMS[3]), CoreDatatype.XSD.INTEGER);
builder.result(queryLn, query, infer, limit);
builder.end();
} else {
throw new BadRequestException("Current user may not read the given query.");
}
} else if ("exec".equals(action)) {
if (canReadSavedQuery(req)) {
service(req, resp, xslPath);
} else {
throw new BadRequestException("Current user may not read the given query.");
}
} else {
throw new BadRequestException("POST with unexpected action parameter value: " + action);
}
}
private void saveQuery(final WorkbenchRequest req, final HttpServletResponse resp)
throws IOException, BadRequestException, RDF4JException {
resp.setContentType("application/json");
ObjectNode jsonObject = mapper.createObjectNode();
jsonObject.put("queryText", getQueryText(req));
final HTTPRepository http = (HTTPRepository) repository;
final boolean accessible = storage.checkAccess(http);
jsonObject.put("accessible", accessible);
if (accessible) {
final String queryName = req.getParameter("query-name");
String userName = getUserNameFromParameter(req, SERVER_USER);
final boolean existed = storage.askExists(http, queryName, userName);
jsonObject.put("existed", existed);
final boolean written = Boolean.valueOf(req.getParameter("overwrite")) || !existed;
if (written) {
final boolean shared = !Boolean.valueOf(req.getParameter("save-private"));
final QueryLanguage queryLanguage = QueryLanguage.valueOf(req.getParameter(QUERY_LN));
final String queryText = req.getParameter(QUERY);
final boolean infer = req.isParameterPresent(INFER) ? Boolean.valueOf(req.getParameter(INFER)) : false;
final int rowsPerPage = Integer.valueOf(req.getParameter(LIMIT));
if (existed) {
final IRI query = storage.selectSavedQuery(http, userName, queryName);
storage.updateQuery(query, userName, shared, queryLanguage, queryText, infer, rowsPerPage);
} else {
storage.saveQuery(http, queryName, userName, shared, queryLanguage, queryText, infer, rowsPerPage);
}
}
jsonObject.put("written", written);
}
final PrintWriter writer = new PrintWriter(new BufferedWriter(resp.getWriter()));
writer.write(mapper.writeValueAsString(jsonObject));
writer.flush();
}
private String getUserNameFromParameter(WorkbenchRequest req, String parameter) {
String userName = req.getParameter(parameter);
if (null == userName) {
userName = "";
}
return userName;
}
private void setContentType(final WorkbenchRequest req, final HttpServletResponse resp) {
String result = "application/xml";
String ext = "xml";
if (req.isParameterPresent(ACCEPT)) {
final String accept = req.getParameter(ACCEPT);
final Optional<RDFFormat> format = Rio.getWriterFormatForMIMEType(accept);
if (format.isPresent()) {
result = format.get().getDefaultMIMEType();
ext = format.get().getDefaultFileExtension();
} else {
final Optional<QueryResultFormat> tupleFormat = QueryResultIO.getWriterFormatForMIMEType(accept);
if (tupleFormat.isPresent()) {
result = tupleFormat.get().getDefaultMIMEType();
ext = tupleFormat.get().getDefaultFileExtension();
} else {
final Optional<QueryResultFormat> booleanFormat = QueryResultIO
.getBooleanWriterFormatForMIMEType(accept);
if (booleanFormat.isPresent()) {
result = booleanFormat.get().getDefaultMIMEType();
ext = booleanFormat.get().getDefaultFileExtension();
}
}
}
}
resp.setContentType(result);
if (!result.equals("application/xml")) {
final String attachment = "attachment; filename=query." + ext;
resp.setHeader("Content-disposition", attachment);
}
}
private void service(final WorkbenchRequest req, final HttpServletResponse resp, final OutputStream out,
final String xslPath)
throws BadRequestException, RDF4JException, UnsupportedQueryResultFormatException, IOException {
try (RepositoryConnection con = repository.getConnection()) {
con.setParserConfig(NON_VERIFYING_PARSER_CONFIG);
final TupleResultBuilder builder = getTupleResultBuilder(req, resp, resp.getOutputStream());
for (Namespace ns : Iterations.asList(con.getNamespaces())) {
builder.prefix(ns.getPrefix(), ns.getName());
}
String query = getQueryText(req);
if (query.isEmpty()) {
builder.transform(xslPath, "query.xsl");
builder.start();
builder.link(Arrays.asList(INFO, "namespaces"));
builder.end();
} else {
try {
EVAL.extractQueryAndEvaluate(builder, resp, out, xslPath, con, query, req, this.cookies);
} catch (MalformedQueryException exc) {
throw new BadRequestException(exc.getMessage(), exc);
} catch (HTTPQueryEvaluationException exc) {
if (exc.getCause() instanceof MalformedQueryException) {
throw new BadRequestException(exc.getCause().getMessage(), exc);
}
throw exc;
}
}
}
}
/**
* @param req for looking at the request parameters
* @return the query text, if it can somehow be retrieved from request parameters, otherwise an empty string
* @throws BadRequestException if a problem occurs grabbing the request from storage
* @throws RDF4JException if a problem occurs grabbing the request from storage
*/
protected String getQueryText(WorkbenchRequest req) throws BadRequestException, RDF4JException {
String result;
if (req.isParameterPresent(QUERY)) {
String query = req.getParameter(QUERY);
if (req.isParameterPresent(REF)) {
String ref = req.getParameter(REF);
if ("text".equals(ref)) {
result = query;
} else if ("hash".equals(ref)) {
result = queryCache.get(query);
if (null == result) {
result = "";
}
} else if ("id".equals(ref)) {
result = storage.getQueryText((HTTPRepository) repository, getUserNameFromParameter(req, "owner"),
query);
} else {
// if ref not recognized assume request meant "text"
result = query;
}
} else {
result = query;
}
} else {
result = "";
}
return result;
}
private boolean canReadSavedQuery(WorkbenchRequest req) throws BadRequestException, RDF4JException {
if (req.isParameterPresent(REF)) {
return "id".equals(req.getParameter(REF))
? storage.canRead(
storage.selectSavedQuery((HTTPRepository) repository,
getUserNameFromParameter(req, "owner"), req.getParameter(QUERY)),
getUserNameFromParameter(req, SERVER_USER))
: true;
} else {
throw new BadRequestException("Expected 'ref' parameter in request.");
}
}
}