diff --git a/core/http/client/src/main/java/org/eclipse/rdf4j/http/client/RDF4JProtocolSession.java b/core/http/client/src/main/java/org/eclipse/rdf4j/http/client/RDF4JProtocolSession.java index 158216ba4c2..07a4fbe7dec 100644 --- a/core/http/client/src/main/java/org/eclipse/rdf4j/http/client/RDF4JProtocolSession.java +++ b/core/http/client/src/main/java/org/eclipse/rdf4j/http/client/RDF4JProtocolSession.java @@ -20,6 +20,7 @@ import java.io.OutputStreamWriter; import java.io.Reader; import java.net.HttpURLConnection; +import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; @@ -121,6 +122,8 @@ public class RDF4JProtocolSession extends SPARQLProtocolSession { private long pingDelay = PINGDELAY; + private int serverProtocolVersion = 0; + /** * @deprecated Use {@link #RDF4JProtocolSession(HttpClient, ExecutorService)} instead */ @@ -165,6 +168,7 @@ public void setServerURL(String serverURL) { } this.serverURL = serverURL; + this.serverProtocolVersion = 0; // side effect } public String getServerURL() { @@ -275,6 +279,16 @@ public String getServerProtocol() throws IOException, RepositoryException, Unaut } } + private int getServerProtocolVersion() throws UnauthorizedException, RepositoryException, IOException { + if (serverProtocolVersion > 0) { + return serverProtocolVersion; + } + + var protocolVersionString = getServerProtocol(); + serverProtocolVersion = Integer.parseInt(protocolVersionString); + return serverProtocolVersion; + } + /*-------------------------* * Repository/context size * *-------------------------*/ @@ -286,9 +300,8 @@ public long size(Resource... contexts) throws IOException, RepositoryException, String transactionURL = getTransactionURL(); final boolean useTransaction = transactionURL != null; - String baseLocation = useTransaction ? appendAction(transactionURL, Action.SIZE) - : Protocol.getSizeLocation(getQueryURL()); - URIBuilder url = new URIBuilder(baseLocation); + URIBuilder url = useTransaction ? getTxnActionURIBuilder(Action.SIZE) + : new URIBuilder(Protocol.getSizeLocation(getQueryURL())); String[] encodedContexts = Protocol.encodeContexts(contexts); for (int i = 0; i < encodedContexts.length; i++) { @@ -591,8 +604,8 @@ public void getStatements(Resource subj, IRI pred, Value obj, boolean includeInf String transactionURL = getTransactionURL(); final boolean useTransaction = transactionURL != null; - String baseLocation = useTransaction ? transactionURL : Protocol.getStatementsLocation(getQueryURL()); - URIBuilder url = new URIBuilder(baseLocation); + URIBuilder url = useTransaction ? getTxnActionURIBuilder(Action.GET) + : new URIBuilder(Protocol.getStatementsLocation(getQueryURL())); if (subj != null) { url.setParameter(Protocol.SUBJECT_PARAM_NAME, Protocol.encodeValue(subj)); @@ -607,9 +620,6 @@ public void getStatements(Resource subj, IRI pred, Value obj, boolean includeInf url.addParameter(Protocol.CONTEXT_PARAM_NAME, encodedContext); } url.setParameter(Protocol.INCLUDE_INFERRED_PARAM_NAME, Boolean.toString(includeInferred)); - if (useTransaction) { - url.setParameter(Protocol.ACTION_PARAM_NAME, Action.GET.toString()); - } HttpRequestBase method = useTransaction ? new HttpPut(url.build()) : new HttpGet(url.build()); method = applyAdditionalHeaders(method); @@ -683,6 +693,24 @@ public synchronized void beginTransaction(TransactionSetting... transactionSetti } } + private URIBuilder getTxnActionURIBuilder(Action action) + throws RDF4JException, IOException, UnauthorizedException { + Objects.requireNonNull(action); + try { + if (getServerProtocolVersion() < 14) { // use legacy action parameter instead of dedicated action endpoint + URIBuilder builder = new URIBuilder(transactionURL); + builder.addParameter(Protocol.ACTION_PARAM_NAME, action.toString()); + return builder; + } + + URIBuilder builder = new URIBuilder(transactionURL + "/" + action.toString().toLowerCase()); + return builder; + } catch (URISyntaxException e) { + logger.error("could not create URL for transaction " + action, e); + throw new RuntimeException(e); + } + } + public synchronized void prepareTransaction() throws RDF4JException, IOException, UnauthorizedException { checkRepositoryURL(); @@ -692,9 +720,8 @@ public synchronized void prepareTransaction() throws RDF4JException, IOException HttpPut method = null; try { - URIBuilder url = new URIBuilder(transactionURL); - url.addParameter(Protocol.ACTION_PARAM_NAME, Action.PREPARE.toString()); - method = applyAdditionalHeaders(new HttpPut(url.build())); + var uriBuilder = getTxnActionURIBuilder(Action.PREPARE); + method = applyAdditionalHeaders(new HttpPut(uriBuilder.build())); final HttpResponse response = execute(method); try { @@ -725,9 +752,8 @@ public synchronized void commitTransaction() throws RDF4JException, IOException, HttpPut method = null; try { - URIBuilder url = new URIBuilder(transactionURL); - url.addParameter(Protocol.ACTION_PARAM_NAME, Action.COMMIT.toString()); - method = applyAdditionalHeaders(new HttpPut(url.build())); + var uriBuilder = getTxnActionURIBuilder(Action.COMMIT); + method = applyAdditionalHeaders(new HttpPut(uriBuilder.build())); final HttpResponse response = execute(method); try { @@ -804,11 +830,10 @@ void executeTransactionPing() { if (transactionURL == null) { return; // transaction has already been closed } - HttpPost method; try { - URIBuilder url = new URIBuilder(transactionURL); - url.addParameter(Protocol.ACTION_PARAM_NAME, Action.PING.toString()); - method = applyAdditionalHeaders(new HttpPost(url.build())); + var uriBuilder = getTxnActionURIBuilder(Action.PING); + var method = applyAdditionalHeaders(new HttpPost(uriBuilder.build())); + String text = EntityUtils.toString(executeOK(method).getEntity()); long timeout = Long.parseLong(text); // clients should ping before server timeouts transaction @@ -824,16 +849,16 @@ void executeTransactionPing() { pingTransaction(); // reschedule } - /** - * Appends the action as a parameter to the supplied url - * - * @param url a url on which to append the parameter. it is assumed the url has no parameters. - * @param action the action to add as a parameter - * @return the url parametrized with the supplied action - */ - private String appendAction(String url, Action action) { - return url + "?" + Protocol.ACTION_PARAM_NAME + "=" + action.toString(); - } +// /** +// * Appends the action as a parameter to the supplied url +// * +// * @param url a url on which to append the parameter. it is assumed the url has no parameters. +// * @param action the action to add as a parameter +// * @return the url parametrized with the supplied action +// */ +// private String appendAction(String url, Action action) { +// return url + "?" + Protocol.ACTION_PARAM_NAME + "=" + action.toString(); +// } /** * Sends a transaction list as serialized XML to the server. @@ -938,9 +963,16 @@ protected HttpUriRequest getQueryMethod(QueryLanguage ql, String query, String b RequestBuilder builder; String transactionURL = getTransactionURL(); if (transactionURL != null) { - builder = RequestBuilder.put(transactionURL); + URI requestURI; + try { + requestURI = getTxnActionURIBuilder(Action.QUERY).build(); + } catch (URISyntaxException | IOException e) { + logger.error("could not create URL for transaction query", e); + throw new RuntimeException(e); + } + + builder = RequestBuilder.put(requestURI); builder.setHeader("Content-Type", Protocol.SPARQL_QUERY_MIME_TYPE + "; charset=utf-8"); - builder.addParameter(Protocol.ACTION_PARAM_NAME, Action.QUERY.toString()); for (NameValuePair nvp : getQueryMethodParameters(ql, null, baseURI, dataset, includeInferred, maxQueryTime, bindings)) { builder.addParameter(nvp); @@ -971,9 +1003,17 @@ protected HttpUriRequest getUpdateMethod(QueryLanguage ql, String update, String RequestBuilder builder; String transactionURL = getTransactionURL(); if (transactionURL != null) { - builder = RequestBuilder.put(transactionURL); + + URI requestURI; + try { + requestURI = getTxnActionURIBuilder(Action.UPDATE).build(); + } catch (URISyntaxException | IOException e) { + logger.error("could not create URL for transaction update", e); + throw new RuntimeException(e); + } + + builder = RequestBuilder.put(requestURI); builder.addHeader("Content-Type", Protocol.SPARQL_UPDATE_MIME_TYPE + "; charset=utf-8"); - builder.addParameter(Protocol.ACTION_PARAM_NAME, Action.UPDATE.toString()); for (NameValuePair nvp : getUpdateMethodParameters(ql, null, baseURI, dataset, includeInferred, maxExecutionTime, bindings)) { builder.addParameter(nvp); @@ -1061,36 +1101,28 @@ protected void upload(HttpEntity reqEntity, String baseURI, boolean overwrite, b boolean useTransaction = transactionURL != null; try { - - String baseLocation = useTransaction ? transactionURL : Protocol.getStatementsLocation(getQueryURL()); - URIBuilder url = new URIBuilder(baseLocation); + URIBuilder uriBuilder = useTransaction ? getTxnActionURIBuilder(action) + : new URIBuilder(Protocol.getStatementsLocation(getQueryURL())); // Set relevant query parameters for (String encodedContext : Protocol.encodeContexts(contexts)) { - url.addParameter(Protocol.CONTEXT_PARAM_NAME, encodedContext); + uriBuilder.addParameter(Protocol.CONTEXT_PARAM_NAME, encodedContext); } if (baseURI != null && baseURI.trim().length() != 0) { String encodedBaseURI = Protocol.encodeValue(SimpleValueFactory.getInstance().createIRI(baseURI)); - url.setParameter(Protocol.BASEURI_PARAM_NAME, encodedBaseURI); + uriBuilder.setParameter(Protocol.BASEURI_PARAM_NAME, encodedBaseURI); } if (preserveNodeIds) { - url.setParameter(Protocol.PRESERVE_BNODE_ID_PARAM_NAME, "true"); - } - - if (useTransaction) { - if (action == null) { - throw new IllegalArgumentException("action can not be null on transaction operation"); - } - url.setParameter(Protocol.ACTION_PARAM_NAME, action.toString()); + uriBuilder.setParameter(Protocol.PRESERVE_BNODE_ID_PARAM_NAME, "true"); } // Select appropriate HTTP method HttpEntityEnclosingRequestBase method = null; try { if (overwrite || useTransaction) { - method = applyAdditionalHeaders(new HttpPut(url.build())); + method = applyAdditionalHeaders(new HttpPut(uriBuilder.build())); } else { - method = applyAdditionalHeaders(new HttpPost(url.build())); + method = applyAdditionalHeaders(new HttpPost(uriBuilder.build())); } // Set payload @@ -1217,4 +1249,9 @@ private T applyAdditionalHeaders(T method) { } return method; } + + private boolean useDeprecatedTxnActions() + throws UnauthorizedException, NumberFormatException, RepositoryException, IOException { + return Integer.parseInt(getServerProtocol()) < 14; + } } diff --git a/core/http/protocol/src/main/java/org/eclipse/rdf4j/http/protocol/Protocol.java b/core/http/protocol/src/main/java/org/eclipse/rdf4j/http/protocol/Protocol.java index 0eb50ddb466..6ead8fee17f 100644 --- a/core/http/protocol/src/main/java/org/eclipse/rdf4j/http/protocol/Protocol.java +++ b/core/http/protocol/src/main/java/org/eclipse/rdf4j/http/protocol/Protocol.java @@ -104,13 +104,15 @@ public enum TIMEOUT { * Protocol version. * * */ - public static final String VERSION = "12"; + public static final String VERSION = "14"; /** * Parameter name for the 'subject' parameter of a statement query. diff --git a/site/content/documentation/reference/rest-api/changelog.md b/site/content/documentation/reference/rest-api/changelog.md new file mode 100644 index 00000000000..6372f9f65a5 --- /dev/null +++ b/site/content/documentation/reference/rest-api/changelog.md @@ -0,0 +1,37 @@ +--- +title: "RDF4J REST API: Changelog" +toc: true +weight: 1 +--- + +[OpenAPI specification](/documentation/reference/rest-api/) + +## Version history + +The RDF4J REST API uses a single integer version numbering scheme. It does not follow semantic versioning. However, the implementations of the API client and server in RDF4J make a conscious effort to stay backward compatible with at least one previous protocol version. + +### 14: since RDF4J 4.3.0 + +- Replaced usage of the `action` parameter on the `/transactions/{txnID}` endpoint with separate endpoints for each available action: `/transactions/{txnID}/add`, `/transactions/{txnID}/query`, and so on. +- `action` parameter for transactions is deprecated + +### 13: since RDF4J 4.0.0 + +- Removed support for the deprecated SYSTEM repository. + +### 12: since RDF4J 3.5.0 + +- Added suport for the `prepare` transaction operation. + +### 11: since RDF4J 3.3.0 + +- Added support for sending general transaction setting parameters. + +### 10: since RDF4J 3.1.0 + +- Added support for retrieving a repository configuration. + +### 9: since RDF4J 3.0.0 + +- Added direct API support for creating a new repository remotely and/or updating an existing repository's configuration. +- SYSTEM repository support is deprecated. diff --git a/site/static/documentation/reference/rest-api/rdf4j-openapi.yaml b/site/static/documentation/reference/rest-api/rdf4j-openapi.yaml index 3bdc164998c..e6c2bdbcccc 100644 --- a/site/static/documentation/reference/rest-api/rdf4j-openapi.yaml +++ b/site/static/documentation/reference/rest-api/rdf4j-openapi.yaml @@ -1,14 +1,16 @@ openapi: "3.0.1" info: title: RDF4J REST API - version: "10" + version: "14" description: | The RDF4J REST API is an HTTP protocol that covers a fully compliant implementation of the [SPARQL 1.1 Protocol W3C Recommendation](https://www.w3.org/TR/sparql11-protocol/). This ensures that RDF4J Server functions as a fully standard-compliant [SPARQL](https://www.w3.org/TR/sparql11-query/) endpoint. The RDF4J REST API additionally supports the [SPARQL 1.1 Graph Store HTTP Protocol W3C Recommendation](https://www.w3.org/TR/sparql11-http-rdf-update/). The RDF4J REST API extends the W3C standards in several aspects, the most important of which is database transaction management. + Version 13 was released as part of RDF4J 4.3.0. See the [REST API Changelog](/documentation/reference/rest-api/changelog) for details. + externalDocs: - url: https://rdf4j.org/documentation/reference/rest-api/ + url: https://rdf4j.org/documentation/reference/rest-api/changelog.md servers: - url: http://localhost:8080/rdf4j-server/ diff --git a/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/transaction/AbstractActionController.java b/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/transaction/AbstractActionController.java new file mode 100644 index 00000000000..4f0b5872b61 --- /dev/null +++ b/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/transaction/AbstractActionController.java @@ -0,0 +1,159 @@ +/******************************************************************************* + * 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.http.server.repository.transaction; + +import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; + +import static org.eclipse.rdf4j.http.protocol.Protocol.CONTEXT_PARAM_NAME; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.rdf4j.common.webapp.views.SimpleResponseView; +import org.eclipse.rdf4j.http.protocol.Protocol; +import org.eclipse.rdf4j.http.server.ClientHTTPException; +import org.eclipse.rdf4j.http.server.ProtocolUtil; +import org.eclipse.rdf4j.http.server.ServerHTTPException; +import org.eclipse.rdf4j.model.Resource; +import org.eclipse.rdf4j.model.impl.SimpleValueFactory; +import org.eclipse.rdf4j.rio.RDFFormat; +import org.eclipse.rdf4j.rio.Rio; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.context.ApplicationContextException; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.AbstractController; + +/** + * Handles requests for transaction operations on a repository. + * + * @author Jeen Broekstra + */ +public abstract class AbstractActionController extends AbstractController implements DisposableBean { + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + public AbstractActionController() throws ApplicationContextException { + setSupportedMethods(new String[] { METHOD_POST, "PUT" }); + } + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) + throws Exception { + UUID transactionId = getTransactionID(request); + logger.debug("transaction id: {}", transactionId); + logger.debug("request content type: {}", request.getContentType()); + + Transaction transaction = ActiveTransactionRegistry.INSTANCE.getTransaction(transactionId); + + if (transaction == null) { + logger.warn("could not find transaction for transaction id {}", transactionId); + throw new ClientHTTPException(SC_BAD_REQUEST, + "unable to find registered transaction for transaction id '" + transactionId + "'"); + } + + try { + var result = handleAction(request, response, transaction); + if (!(transaction.isClosed() || transaction.isComplete())) { + ActiveTransactionRegistry.INSTANCE.active(transaction); + } + + return result; + } catch (Exception e) { + if (e instanceof ClientHTTPException) { + throw (ClientHTTPException) e; + } else if (e instanceof ServerHTTPException) { + throw (ServerHTTPException) e; + } else { + throw new ServerHTTPException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, + "Transaction handling error: " + e.getMessage(), e); + } + } + + } + + // Comes from disposableBean interface so to be able to stop the ActiveTransactionRegistry scheduler + @Override + public void destroy() throws Exception { + ActiveTransactionRegistry.INSTANCE.destroyScheduler(); + } + + /** + * Handle the specific action as part of the supplied {@link Transaction} object. + * + * @param request the request + * @param transaction the transaction on which the action is to be executed + * @return result of the action (may not be null) + */ + protected abstract ModelAndView handleAction(HttpServletRequest request, HttpServletResponse response, + Transaction transaction) throws Exception; + + static RDFFormat getRDFFormat(HttpServletRequest request) { + return Rio.getParserFormatForMIMEType(request.getContentType()) + .orElseThrow(Rio.unsupportedFormat(request.getContentType())); + } + + static String getBaseURI(HttpServletRequest request) { + String baseURI = request.getParameter(Protocol.BASEURI_PARAM_NAME); + return baseURI == null ? "" : baseURI; + } + + static Resource[] getContexts(HttpServletRequest request) throws ClientHTTPException { + return ProtocolUtil.parseContextParam(request, CONTEXT_PARAM_NAME, SimpleValueFactory.getInstance()); + } + + static Charset getCharset(HttpServletRequest request) { + return request.getCharacterEncoding() != null ? Charset.forName(request.getCharacterEncoding()) + : StandardCharsets.UTF_8; + } + + /** + * A {@link ModelAndView} for a 200 OK response with an empty body + */ + static ModelAndView emptyOkResponse() { + Map model = new HashMap<>(); + model.put(SimpleResponseView.SC_KEY, HttpServletResponse.SC_OK); + return new ModelAndView(SimpleResponseView.getInstance(), model); + } + + /* private methods */ + + private UUID getTransactionID(HttpServletRequest request) throws ClientHTTPException { + String pathInfoStr = request.getPathInfo(); + + UUID txnID = null; + + if (pathInfoStr != null && !pathInfoStr.equals("/")) { + String[] pathInfo = pathInfoStr.substring(1).split("/"); + // should be of the form: //transactions/ + if (pathInfo.length == 3) { + try { + txnID = UUID.fromString(pathInfo[2]); + logger.debug("txnID is '{}'", txnID); + } catch (IllegalArgumentException e) { + throw new ClientHTTPException(SC_BAD_REQUEST, "not a valid transaction id: " + pathInfo[2]); + } + } else { + logger.warn("could not determine transaction id from path info {} ", pathInfoStr); + } + } + + return txnID; + } + +} diff --git a/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/transaction/AddController.java b/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/transaction/AddController.java new file mode 100644 index 00000000000..9067c5547d2 --- /dev/null +++ b/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/transaction/AddController.java @@ -0,0 +1,45 @@ +/******************************************************************************* + * Copyright (c) 2023 Eclipse RDF4J contributors. + * + * 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.http.server.repository.transaction; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.rdf4j.http.protocol.Protocol; +import org.eclipse.rdf4j.http.server.ProtocolUtil; +import org.eclipse.rdf4j.rio.RDFFormat; +import org.eclipse.rdf4j.rio.Rio; +import org.springframework.web.servlet.ModelAndView; + +/** + * @author jeen + * + */ +public class AddController extends AbstractActionController { + + @Override + protected ModelAndView handleAction(HttpServletRequest request, HttpServletResponse response, + Transaction transaction) throws Exception { + + var baseURI = getBaseURI(request); + var contexts = getContexts(request); + + boolean preserveNodeIds = ProtocolUtil.parseBooleanParam(request, Protocol.PRESERVE_BNODE_ID_PARAM_NAME, false); + RDFFormat format = Rio.getParserFormatForMIMEType(request.getContentType()) + .orElseThrow(Rio.unsupportedFormat(request.getContentType())); + + transaction.add(request.getInputStream(), baseURI, format, preserveNodeIds, contexts); + + return emptyOkResponse(); + } + +} diff --git a/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/transaction/CommitController.java b/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/transaction/CommitController.java new file mode 100644 index 00000000000..dbc30cbff5a --- /dev/null +++ b/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/transaction/CommitController.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright (c) 2023 Eclipse RDF4J contributors. + * + * 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.http.server.repository.transaction; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.web.servlet.ModelAndView; + +/** + * @author jeen + * + */ +public class CommitController extends AbstractActionController { + + @Override + protected ModelAndView handleAction(HttpServletRequest request, HttpServletResponse response, + Transaction transaction) throws Exception { + + transaction.commit(); + // If commit fails with an exception, deregister should be skipped so the user + // has a chance to do a proper rollback. See #725. + ActiveTransactionRegistry.INSTANCE.deregister(transaction); + + return emptyOkResponse(); + } +} diff --git a/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/transaction/DeleteController.java b/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/transaction/DeleteController.java new file mode 100644 index 00000000000..2bd38caee92 --- /dev/null +++ b/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/transaction/DeleteController.java @@ -0,0 +1,37 @@ +/******************************************************************************* + * Copyright (c) 2023 Eclipse RDF4J contributors. + * + * 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.http.server.repository.transaction; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.web.servlet.ModelAndView; + +/** + * @author jeen + * + */ +public class DeleteController extends AbstractActionController { + + @Override + protected ModelAndView handleAction(HttpServletRequest request, HttpServletResponse response, + Transaction transaction) throws Exception { + + var baseURI = getBaseURI(request); + var format = getRDFFormat(request); + + transaction.delete(format, request.getInputStream(), baseURI); + + return emptyOkResponse(); + } + +} diff --git a/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/transaction/ExportController.java b/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/transaction/ExportController.java new file mode 100644 index 00000000000..d5d7e44b436 --- /dev/null +++ b/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/transaction/ExportController.java @@ -0,0 +1,88 @@ +/******************************************************************************* + * Copyright (c) 2023 Eclipse RDF4J contributors. + * + * 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.http.server.repository.transaction; + +import static org.eclipse.rdf4j.http.protocol.Protocol.CONTEXT_PARAM_NAME; +import static org.eclipse.rdf4j.http.protocol.Protocol.INCLUDE_INFERRED_PARAM_NAME; +import static org.eclipse.rdf4j.http.protocol.Protocol.OBJECT_PARAM_NAME; +import static org.eclipse.rdf4j.http.protocol.Protocol.PREDICATE_PARAM_NAME; +import static org.eclipse.rdf4j.http.protocol.Protocol.SUBJECT_PARAM_NAME; + +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.rdf4j.http.server.ClientHTTPException; +import org.eclipse.rdf4j.http.server.ProtocolUtil; +import org.eclipse.rdf4j.model.IRI; +import org.eclipse.rdf4j.model.Resource; +import org.eclipse.rdf4j.model.Value; +import org.eclipse.rdf4j.model.ValueFactory; +import org.eclipse.rdf4j.model.impl.SimpleValueFactory; +import org.eclipse.rdf4j.rio.RDFWriterFactory; +import org.eclipse.rdf4j.rio.RDFWriterRegistry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.servlet.ModelAndView; + +/** + * @author jeen + * + */ +public class ExportController extends AbstractActionController { + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + @Override + protected ModelAndView handleAction(HttpServletRequest request, HttpServletResponse response, + Transaction transaction) throws Exception { + logger.info("{} txn get/export statements request", request.getMethod()); + var result = getExportStatementsResult(transaction, request, response); + logger.info("{} txn get/export statements request finished", request.getMethod()); + return result; + } + + /** + * Get all statements and export them as RDF. + * + * @return a model and view for exporting the statements. + */ + private ModelAndView getExportStatementsResult(Transaction transaction, HttpServletRequest request, + HttpServletResponse response) throws ClientHTTPException { + ProtocolUtil.logRequestParameters(request); + + ValueFactory vf = SimpleValueFactory.getInstance(); + + Resource subj = ProtocolUtil.parseResourceParam(request, SUBJECT_PARAM_NAME, vf); + IRI pred = ProtocolUtil.parseURIParam(request, PREDICATE_PARAM_NAME, vf); + Value obj = ProtocolUtil.parseValueParam(request, OBJECT_PARAM_NAME, vf); + Resource[] contexts = ProtocolUtil.parseContextParam(request, CONTEXT_PARAM_NAME, vf); + boolean useInferencing = ProtocolUtil.parseBooleanParam(request, INCLUDE_INFERRED_PARAM_NAME, true); + + RDFWriterFactory rdfWriterFactory = ProtocolUtil.getAcceptableService(request, response, + RDFWriterRegistry.getInstance()); + + Map model = new HashMap<>(); + model.put(TransactionExportStatementsView.SUBJECT_KEY, subj); + model.put(TransactionExportStatementsView.PREDICATE_KEY, pred); + model.put(TransactionExportStatementsView.OBJECT_KEY, obj); + model.put(TransactionExportStatementsView.CONTEXTS_KEY, contexts); + model.put(TransactionExportStatementsView.USE_INFERENCING_KEY, Boolean.valueOf(useInferencing)); + model.put(TransactionExportStatementsView.FACTORY_KEY, rdfWriterFactory); + model.put(TransactionExportStatementsView.HEADERS_ONLY, METHOD_HEAD.equals(request.getMethod())); + + model.put(TransactionExportStatementsView.TRANSACTION_KEY, transaction); + return new ModelAndView(TransactionExportStatementsView.getInstance(), model); + } +} diff --git a/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/transaction/PingController.java b/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/transaction/PingController.java new file mode 100644 index 00000000000..74a021aea95 --- /dev/null +++ b/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/transaction/PingController.java @@ -0,0 +1,44 @@ +/******************************************************************************* + * Copyright (c) 2023 Eclipse RDF4J contributors. + * + * 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.http.server.repository.transaction; + +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.rdf4j.common.webapp.views.SimpleResponseView; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.servlet.ModelAndView; + +/** + * @author jeen + * + */ +public class PingController extends AbstractActionController { + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + @Override + protected ModelAndView handleAction(HttpServletRequest request, HttpServletResponse response, + Transaction transaction) throws Exception { + + String text = Long.toString(ActiveTransactionRegistry.INSTANCE.getTimeout(TimeUnit.MILLISECONDS)); + Map model = Collections.singletonMap(SimpleResponseView.CONTENT_KEY, text); + var result = new ModelAndView(SimpleResponseView.getInstance(), model); + return result; + } + +} diff --git a/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/transaction/PrepareController.java b/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/transaction/PrepareController.java new file mode 100644 index 00000000000..9eabddcc707 --- /dev/null +++ b/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/transaction/PrepareController.java @@ -0,0 +1,32 @@ +/******************************************************************************* + * Copyright (c) 2023 Eclipse RDF4J contributors. + * + * 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.http.server.repository.transaction; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.web.servlet.ModelAndView; + +/** + * @author jeen + * + */ +public class PrepareController extends AbstractActionController { + + @Override + protected ModelAndView handleAction(HttpServletRequest request, HttpServletResponse response, + Transaction transaction) throws Exception { + + transaction.prepare(); + return emptyOkResponse(); + } +} diff --git a/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/transaction/QueryController.java b/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/transaction/QueryController.java new file mode 100644 index 00000000000..f646ecf267e --- /dev/null +++ b/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/transaction/QueryController.java @@ -0,0 +1,266 @@ +/******************************************************************************* + * Copyright (c) 2023 Eclipse RDF4J contributors. + * + * 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.http.server.repository.transaction; + +import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; +import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; +import static javax.servlet.http.HttpServletResponse.SC_SERVICE_UNAVAILABLE; + +import static org.eclipse.rdf4j.http.protocol.Protocol.BINDING_PREFIX; +import static org.eclipse.rdf4j.http.protocol.Protocol.DEFAULT_GRAPH_PARAM_NAME; +import static org.eclipse.rdf4j.http.protocol.Protocol.INCLUDE_INFERRED_PARAM_NAME; +import static org.eclipse.rdf4j.http.protocol.Protocol.NAMED_GRAPH_PARAM_NAME; +import static org.eclipse.rdf4j.http.protocol.Protocol.QUERY_LANGUAGE_PARAM_NAME; +import static org.eclipse.rdf4j.http.protocol.Protocol.QUERY_PARAM_NAME; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutionException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.io.IOUtils; +import org.eclipse.rdf4j.common.lang.FileFormat; +import org.eclipse.rdf4j.common.lang.service.FileFormatServiceRegistry; +import org.eclipse.rdf4j.http.protocol.Protocol; +import org.eclipse.rdf4j.http.protocol.error.ErrorInfo; +import org.eclipse.rdf4j.http.protocol.error.ErrorType; +import org.eclipse.rdf4j.http.server.ClientHTTPException; +import org.eclipse.rdf4j.http.server.HTTPException; +import org.eclipse.rdf4j.http.server.ProtocolUtil; +import org.eclipse.rdf4j.http.server.ServerHTTPException; +import org.eclipse.rdf4j.http.server.repository.BooleanQueryResultView; +import org.eclipse.rdf4j.http.server.repository.GraphQueryResultView; +import org.eclipse.rdf4j.http.server.repository.QueryResultView; +import org.eclipse.rdf4j.http.server.repository.TupleQueryResultView; +import org.eclipse.rdf4j.model.IRI; +import org.eclipse.rdf4j.model.Value; +import org.eclipse.rdf4j.model.impl.SimpleValueFactory; +import org.eclipse.rdf4j.query.BooleanQuery; +import org.eclipse.rdf4j.query.GraphQuery; +import org.eclipse.rdf4j.query.MalformedQueryException; +import org.eclipse.rdf4j.query.Query; +import org.eclipse.rdf4j.query.QueryEvaluationException; +import org.eclipse.rdf4j.query.QueryInterruptedException; +import org.eclipse.rdf4j.query.QueryLanguage; +import org.eclipse.rdf4j.query.TupleQuery; +import org.eclipse.rdf4j.query.UnsupportedQueryLanguageException; +import org.eclipse.rdf4j.query.impl.SimpleDataset; +import org.eclipse.rdf4j.query.resultio.BooleanQueryResultWriterRegistry; +import org.eclipse.rdf4j.query.resultio.TupleQueryResultWriterRegistry; +import org.eclipse.rdf4j.repository.RepositoryException; +import org.eclipse.rdf4j.rio.RDFWriterRegistry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationContextException; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.View; + +/** + * @author jeen + * + */ +public class QueryController extends AbstractActionController { + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + public QueryController() throws ApplicationContextException { + setSupportedMethods(METHOD_POST, "PUT"); + } + + @Override + protected ModelAndView handleAction(HttpServletRequest request, HttpServletResponse response, + Transaction transaction) throws Exception { + logger.info("{} txn query request", request.getMethod()); + + String queryStr; + final String contentType = request.getContentType(); + if (contentType != null && contentType.contains(Protocol.SPARQL_QUERY_MIME_TYPE)) { + Charset charset = getCharset(request); + queryStr = IOUtils.toString(request.getInputStream(), charset); + } else { + queryStr = request.getParameter(QUERY_PARAM_NAME); + } + + View view; + Object queryResult; + FileFormatServiceRegistry registry; + + try { + Query query = getQuery(transaction, queryStr, request, response); + + if (query instanceof TupleQuery) { + TupleQuery tQuery = (TupleQuery) query; + + queryResult = transaction.evaluate(tQuery); + registry = TupleQueryResultWriterRegistry.getInstance(); + view = TupleQueryResultView.getInstance(); + } else if (query instanceof GraphQuery) { + GraphQuery gQuery = (GraphQuery) query; + + queryResult = transaction.evaluate(gQuery); + registry = RDFWriterRegistry.getInstance(); + view = GraphQueryResultView.getInstance(); + } else if (query instanceof BooleanQuery) { + BooleanQuery bQuery = (BooleanQuery) query; + + queryResult = transaction.evaluate(bQuery); + registry = BooleanQueryResultWriterRegistry.getInstance(); + view = BooleanQueryResultView.getInstance(); + } else { + throw new ClientHTTPException(SC_BAD_REQUEST, "Unsupported query type: " + query.getClass().getName()); + } + } catch (QueryInterruptedException | InterruptedException | ExecutionException e) { + if (e.getCause() != null && e.getCause() instanceof MalformedQueryException) { + ErrorInfo errInfo = new ErrorInfo(ErrorType.MALFORMED_QUERY, e.getCause().getMessage()); + throw new ClientHTTPException(SC_BAD_REQUEST, errInfo.toString()); + } else { + logger.info("Query interrupted", e); + throw new ServerHTTPException(SC_SERVICE_UNAVAILABLE, "Query execution interrupted"); + } + } catch (QueryEvaluationException e) { + logger.info("Query evaluation error", e); + if (e.getCause() != null && e.getCause() instanceof HTTPException) { + // custom signal from the backend, throw as HTTPException + // directly (see SES-1016). + throw (HTTPException) e.getCause(); + } else { + throw new ServerHTTPException("Query evaluation error: " + e.getMessage()); + } + } + Object factory = ProtocolUtil.getAcceptableService(request, response, registry); + + Map model = new HashMap<>(); + model.put(QueryResultView.FILENAME_HINT_KEY, "query-result"); + model.put(QueryResultView.QUERY_RESULT_KEY, queryResult); + model.put(QueryResultView.FACTORY_KEY, factory); + model.put(QueryResultView.HEADERS_ONLY, false); // TODO needed for HEAD requests. + + logger.info("{} txn query request finished", request.getMethod()); + return new ModelAndView(view, model); + } + + private Query getQuery(Transaction txn, String queryStr, HttpServletRequest request, HttpServletResponse response) + throws IOException, ClientHTTPException, InterruptedException, ExecutionException { + Query result = null; + + // default query language is SPARQL + QueryLanguage queryLn = QueryLanguage.SPARQL; + + String queryLnStr = request.getParameter(QUERY_LANGUAGE_PARAM_NAME); + logger.debug("query language param = {}", queryLnStr); + + if (queryLnStr != null) { + queryLn = QueryLanguage.valueOf(queryLnStr); + + if (queryLn == null) { + throw new ClientHTTPException(SC_BAD_REQUEST, "Unknown query language: " + queryLnStr); + } + } + + String baseURI = request.getParameter(Protocol.BASEURI_PARAM_NAME); + + // determine if inferred triples should be included in query evaluation + boolean includeInferred = ProtocolUtil.parseBooleanParam(request, INCLUDE_INFERRED_PARAM_NAME, true); + + String timeout = request.getParameter(Protocol.TIMEOUT_PARAM_NAME); + int maxQueryTime = 0; + if (timeout != null) { + try { + maxQueryTime = Integer.parseInt(timeout); + } catch (NumberFormatException e) { + throw new ClientHTTPException(SC_BAD_REQUEST, "Invalid timeout value: " + timeout); + } + } + + // build a dataset, if specified + String[] defaultGraphURIs = request.getParameterValues(DEFAULT_GRAPH_PARAM_NAME); + String[] namedGraphURIs = request.getParameterValues(NAMED_GRAPH_PARAM_NAME); + + SimpleDataset dataset = null; + if (defaultGraphURIs != null || namedGraphURIs != null) { + dataset = new SimpleDataset(); + + if (defaultGraphURIs != null) { + for (String defaultGraphURI : defaultGraphURIs) { + try { + IRI uri = null; + if (!"null".equals(defaultGraphURI)) { + uri = SimpleValueFactory.getInstance().createIRI(defaultGraphURI); + } + dataset.addDefaultGraph(uri); + } catch (IllegalArgumentException e) { + throw new ClientHTTPException(SC_BAD_REQUEST, + "Illegal URI for default graph: " + defaultGraphURI); + } + } + } + + if (namedGraphURIs != null) { + for (String namedGraphURI : namedGraphURIs) { + try { + IRI uri = null; + if (!"null".equals(namedGraphURI)) { + uri = SimpleValueFactory.getInstance().createIRI(namedGraphURI); + } + dataset.addNamedGraph(uri); + } catch (IllegalArgumentException e) { + throw new ClientHTTPException(SC_BAD_REQUEST, "Illegal URI for named graph: " + namedGraphURI); + } + } + } + } + + try { + result = txn.prepareQuery(queryLn, queryStr, baseURI); + result.setIncludeInferred(includeInferred); + + if (maxQueryTime > 0) { + result.setMaxExecutionTime(maxQueryTime); + } + + if (dataset != null) { + result.setDataset(dataset); + } + + // determine if any variable bindings have been set on this query. + @SuppressWarnings("unchecked") + Enumeration parameterNames = request.getParameterNames(); + + while (parameterNames.hasMoreElements()) { + String parameterName = parameterNames.nextElement(); + + if (parameterName.startsWith(BINDING_PREFIX) && parameterName.length() > BINDING_PREFIX.length()) { + String bindingName = parameterName.substring(BINDING_PREFIX.length()); + Value bindingValue = ProtocolUtil.parseValueParam(request, parameterName, + SimpleValueFactory.getInstance()); + result.setBinding(bindingName, bindingValue); + } + } + } catch (UnsupportedQueryLanguageException e) { + ErrorInfo errInfo = new ErrorInfo(ErrorType.UNSUPPORTED_QUERY_LANGUAGE, queryLn.getName()); + throw new ClientHTTPException(SC_BAD_REQUEST, errInfo.toString()); + } catch (MalformedQueryException e) { + ErrorInfo errInfo = new ErrorInfo(ErrorType.MALFORMED_QUERY, e.getMessage()); + throw new ClientHTTPException(SC_BAD_REQUEST, errInfo.toString()); + } catch (RepositoryException e) { + logger.error("Repository error", e); + response.sendError(SC_INTERNAL_SERVER_ERROR); + } + + return result; + } +} diff --git a/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/transaction/RollbackController.java b/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/transaction/RollbackController.java new file mode 100644 index 00000000000..be93ad297bc --- /dev/null +++ b/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/transaction/RollbackController.java @@ -0,0 +1,52 @@ +/******************************************************************************* + * Copyright (c) 2023 Eclipse RDF4J contributors. + * + * 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.http.server.repository.transaction; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.rdf4j.common.webapp.views.EmptySuccessView; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationContextException; +import org.springframework.web.servlet.ModelAndView; + +/** + * @author jeen + * + */ +public class RollbackController extends AbstractActionController { + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + public RollbackController() throws ApplicationContextException { + setSupportedMethods("DELETE"); + } + + @Override + protected ModelAndView handleAction(HttpServletRequest request, HttpServletResponse response, + Transaction transaction) throws Exception { + logger.info("transaction rollback"); + try { + transaction.rollback(); + } finally { + try { + transaction.close(); + } finally { + ActiveTransactionRegistry.INSTANCE.deregister(transaction); + } + } + logger.info("transaction rollback request finished."); + + return new ModelAndView(EmptySuccessView.getInstance()); + } +} diff --git a/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/transaction/SizeController.java b/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/transaction/SizeController.java new file mode 100644 index 00000000000..706f174f604 --- /dev/null +++ b/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/transaction/SizeController.java @@ -0,0 +1,79 @@ +/******************************************************************************* + * Copyright (c) 2023 Eclipse RDF4J contributors. + * + * 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.http.server.repository.transaction; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutionException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.rdf4j.common.webapp.views.SimpleResponseView; +import org.eclipse.rdf4j.http.protocol.Protocol; +import org.eclipse.rdf4j.http.server.HTTPException; +import org.eclipse.rdf4j.http.server.ProtocolUtil; +import org.eclipse.rdf4j.http.server.ServerHTTPException; +import org.eclipse.rdf4j.http.server.repository.RepositoryInterceptor; +import org.eclipse.rdf4j.model.Resource; +import org.eclipse.rdf4j.model.ValueFactory; +import org.eclipse.rdf4j.repository.Repository; +import org.eclipse.rdf4j.repository.RepositoryException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.servlet.ModelAndView; + +/** + * @author jeen + * + */ +public class SizeController extends AbstractActionController { + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + @Override + protected ModelAndView handleAction(HttpServletRequest request, HttpServletResponse response, + Transaction transaction) throws Exception { + logger.info("{} txn size request", request.getMethod()); + var result = getSize(transaction, request, response); + logger.info("{} txn size request finished", request.getMethod()); + + return result; + } + + private ModelAndView getSize(Transaction transaction, HttpServletRequest request, HttpServletResponse response) + throws HTTPException { + ProtocolUtil.logRequestParameters(request); + + Map model = new HashMap<>(); + final boolean headersOnly = METHOD_HEAD.equals(request.getMethod()); + + if (!headersOnly) { + Repository repository = RepositoryInterceptor.getRepository(request); + + ValueFactory vf = repository.getValueFactory(); + Resource[] contexts = ProtocolUtil.parseContextParam(request, Protocol.CONTEXT_PARAM_NAME, vf); + + long size; + + try { + size = transaction.getSize(contexts); + } catch (RepositoryException | InterruptedException | ExecutionException e) { + throw new ServerHTTPException("Repository error: " + e.getMessage(), e); + } + model.put(SimpleResponseView.CONTENT_KEY, String.valueOf(size)); + } + + return new ModelAndView(SimpleResponseView.getInstance(), model); + } + +} diff --git a/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/transaction/TransactionController.java b/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/transaction/TransactionController.java index fbe58ca6071..53e69115888 100644 --- a/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/transaction/TransactionController.java +++ b/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/transaction/TransactionController.java @@ -11,91 +11,26 @@ package org.eclipse.rdf4j.http.server.repository.transaction; import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; -import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; -import static javax.servlet.http.HttpServletResponse.SC_NOT_ACCEPTABLE; -import static javax.servlet.http.HttpServletResponse.SC_SERVICE_UNAVAILABLE; -import static org.eclipse.rdf4j.http.protocol.Protocol.BINDING_PREFIX; -import static org.eclipse.rdf4j.http.protocol.Protocol.CONTEXT_PARAM_NAME; -import static org.eclipse.rdf4j.http.protocol.Protocol.DEFAULT_GRAPH_PARAM_NAME; -import static org.eclipse.rdf4j.http.protocol.Protocol.INCLUDE_INFERRED_PARAM_NAME; -import static org.eclipse.rdf4j.http.protocol.Protocol.INSERT_GRAPH_PARAM_NAME; -import static org.eclipse.rdf4j.http.protocol.Protocol.NAMED_GRAPH_PARAM_NAME; -import static org.eclipse.rdf4j.http.protocol.Protocol.OBJECT_PARAM_NAME; -import static org.eclipse.rdf4j.http.protocol.Protocol.PREDICATE_PARAM_NAME; -import static org.eclipse.rdf4j.http.protocol.Protocol.QUERY_LANGUAGE_PARAM_NAME; -import static org.eclipse.rdf4j.http.protocol.Protocol.QUERY_PARAM_NAME; -import static org.eclipse.rdf4j.http.protocol.Protocol.REMOVE_GRAPH_PARAM_NAME; -import static org.eclipse.rdf4j.http.protocol.Protocol.SUBJECT_PARAM_NAME; -import static org.eclipse.rdf4j.http.protocol.Protocol.USING_GRAPH_PARAM_NAME; -import static org.eclipse.rdf4j.http.protocol.Protocol.USING_NAMED_GRAPH_PARAM_NAME; - -import java.io.IOException; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.util.Collections; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.Map; import java.util.UUID; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.apache.commons.io.IOUtils; -import org.eclipse.rdf4j.common.lang.FileFormat; -import org.eclipse.rdf4j.common.lang.service.FileFormatServiceRegistry; import org.eclipse.rdf4j.common.webapp.views.EmptySuccessView; -import org.eclipse.rdf4j.common.webapp.views.SimpleResponseView; import org.eclipse.rdf4j.http.protocol.Protocol; import org.eclipse.rdf4j.http.protocol.Protocol.Action; -import org.eclipse.rdf4j.http.protocol.error.ErrorInfo; -import org.eclipse.rdf4j.http.protocol.error.ErrorType; import org.eclipse.rdf4j.http.server.ClientHTTPException; -import org.eclipse.rdf4j.http.server.HTTPException; -import org.eclipse.rdf4j.http.server.ProtocolUtil; -import org.eclipse.rdf4j.http.server.ServerHTTPException; -import org.eclipse.rdf4j.http.server.repository.BooleanQueryResultView; -import org.eclipse.rdf4j.http.server.repository.GraphQueryResultView; -import org.eclipse.rdf4j.http.server.repository.QueryResultView; -import org.eclipse.rdf4j.http.server.repository.RepositoryInterceptor; -import org.eclipse.rdf4j.http.server.repository.TupleQueryResultView; -import org.eclipse.rdf4j.model.IRI; -import org.eclipse.rdf4j.model.Resource; -import org.eclipse.rdf4j.model.Value; -import org.eclipse.rdf4j.model.ValueFactory; -import org.eclipse.rdf4j.model.impl.SimpleValueFactory; -import org.eclipse.rdf4j.query.BooleanQuery; -import org.eclipse.rdf4j.query.GraphQuery; -import org.eclipse.rdf4j.query.MalformedQueryException; -import org.eclipse.rdf4j.query.Query; -import org.eclipse.rdf4j.query.QueryEvaluationException; -import org.eclipse.rdf4j.query.QueryInterruptedException; -import org.eclipse.rdf4j.query.QueryLanguage; -import org.eclipse.rdf4j.query.TupleQuery; -import org.eclipse.rdf4j.query.UnsupportedQueryLanguageException; -import org.eclipse.rdf4j.query.UpdateExecutionException; -import org.eclipse.rdf4j.query.impl.SimpleDataset; -import org.eclipse.rdf4j.query.resultio.BooleanQueryResultWriterRegistry; -import org.eclipse.rdf4j.query.resultio.TupleQueryResultWriterRegistry; -import org.eclipse.rdf4j.repository.Repository; -import org.eclipse.rdf4j.repository.RepositoryException; -import org.eclipse.rdf4j.rio.RDFFormat; -import org.eclipse.rdf4j.rio.RDFWriterFactory; -import org.eclipse.rdf4j.rio.RDFWriterRegistry; -import org.eclipse.rdf4j.rio.Rio; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.DisposableBean; import org.springframework.context.ApplicationContextException; import org.springframework.web.servlet.ModelAndView; -import org.springframework.web.servlet.View; import org.springframework.web.servlet.mvc.AbstractController; /** - * Handles requests for transaction creation on a repository. + * Handles requests for transaction rollbacks on a repository, and provides backward-compatible (deprecated) support for + * all other transaction operations. * * @author Jeen Broekstra */ @@ -110,6 +45,7 @@ public TransactionController() throws ApplicationContextException { @Override protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + ModelAndView result; String reqMethod = request.getMethod(); @@ -125,55 +61,81 @@ protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpSer "unable to find registered transaction for transaction id '" + transactionId + "'"); } - // if no action is specified in the request, it's a rollback (since it's - // the only txn operation that does not require the action parameter). + if ("DELETE".equals(reqMethod)) { + logger.info("transaction rollback"); + try { + transaction.rollback(); + } finally { + try { + transaction.close(); + } finally { + ActiveTransactionRegistry.INSTANCE.deregister(transaction); + } + } + result = new ModelAndView(EmptySuccessView.getInstance()); + logger.info("transaction rollback request finished."); + if (!(transaction.isClosed() || transaction.isComplete())) { + ActiveTransactionRegistry.INSTANCE.active(transaction); + } + return result; + } + + if (!("PUT".equals(reqMethod) || METHOD_POST.equals(reqMethod))) { + throw new ClientHTTPException(HttpServletResponse.SC_METHOD_NOT_ALLOWED, + "Method not allowed: " + reqMethod); + } + + // if no action is specified in the request, it's a rollback final String actionParam = request.getParameter(Protocol.ACTION_PARAM_NAME); final Action action = actionParam != null ? Action.valueOf(actionParam) : Action.ROLLBACK; + switch (action) { + case ADD: + logger.warn("{} {}: deprecated endpoint. Use {} instead", reqMethod, request.getServletPath(), + request.getServletPath() + "/" + action.toString().toLowerCase()); + result = new AddController().handleAction(request, response, transaction); + break; + case DELETE: + logger.warn("{} {}: deprecated endpoint. Use {} instead", reqMethod, request.getServletPath(), + request.getServletPath() + "/" + action.toString().toLowerCase()); + result = new DeleteController().handleAction(request, response, transaction); + break; + case UPDATE: + logger.warn("{} {}: deprecated endpoint. Use {} instead", reqMethod, request.getServletPath(), + request.getServletPath() + "/" + action.toString().toLowerCase()); + result = new UpdateController().handleAction(request, response, transaction); + break; + case PREPARE: + logger.warn("{} {}: deprecated endpoint. Use {} instead", reqMethod, request.getServletPath(), + request.getServletPath() + "/" + action.toString().toLowerCase()); + result = new PrepareController().handleAction(request, response, transaction); + break; case QUERY: - // TODO SES-2238 note that we allow POST requests for backward - // compatibility reasons with earlier - // 2.8.x releases, even though according to the protocol spec only - // PUT is allowed. - if ("PUT".equals(reqMethod) || METHOD_POST.equals(reqMethod)) { - logger.info("{} txn query request", reqMethod); - result = processQuery(transaction, request, response); - logger.info("{} txn query request finished", reqMethod); - } else { - throw new ClientHTTPException(HttpServletResponse.SC_METHOD_NOT_ALLOWED, - "Method not allowed: " + reqMethod); - } + logger.warn("{} {}: deprecated endpoint. Use {} instead", reqMethod, request.getServletPath(), + request.getServletPath() + "/" + action.toString().toLowerCase()); + result = new QueryController().handleAction(request, response, transaction); + break; + case COMMIT: + logger.warn("{} {}: deprecated endpoint. Use {} instead", reqMethod, request.getServletPath(), + request.getServletPath() + "/" + action.toString().toLowerCase()); + result = new CommitController().handleAction(request, response, transaction); break; case GET: - if ("PUT".equals(reqMethod) || METHOD_POST.equals(reqMethod)) { - logger.info("{} txn get/export statements request", reqMethod); - result = getExportStatementsResult(transaction, request, response); - logger.info("{} txn get/export statements request finished", reqMethod); - } else { - throw new ClientHTTPException(HttpServletResponse.SC_METHOD_NOT_ALLOWED, - "Method not allowed: " + reqMethod); - } + logger.warn("{} {}: deprecated endpoint", reqMethod, request.getServletPath()); + result = new ExportController().handleAction(request, response, transaction); break; case SIZE: - if ("PUT".equals(reqMethod) || METHOD_POST.equals(reqMethod)) { - logger.info("{} txn size request", reqMethod); - result = getSize(transaction, request, response); - logger.info("{} txn size request finished", reqMethod); - } else { - throw new ClientHTTPException(HttpServletResponse.SC_METHOD_NOT_ALLOWED, - "Method not allowed: " + reqMethod); - } + logger.warn("{} {}: deprecated endpoint", reqMethod, request.getServletPath()); + result = new SizeController().handleAction(request, response, transaction); break; case PING: - String text = Long.toString(ActiveTransactionRegistry.INSTANCE.getTimeout(TimeUnit.MILLISECONDS)); - Map model = Collections.singletonMap(SimpleResponseView.CONTENT_KEY, text); - result = new ModelAndView(SimpleResponseView.getInstance(), model); + logger.warn("{} {}: deprecated endpoint", reqMethod, request.getServletPath()); + result = new PingController().handleAction(request, response, transaction); break; default: - // TODO Action.ROLLBACK check is for backward compatibility with - // older 2.8.x releases only. It's not in the protocol spec. - if ("DELETE".equals(reqMethod) - || (action.equals(Action.ROLLBACK) && ("PUT".equals(reqMethod) || METHOD_POST.equals(reqMethod)))) { + if (action.equals(Action.ROLLBACK) && ("PUT".equals(reqMethod) || METHOD_POST.equals(reqMethod))) { + logger.warn("{} {}: deprecated verb for rollback action. Use DELETE instead", reqMethod, + request.getServletPath()); logger.info("transaction rollback"); try { transaction.rollback(); @@ -186,17 +148,13 @@ protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpSer } result = new ModelAndView(EmptySuccessView.getInstance()); logger.info("transaction rollback request finished."); - } else if ("PUT".equals(reqMethod) || METHOD_POST.equals(reqMethod)) { - // TODO filter for appropriate PUT operations - logger.info("{} txn operation", reqMethod); - result = processModificationOperation(transaction, action, request, response); - logger.info("PUT txn operation request finished."); } else { throw new ClientHTTPException(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "Method not allowed: " + reqMethod); } break; } + if (!(transaction.isClosed() || transaction.isComplete())) { ActiveTransactionRegistry.INSTANCE.active(transaction); } @@ -226,454 +184,6 @@ private UUID getTransactionID(HttpServletRequest request) throws ClientHTTPExcep return txnID; } - private ModelAndView processModificationOperation(Transaction transaction, Action action, - HttpServletRequest request, HttpServletResponse response) throws IOException, HTTPException { - ProtocolUtil.logRequestParameters(request); - - Map model = new HashMap<>(); - - String baseURI = request.getParameter(Protocol.BASEURI_PARAM_NAME); - if (baseURI == null) { - baseURI = ""; - } - - final Resource[] contexts = ProtocolUtil.parseContextParam(request, CONTEXT_PARAM_NAME, - SimpleValueFactory.getInstance()); - - final boolean preserveNodeIds = ProtocolUtil.parseBooleanParam(request, Protocol.PRESERVE_BNODE_ID_PARAM_NAME, - false); - - try { - RDFFormat format; - switch (action) { - case ADD: - format = Rio.getParserFormatForMIMEType(request.getContentType()) - .orElseThrow(Rio.unsupportedFormat(request.getContentType())); - transaction.add(request.getInputStream(), baseURI, format, preserveNodeIds, contexts); - break; - case DELETE: - format = Rio.getParserFormatForMIMEType(request.getContentType()) - .orElseThrow(Rio.unsupportedFormat(request.getContentType())); - transaction.delete(format, request.getInputStream(), baseURI); - break; - case UPDATE: - return getSparqlUpdateResult(transaction, request, response); - case PREPARE: - transaction.prepare(); - break; - case COMMIT: - transaction.commit(); - // If commit fails with an exception, deregister should be skipped so the user - // has a chance to do a proper rollback. See #725. - ActiveTransactionRegistry.INSTANCE.deregister(transaction); - break; - default: - logger.warn("transaction modification action '{}' not recognized", action); - throw new ClientHTTPException("modification action not recognized: " + action); - } - - model.put(SimpleResponseView.SC_KEY, HttpServletResponse.SC_OK); - return new ModelAndView(SimpleResponseView.getInstance(), model); - } catch (Exception e) { - if (e instanceof ClientHTTPException) { - throw (ClientHTTPException) e; - } else { - throw new ServerHTTPException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, - "Transaction handling error: " + e.getMessage(), e); - } - } - } - - private ModelAndView getSize(Transaction transaction, HttpServletRequest request, HttpServletResponse response) - throws HTTPException { - ProtocolUtil.logRequestParameters(request); - - Map model = new HashMap<>(); - final boolean headersOnly = METHOD_HEAD.equals(request.getMethod()); - - if (!headersOnly) { - Repository repository = RepositoryInterceptor.getRepository(request); - - ValueFactory vf = repository.getValueFactory(); - Resource[] contexts = ProtocolUtil.parseContextParam(request, Protocol.CONTEXT_PARAM_NAME, vf); - - long size; - - try { - size = transaction.getSize(contexts); - } catch (RepositoryException | InterruptedException | ExecutionException e) { - throw new ServerHTTPException("Repository error: " + e.getMessage(), e); - } - model.put(SimpleResponseView.CONTENT_KEY, String.valueOf(size)); - } - - return new ModelAndView(SimpleResponseView.getInstance(), model); - } - - /** - * Get all statements and export them as RDF. - * - * @return a model and view for exporting the statements. - */ - private ModelAndView getExportStatementsResult(Transaction transaction, HttpServletRequest request, - HttpServletResponse response) throws ClientHTTPException { - ProtocolUtil.logRequestParameters(request); - - ValueFactory vf = SimpleValueFactory.getInstance(); - - Resource subj = ProtocolUtil.parseResourceParam(request, SUBJECT_PARAM_NAME, vf); - IRI pred = ProtocolUtil.parseURIParam(request, PREDICATE_PARAM_NAME, vf); - Value obj = ProtocolUtil.parseValueParam(request, OBJECT_PARAM_NAME, vf); - Resource[] contexts = ProtocolUtil.parseContextParam(request, CONTEXT_PARAM_NAME, vf); - boolean useInferencing = ProtocolUtil.parseBooleanParam(request, INCLUDE_INFERRED_PARAM_NAME, true); - - RDFWriterFactory rdfWriterFactory = ProtocolUtil.getAcceptableService(request, response, - RDFWriterRegistry.getInstance()); - - Map model = new HashMap<>(); - model.put(TransactionExportStatementsView.SUBJECT_KEY, subj); - model.put(TransactionExportStatementsView.PREDICATE_KEY, pred); - model.put(TransactionExportStatementsView.OBJECT_KEY, obj); - model.put(TransactionExportStatementsView.CONTEXTS_KEY, contexts); - model.put(TransactionExportStatementsView.USE_INFERENCING_KEY, Boolean.valueOf(useInferencing)); - model.put(TransactionExportStatementsView.FACTORY_KEY, rdfWriterFactory); - model.put(TransactionExportStatementsView.HEADERS_ONLY, METHOD_HEAD.equals(request.getMethod())); - - model.put(TransactionExportStatementsView.TRANSACTION_KEY, transaction); - return new ModelAndView(TransactionExportStatementsView.getInstance(), model); - } - - /** - * Evaluates a query on the given connection and returns the resulting {@link QueryResultView}. The - * {@link QueryResultView} will take care of correctly releasing the connection back to the - * {@link ActiveTransactionRegistry}, after fully rendering the query result for sending over the wire. - */ - private ModelAndView processQuery(Transaction txn, HttpServletRequest request, HttpServletResponse response) - throws IOException, HTTPException { - String queryStr; - final String contentType = request.getContentType(); - if (contentType != null && contentType.contains(Protocol.SPARQL_QUERY_MIME_TYPE)) { - Charset charset = getCharset(request); - queryStr = IOUtils.toString(request.getInputStream(), charset); - } else { - queryStr = request.getParameter(QUERY_PARAM_NAME); - } - - View view; - Object queryResult; - FileFormatServiceRegistry registry; - - try { - Query query = getQuery(txn, queryStr, request, response); - - if (query instanceof TupleQuery) { - TupleQuery tQuery = (TupleQuery) query; - - queryResult = txn.evaluate(tQuery); - registry = TupleQueryResultWriterRegistry.getInstance(); - view = TupleQueryResultView.getInstance(); - } else if (query instanceof GraphQuery) { - GraphQuery gQuery = (GraphQuery) query; - - queryResult = txn.evaluate(gQuery); - registry = RDFWriterRegistry.getInstance(); - view = GraphQueryResultView.getInstance(); - } else if (query instanceof BooleanQuery) { - BooleanQuery bQuery = (BooleanQuery) query; - - queryResult = txn.evaluate(bQuery); - registry = BooleanQueryResultWriterRegistry.getInstance(); - view = BooleanQueryResultView.getInstance(); - } else { - throw new ClientHTTPException(SC_BAD_REQUEST, "Unsupported query type: " + query.getClass().getName()); - } - } catch (QueryInterruptedException | InterruptedException | ExecutionException e) { - if (e.getCause() != null && e.getCause() instanceof MalformedQueryException) { - ErrorInfo errInfo = new ErrorInfo(ErrorType.MALFORMED_QUERY, e.getCause().getMessage()); - throw new ClientHTTPException(SC_BAD_REQUEST, errInfo.toString()); - } else { - logger.info("Query interrupted", e); - throw new ServerHTTPException(SC_SERVICE_UNAVAILABLE, "Query execution interrupted"); - } - } catch (QueryEvaluationException e) { - logger.info("Query evaluation error", e); - if (e.getCause() != null && e.getCause() instanceof HTTPException) { - // custom signal from the backend, throw as HTTPException - // directly (see SES-1016). - throw (HTTPException) e.getCause(); - } else { - throw new ServerHTTPException("Query evaluation error: " + e.getMessage()); - } - } - Object factory = ProtocolUtil.getAcceptableService(request, response, registry); - - Map model = new HashMap<>(); - model.put(QueryResultView.FILENAME_HINT_KEY, "query-result"); - model.put(QueryResultView.QUERY_RESULT_KEY, queryResult); - model.put(QueryResultView.FACTORY_KEY, factory); - model.put(QueryResultView.HEADERS_ONLY, false); // TODO needed for HEAD - // requests. - return new ModelAndView(view, model); - } - - private static Charset getCharset(HttpServletRequest request) { - return request.getCharacterEncoding() != null ? Charset.forName(request.getCharacterEncoding()) - : StandardCharsets.UTF_8; - } - - private Query getQuery(Transaction txn, String queryStr, HttpServletRequest request, HttpServletResponse response) - throws IOException, ClientHTTPException, InterruptedException, ExecutionException { - Query result = null; - - // default query language is SPARQL - QueryLanguage queryLn = QueryLanguage.SPARQL; - - String queryLnStr = request.getParameter(QUERY_LANGUAGE_PARAM_NAME); - logger.debug("query language param = {}", queryLnStr); - - if (queryLnStr != null) { - queryLn = QueryLanguage.valueOf(queryLnStr); - - if (queryLn == null) { - throw new ClientHTTPException(SC_BAD_REQUEST, "Unknown query language: " + queryLnStr); - } - } - - String baseURI = request.getParameter(Protocol.BASEURI_PARAM_NAME); - - // determine if inferred triples should be included in query evaluation - boolean includeInferred = ProtocolUtil.parseBooleanParam(request, INCLUDE_INFERRED_PARAM_NAME, true); - - String timeout = request.getParameter(Protocol.TIMEOUT_PARAM_NAME); - int maxQueryTime = 0; - if (timeout != null) { - try { - maxQueryTime = Integer.parseInt(timeout); - } catch (NumberFormatException e) { - throw new ClientHTTPException(SC_BAD_REQUEST, "Invalid timeout value: " + timeout); - } - } - - // build a dataset, if specified - String[] defaultGraphURIs = request.getParameterValues(DEFAULT_GRAPH_PARAM_NAME); - String[] namedGraphURIs = request.getParameterValues(NAMED_GRAPH_PARAM_NAME); - - SimpleDataset dataset = null; - if (defaultGraphURIs != null || namedGraphURIs != null) { - dataset = new SimpleDataset(); - - if (defaultGraphURIs != null) { - for (String defaultGraphURI : defaultGraphURIs) { - try { - IRI uri = null; - if (!"null".equals(defaultGraphURI)) { - uri = SimpleValueFactory.getInstance().createIRI(defaultGraphURI); - } - dataset.addDefaultGraph(uri); - } catch (IllegalArgumentException e) { - throw new ClientHTTPException(SC_BAD_REQUEST, - "Illegal URI for default graph: " + defaultGraphURI); - } - } - } - - if (namedGraphURIs != null) { - for (String namedGraphURI : namedGraphURIs) { - try { - IRI uri = null; - if (!"null".equals(namedGraphURI)) { - uri = SimpleValueFactory.getInstance().createIRI(namedGraphURI); - } - dataset.addNamedGraph(uri); - } catch (IllegalArgumentException e) { - throw new ClientHTTPException(SC_BAD_REQUEST, "Illegal URI for named graph: " + namedGraphURI); - } - } - } - } - - try { - result = txn.prepareQuery(queryLn, queryStr, baseURI); - result.setIncludeInferred(includeInferred); - - if (maxQueryTime > 0) { - result.setMaxExecutionTime(maxQueryTime); - } - - if (dataset != null) { - result.setDataset(dataset); - } - - // determine if any variable bindings have been set on this query. - @SuppressWarnings("unchecked") - Enumeration parameterNames = request.getParameterNames(); - - while (parameterNames.hasMoreElements()) { - String parameterName = parameterNames.nextElement(); - - if (parameterName.startsWith(BINDING_PREFIX) && parameterName.length() > BINDING_PREFIX.length()) { - String bindingName = parameterName.substring(BINDING_PREFIX.length()); - Value bindingValue = ProtocolUtil.parseValueParam(request, parameterName, - SimpleValueFactory.getInstance()); - result.setBinding(bindingName, bindingValue); - } - } - } catch (UnsupportedQueryLanguageException e) { - ErrorInfo errInfo = new ErrorInfo(ErrorType.UNSUPPORTED_QUERY_LANGUAGE, queryLn.getName()); - throw new ClientHTTPException(SC_BAD_REQUEST, errInfo.toString()); - } catch (MalformedQueryException e) { - ErrorInfo errInfo = new ErrorInfo(ErrorType.MALFORMED_QUERY, e.getMessage()); - throw new ClientHTTPException(SC_BAD_REQUEST, errInfo.toString()); - } catch (RepositoryException e) { - logger.error("Repository error", e); - response.sendError(SC_INTERNAL_SERVER_ERROR); - } - - return result; - } - - private ModelAndView getSparqlUpdateResult(Transaction transaction, HttpServletRequest request, - HttpServletResponse response) throws ServerHTTPException, ClientHTTPException, HTTPException { - String sparqlUpdateString; - final String contentType = request.getContentType(); - if (contentType != null && contentType.contains(Protocol.SPARQL_UPDATE_MIME_TYPE)) { - try { - Charset charset = getCharset(request); - sparqlUpdateString = IOUtils.toString(request.getInputStream(), charset); - } catch (IOException e) { - logger.warn("error reading sparql update string from request body", e); - throw new ClientHTTPException(SC_BAD_REQUEST, - "could not read SPARQL update string from body: " + e.getMessage()); - } - } else { - sparqlUpdateString = request.getParameter(Protocol.UPDATE_PARAM_NAME); - } - - if (null == sparqlUpdateString) { - throw new ClientHTTPException(SC_NOT_ACCEPTABLE, "Could not read SPARQL update string from body."); - } - - logger.debug("SPARQL update string: {}", sparqlUpdateString); - - // default query language is SPARQL - QueryLanguage queryLn = QueryLanguage.SPARQL; - - String queryLnStr = request.getParameter(QUERY_LANGUAGE_PARAM_NAME); - logger.debug("query language param = {}", queryLnStr); - - if (queryLnStr != null) { - queryLn = QueryLanguage.valueOf(queryLnStr); - - if (queryLn == null) { - throw new ClientHTTPException(SC_BAD_REQUEST, "Unknown query language: " + queryLnStr); - } - } - - String baseURI = request.getParameter(Protocol.BASEURI_PARAM_NAME); - - // determine if inferred triples should be included in query evaluation - boolean includeInferred = ProtocolUtil.parseBooleanParam(request, INCLUDE_INFERRED_PARAM_NAME, true); - - // build a dataset, if specified - String[] defaultRemoveGraphURIs = request.getParameterValues(REMOVE_GRAPH_PARAM_NAME); - String[] defaultInsertGraphURIs = request.getParameterValues(INSERT_GRAPH_PARAM_NAME); - String[] defaultGraphURIs = request.getParameterValues(USING_GRAPH_PARAM_NAME); - String[] namedGraphURIs = request.getParameterValues(USING_NAMED_GRAPH_PARAM_NAME); - - SimpleDataset dataset = new SimpleDataset(); - - if (defaultRemoveGraphURIs != null) { - for (String graphURI : defaultRemoveGraphURIs) { - try { - IRI uri = null; - if (!"null".equals(graphURI)) { - uri = SimpleValueFactory.getInstance().createIRI(graphURI); - } - dataset.addDefaultRemoveGraph(uri); - } catch (IllegalArgumentException e) { - throw new ClientHTTPException(SC_BAD_REQUEST, "Illegal URI for default remove graph: " + graphURI); - } - } - } - - if (defaultInsertGraphURIs != null && defaultInsertGraphURIs.length > 0) { - String graphURI = defaultInsertGraphURIs[0]; - try { - IRI uri = null; - if (!"null".equals(graphURI)) { - uri = SimpleValueFactory.getInstance().createIRI(graphURI); - } - dataset.setDefaultInsertGraph(uri); - } catch (IllegalArgumentException e) { - throw new ClientHTTPException(SC_BAD_REQUEST, "Illegal URI for default insert graph: " + graphURI); - } - } - - if (defaultGraphURIs != null) { - for (String defaultGraphURI : defaultGraphURIs) { - try { - IRI uri = null; - if (!"null".equals(defaultGraphURI)) { - uri = SimpleValueFactory.getInstance().createIRI(defaultGraphURI); - } - dataset.addDefaultGraph(uri); - } catch (IllegalArgumentException e) { - throw new ClientHTTPException(SC_BAD_REQUEST, "Illegal URI for default graph: " + defaultGraphURI); - } - } - } - - if (namedGraphURIs != null) { - for (String namedGraphURI : namedGraphURIs) { - try { - IRI uri = null; - if (!"null".equals(namedGraphURI)) { - uri = SimpleValueFactory.getInstance().createIRI(namedGraphURI); - } - dataset.addNamedGraph(uri); - } catch (IllegalArgumentException e) { - throw new ClientHTTPException(SC_BAD_REQUEST, "Illegal URI for named graph: " + namedGraphURI); - } - } - } - - try { - // determine if any variable bindings have been set on this update. - @SuppressWarnings("unchecked") - Enumeration parameterNames = request.getParameterNames(); - - Map bindings = new HashMap<>(); - while (parameterNames.hasMoreElements()) { - String parameterName = parameterNames.nextElement(); - - if (parameterName.startsWith(BINDING_PREFIX) && parameterName.length() > BINDING_PREFIX.length()) { - String bindingName = parameterName.substring(BINDING_PREFIX.length()); - Value bindingValue = ProtocolUtil.parseValueParam(request, parameterName, - SimpleValueFactory.getInstance()); - bindings.put(bindingName, bindingValue); - } - } - - transaction.executeUpdate(queryLn, sparqlUpdateString, baseURI, includeInferred, dataset, bindings); - - return new ModelAndView(EmptySuccessView.getInstance()); - } catch (UpdateExecutionException | InterruptedException | ExecutionException | RepositoryException e) { - if (e.getCause() != null && e.getCause() instanceof HTTPException) { - // custom signal from the backend, throw as HTTPException directly - // (see SES-1016). - throw (HTTPException) e.getCause(); - } else { - throw new ServerHTTPException("Repository update error: " + e.getMessage(), e); - } - } - // custom signal from the backend, throw as HTTPException directly - // (see SES-1016). - catch (MalformedQueryException e) { - ErrorInfo errInfo = new ErrorInfo(ErrorType.MALFORMED_QUERY, e.getMessage()); - throw new ClientHTTPException(SC_BAD_REQUEST, errInfo.toString()); - } - } - // Comes from disposableBean interface so to be able to stop the ActiveTransactionRegistry scheduler @Override public void destroy() diff --git a/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/transaction/UpdateController.java b/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/transaction/UpdateController.java new file mode 100644 index 00000000000..75a0b192b71 --- /dev/null +++ b/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/transaction/UpdateController.java @@ -0,0 +1,207 @@ +/******************************************************************************* + * Copyright (c) 2023 Eclipse RDF4J contributors. + * + * 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.http.server.repository.transaction; + +import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; +import static javax.servlet.http.HttpServletResponse.SC_NOT_ACCEPTABLE; + +import static org.eclipse.rdf4j.http.protocol.Protocol.BINDING_PREFIX; +import static org.eclipse.rdf4j.http.protocol.Protocol.INCLUDE_INFERRED_PARAM_NAME; +import static org.eclipse.rdf4j.http.protocol.Protocol.INSERT_GRAPH_PARAM_NAME; +import static org.eclipse.rdf4j.http.protocol.Protocol.QUERY_LANGUAGE_PARAM_NAME; +import static org.eclipse.rdf4j.http.protocol.Protocol.REMOVE_GRAPH_PARAM_NAME; +import static org.eclipse.rdf4j.http.protocol.Protocol.USING_GRAPH_PARAM_NAME; +import static org.eclipse.rdf4j.http.protocol.Protocol.USING_NAMED_GRAPH_PARAM_NAME; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutionException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.io.IOUtils; +import org.eclipse.rdf4j.common.webapp.views.EmptySuccessView; +import org.eclipse.rdf4j.http.protocol.Protocol; +import org.eclipse.rdf4j.http.protocol.error.ErrorInfo; +import org.eclipse.rdf4j.http.protocol.error.ErrorType; +import org.eclipse.rdf4j.http.server.ClientHTTPException; +import org.eclipse.rdf4j.http.server.HTTPException; +import org.eclipse.rdf4j.http.server.ProtocolUtil; +import org.eclipse.rdf4j.http.server.ServerHTTPException; +import org.eclipse.rdf4j.model.IRI; +import org.eclipse.rdf4j.model.Value; +import org.eclipse.rdf4j.model.impl.SimpleValueFactory; +import org.eclipse.rdf4j.query.MalformedQueryException; +import org.eclipse.rdf4j.query.QueryLanguage; +import org.eclipse.rdf4j.query.UpdateExecutionException; +import org.eclipse.rdf4j.query.impl.SimpleDataset; +import org.eclipse.rdf4j.repository.RepositoryException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.servlet.ModelAndView; + +/** + * @author jeen + * + */ +public class UpdateController extends AbstractActionController { + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + @Override + protected ModelAndView handleAction(HttpServletRequest request, HttpServletResponse response, + Transaction transaction) + throws Exception { + + String sparqlUpdateString; + final String contentType = request.getContentType(); + if (contentType != null && contentType.contains(Protocol.SPARQL_UPDATE_MIME_TYPE)) { + try { + Charset charset = getCharset(request); + sparqlUpdateString = IOUtils.toString(request.getInputStream(), charset); + } catch (IOException e) { + logger.warn("error reading sparql update string from request body", e); + throw new ClientHTTPException(SC_BAD_REQUEST, + "could not read SPARQL update string from body: " + e.getMessage()); + } + } else { + sparqlUpdateString = request.getParameter(Protocol.UPDATE_PARAM_NAME); + } + + if (null == sparqlUpdateString) { + throw new ClientHTTPException(SC_NOT_ACCEPTABLE, "Could not read SPARQL update string from body."); + } + + logger.debug("SPARQL update string: {}", sparqlUpdateString); + + // default query language is SPARQL + QueryLanguage queryLn = QueryLanguage.SPARQL; + + String queryLnStr = request.getParameter(QUERY_LANGUAGE_PARAM_NAME); + logger.debug("query language param = {}", queryLnStr); + + if (queryLnStr != null) { + queryLn = QueryLanguage.valueOf(queryLnStr); + + if (queryLn == null) { + throw new ClientHTTPException(SC_BAD_REQUEST, "Unknown query language: " + queryLnStr); + } + } + + String baseURI = request.getParameter(Protocol.BASEURI_PARAM_NAME); + + // determine if inferred triples should be included in query evaluation + boolean includeInferred = ProtocolUtil.parseBooleanParam(request, INCLUDE_INFERRED_PARAM_NAME, true); + + // build a dataset, if specified + String[] defaultRemoveGraphURIs = request.getParameterValues(REMOVE_GRAPH_PARAM_NAME); + String[] defaultInsertGraphURIs = request.getParameterValues(INSERT_GRAPH_PARAM_NAME); + String[] defaultGraphURIs = request.getParameterValues(USING_GRAPH_PARAM_NAME); + String[] namedGraphURIs = request.getParameterValues(USING_NAMED_GRAPH_PARAM_NAME); + + SimpleDataset dataset = new SimpleDataset(); + + if (defaultRemoveGraphURIs != null) { + for (String graphURI : defaultRemoveGraphURIs) { + try { + IRI uri = null; + if (!"null".equals(graphURI)) { + uri = SimpleValueFactory.getInstance().createIRI(graphURI); + } + dataset.addDefaultRemoveGraph(uri); + } catch (IllegalArgumentException e) { + throw new ClientHTTPException(SC_BAD_REQUEST, "Illegal URI for default remove graph: " + graphURI); + } + } + } + + if (defaultInsertGraphURIs != null && defaultInsertGraphURIs.length > 0) { + String graphURI = defaultInsertGraphURIs[0]; + try { + IRI uri = null; + if (!"null".equals(graphURI)) { + uri = SimpleValueFactory.getInstance().createIRI(graphURI); + } + dataset.setDefaultInsertGraph(uri); + } catch (IllegalArgumentException e) { + throw new ClientHTTPException(SC_BAD_REQUEST, "Illegal URI for default insert graph: " + graphURI); + } + } + + if (defaultGraphURIs != null) { + for (String defaultGraphURI : defaultGraphURIs) { + try { + IRI uri = null; + if (!"null".equals(defaultGraphURI)) { + uri = SimpleValueFactory.getInstance().createIRI(defaultGraphURI); + } + dataset.addDefaultGraph(uri); + } catch (IllegalArgumentException e) { + throw new ClientHTTPException(SC_BAD_REQUEST, "Illegal URI for default graph: " + defaultGraphURI); + } + } + } + + if (namedGraphURIs != null) { + for (String namedGraphURI : namedGraphURIs) { + try { + IRI uri = null; + if (!"null".equals(namedGraphURI)) { + uri = SimpleValueFactory.getInstance().createIRI(namedGraphURI); + } + dataset.addNamedGraph(uri); + } catch (IllegalArgumentException e) { + throw new ClientHTTPException(SC_BAD_REQUEST, "Illegal URI for named graph: " + namedGraphURI); + } + } + } + + try { + // determine if any variable bindings have been set on this update. + Enumeration parameterNames = request.getParameterNames(); + + Map bindings = new HashMap<>(); + while (parameterNames.hasMoreElements()) { + String parameterName = parameterNames.nextElement(); + + if (parameterName.startsWith(BINDING_PREFIX) && parameterName.length() > BINDING_PREFIX.length()) { + String bindingName = parameterName.substring(BINDING_PREFIX.length()); + Value bindingValue = ProtocolUtil.parseValueParam(request, parameterName, + SimpleValueFactory.getInstance()); + bindings.put(bindingName, bindingValue); + } + } + + transaction.executeUpdate(queryLn, sparqlUpdateString, baseURI, includeInferred, dataset, bindings); + + return new ModelAndView(EmptySuccessView.getInstance()); + } catch (UpdateExecutionException | InterruptedException | ExecutionException | RepositoryException e) { + if (e.getCause() != null && e.getCause() instanceof HTTPException) { + // custom signal from the backend, throw as HTTPException directly + // (see SES-1016). + throw (HTTPException) e.getCause(); + } else { + throw new ServerHTTPException("Repository update error: " + e.getMessage(), e); + } + } + // custom signal from the backend, throw as HTTPException directly + // (see SES-1016). + catch (MalformedQueryException e) { + ErrorInfo errInfo = new ErrorInfo(ErrorType.MALFORMED_QUERY, e.getMessage()); + throw new ClientHTTPException(SC_BAD_REQUEST, errInfo.toString()); + } + } +} diff --git a/tools/server/src/main/resources/org/TransactionController.java b/tools/server/src/main/resources/org/TransactionController.java new file mode 100644 index 00000000000..fbe58ca6071 --- /dev/null +++ b/tools/server/src/main/resources/org/TransactionController.java @@ -0,0 +1,684 @@ +/******************************************************************************* + * 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.http.server.repository.transaction; + +import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; +import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; +import static javax.servlet.http.HttpServletResponse.SC_NOT_ACCEPTABLE; +import static javax.servlet.http.HttpServletResponse.SC_SERVICE_UNAVAILABLE; + +import static org.eclipse.rdf4j.http.protocol.Protocol.BINDING_PREFIX; +import static org.eclipse.rdf4j.http.protocol.Protocol.CONTEXT_PARAM_NAME; +import static org.eclipse.rdf4j.http.protocol.Protocol.DEFAULT_GRAPH_PARAM_NAME; +import static org.eclipse.rdf4j.http.protocol.Protocol.INCLUDE_INFERRED_PARAM_NAME; +import static org.eclipse.rdf4j.http.protocol.Protocol.INSERT_GRAPH_PARAM_NAME; +import static org.eclipse.rdf4j.http.protocol.Protocol.NAMED_GRAPH_PARAM_NAME; +import static org.eclipse.rdf4j.http.protocol.Protocol.OBJECT_PARAM_NAME; +import static org.eclipse.rdf4j.http.protocol.Protocol.PREDICATE_PARAM_NAME; +import static org.eclipse.rdf4j.http.protocol.Protocol.QUERY_LANGUAGE_PARAM_NAME; +import static org.eclipse.rdf4j.http.protocol.Protocol.QUERY_PARAM_NAME; +import static org.eclipse.rdf4j.http.protocol.Protocol.REMOVE_GRAPH_PARAM_NAME; +import static org.eclipse.rdf4j.http.protocol.Protocol.SUBJECT_PARAM_NAME; +import static org.eclipse.rdf4j.http.protocol.Protocol.USING_GRAPH_PARAM_NAME; +import static org.eclipse.rdf4j.http.protocol.Protocol.USING_NAMED_GRAPH_PARAM_NAME; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.io.IOUtils; +import org.eclipse.rdf4j.common.lang.FileFormat; +import org.eclipse.rdf4j.common.lang.service.FileFormatServiceRegistry; +import org.eclipse.rdf4j.common.webapp.views.EmptySuccessView; +import org.eclipse.rdf4j.common.webapp.views.SimpleResponseView; +import org.eclipse.rdf4j.http.protocol.Protocol; +import org.eclipse.rdf4j.http.protocol.Protocol.Action; +import org.eclipse.rdf4j.http.protocol.error.ErrorInfo; +import org.eclipse.rdf4j.http.protocol.error.ErrorType; +import org.eclipse.rdf4j.http.server.ClientHTTPException; +import org.eclipse.rdf4j.http.server.HTTPException; +import org.eclipse.rdf4j.http.server.ProtocolUtil; +import org.eclipse.rdf4j.http.server.ServerHTTPException; +import org.eclipse.rdf4j.http.server.repository.BooleanQueryResultView; +import org.eclipse.rdf4j.http.server.repository.GraphQueryResultView; +import org.eclipse.rdf4j.http.server.repository.QueryResultView; +import org.eclipse.rdf4j.http.server.repository.RepositoryInterceptor; +import org.eclipse.rdf4j.http.server.repository.TupleQueryResultView; +import org.eclipse.rdf4j.model.IRI; +import org.eclipse.rdf4j.model.Resource; +import org.eclipse.rdf4j.model.Value; +import org.eclipse.rdf4j.model.ValueFactory; +import org.eclipse.rdf4j.model.impl.SimpleValueFactory; +import org.eclipse.rdf4j.query.BooleanQuery; +import org.eclipse.rdf4j.query.GraphQuery; +import org.eclipse.rdf4j.query.MalformedQueryException; +import org.eclipse.rdf4j.query.Query; +import org.eclipse.rdf4j.query.QueryEvaluationException; +import org.eclipse.rdf4j.query.QueryInterruptedException; +import org.eclipse.rdf4j.query.QueryLanguage; +import org.eclipse.rdf4j.query.TupleQuery; +import org.eclipse.rdf4j.query.UnsupportedQueryLanguageException; +import org.eclipse.rdf4j.query.UpdateExecutionException; +import org.eclipse.rdf4j.query.impl.SimpleDataset; +import org.eclipse.rdf4j.query.resultio.BooleanQueryResultWriterRegistry; +import org.eclipse.rdf4j.query.resultio.TupleQueryResultWriterRegistry; +import org.eclipse.rdf4j.repository.Repository; +import org.eclipse.rdf4j.repository.RepositoryException; +import org.eclipse.rdf4j.rio.RDFFormat; +import org.eclipse.rdf4j.rio.RDFWriterFactory; +import org.eclipse.rdf4j.rio.RDFWriterRegistry; +import org.eclipse.rdf4j.rio.Rio; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.context.ApplicationContextException; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.View; +import org.springframework.web.servlet.mvc.AbstractController; + +/** + * Handles requests for transaction creation on a repository. + * + * @author Jeen Broekstra + */ +public class TransactionController extends AbstractController implements DisposableBean { + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + public TransactionController() throws ApplicationContextException { + setSupportedMethods(new String[] { METHOD_POST, "PUT", "DELETE" }); + } + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) + throws Exception { + ModelAndView result; + + String reqMethod = request.getMethod(); + UUID transactionId = getTransactionID(request); + logger.debug("transaction id: {}", transactionId); + logger.debug("request content type: {}", request.getContentType()); + + Transaction transaction = ActiveTransactionRegistry.INSTANCE.getTransaction(transactionId); + + if (transaction == null) { + logger.warn("could not find transaction for transaction id {}", transactionId); + throw new ClientHTTPException(SC_BAD_REQUEST, + "unable to find registered transaction for transaction id '" + transactionId + "'"); + } + + // if no action is specified in the request, it's a rollback (since it's + // the only txn operation that does not require the action parameter). + final String actionParam = request.getParameter(Protocol.ACTION_PARAM_NAME); + final Action action = actionParam != null ? Action.valueOf(actionParam) : Action.ROLLBACK; + switch (action) { + case QUERY: + // TODO SES-2238 note that we allow POST requests for backward + // compatibility reasons with earlier + // 2.8.x releases, even though according to the protocol spec only + // PUT is allowed. + if ("PUT".equals(reqMethod) || METHOD_POST.equals(reqMethod)) { + logger.info("{} txn query request", reqMethod); + result = processQuery(transaction, request, response); + logger.info("{} txn query request finished", reqMethod); + } else { + throw new ClientHTTPException(HttpServletResponse.SC_METHOD_NOT_ALLOWED, + "Method not allowed: " + reqMethod); + } + break; + case GET: + if ("PUT".equals(reqMethod) || METHOD_POST.equals(reqMethod)) { + logger.info("{} txn get/export statements request", reqMethod); + result = getExportStatementsResult(transaction, request, response); + logger.info("{} txn get/export statements request finished", reqMethod); + } else { + throw new ClientHTTPException(HttpServletResponse.SC_METHOD_NOT_ALLOWED, + "Method not allowed: " + reqMethod); + } + break; + case SIZE: + if ("PUT".equals(reqMethod) || METHOD_POST.equals(reqMethod)) { + logger.info("{} txn size request", reqMethod); + result = getSize(transaction, request, response); + logger.info("{} txn size request finished", reqMethod); + } else { + throw new ClientHTTPException(HttpServletResponse.SC_METHOD_NOT_ALLOWED, + "Method not allowed: " + reqMethod); + } + break; + case PING: + String text = Long.toString(ActiveTransactionRegistry.INSTANCE.getTimeout(TimeUnit.MILLISECONDS)); + Map model = Collections.singletonMap(SimpleResponseView.CONTENT_KEY, text); + result = new ModelAndView(SimpleResponseView.getInstance(), model); + break; + default: + // TODO Action.ROLLBACK check is for backward compatibility with + // older 2.8.x releases only. It's not in the protocol spec. + if ("DELETE".equals(reqMethod) + || (action.equals(Action.ROLLBACK) && ("PUT".equals(reqMethod) || METHOD_POST.equals(reqMethod)))) { + logger.info("transaction rollback"); + try { + transaction.rollback(); + } finally { + try { + transaction.close(); + } finally { + ActiveTransactionRegistry.INSTANCE.deregister(transaction); + } + } + result = new ModelAndView(EmptySuccessView.getInstance()); + logger.info("transaction rollback request finished."); + } else if ("PUT".equals(reqMethod) || METHOD_POST.equals(reqMethod)) { + // TODO filter for appropriate PUT operations + logger.info("{} txn operation", reqMethod); + result = processModificationOperation(transaction, action, request, response); + logger.info("PUT txn operation request finished."); + } else { + throw new ClientHTTPException(HttpServletResponse.SC_METHOD_NOT_ALLOWED, + "Method not allowed: " + reqMethod); + } + break; + } + if (!(transaction.isClosed() || transaction.isComplete())) { + ActiveTransactionRegistry.INSTANCE.active(transaction); + } + return result; + } + + private UUID getTransactionID(HttpServletRequest request) throws ClientHTTPException { + String pathInfoStr = request.getPathInfo(); + + UUID txnID = null; + + if (pathInfoStr != null && !pathInfoStr.equals("/")) { + String[] pathInfo = pathInfoStr.substring(1).split("/"); + // should be of the form: //transactions/ + if (pathInfo.length == 3) { + try { + txnID = UUID.fromString(pathInfo[2]); + logger.debug("txnID is '{}'", txnID); + } catch (IllegalArgumentException e) { + throw new ClientHTTPException(SC_BAD_REQUEST, "not a valid transaction id: " + pathInfo[2]); + } + } else { + logger.warn("could not determine transaction id from path info {} ", pathInfoStr); + } + } + + return txnID; + } + + private ModelAndView processModificationOperation(Transaction transaction, Action action, + HttpServletRequest request, HttpServletResponse response) throws IOException, HTTPException { + ProtocolUtil.logRequestParameters(request); + + Map model = new HashMap<>(); + + String baseURI = request.getParameter(Protocol.BASEURI_PARAM_NAME); + if (baseURI == null) { + baseURI = ""; + } + + final Resource[] contexts = ProtocolUtil.parseContextParam(request, CONTEXT_PARAM_NAME, + SimpleValueFactory.getInstance()); + + final boolean preserveNodeIds = ProtocolUtil.parseBooleanParam(request, Protocol.PRESERVE_BNODE_ID_PARAM_NAME, + false); + + try { + RDFFormat format; + switch (action) { + case ADD: + format = Rio.getParserFormatForMIMEType(request.getContentType()) + .orElseThrow(Rio.unsupportedFormat(request.getContentType())); + transaction.add(request.getInputStream(), baseURI, format, preserveNodeIds, contexts); + break; + case DELETE: + format = Rio.getParserFormatForMIMEType(request.getContentType()) + .orElseThrow(Rio.unsupportedFormat(request.getContentType())); + transaction.delete(format, request.getInputStream(), baseURI); + break; + case UPDATE: + return getSparqlUpdateResult(transaction, request, response); + case PREPARE: + transaction.prepare(); + break; + case COMMIT: + transaction.commit(); + // If commit fails with an exception, deregister should be skipped so the user + // has a chance to do a proper rollback. See #725. + ActiveTransactionRegistry.INSTANCE.deregister(transaction); + break; + default: + logger.warn("transaction modification action '{}' not recognized", action); + throw new ClientHTTPException("modification action not recognized: " + action); + } + + model.put(SimpleResponseView.SC_KEY, HttpServletResponse.SC_OK); + return new ModelAndView(SimpleResponseView.getInstance(), model); + } catch (Exception e) { + if (e instanceof ClientHTTPException) { + throw (ClientHTTPException) e; + } else { + throw new ServerHTTPException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, + "Transaction handling error: " + e.getMessage(), e); + } + } + } + + private ModelAndView getSize(Transaction transaction, HttpServletRequest request, HttpServletResponse response) + throws HTTPException { + ProtocolUtil.logRequestParameters(request); + + Map model = new HashMap<>(); + final boolean headersOnly = METHOD_HEAD.equals(request.getMethod()); + + if (!headersOnly) { + Repository repository = RepositoryInterceptor.getRepository(request); + + ValueFactory vf = repository.getValueFactory(); + Resource[] contexts = ProtocolUtil.parseContextParam(request, Protocol.CONTEXT_PARAM_NAME, vf); + + long size; + + try { + size = transaction.getSize(contexts); + } catch (RepositoryException | InterruptedException | ExecutionException e) { + throw new ServerHTTPException("Repository error: " + e.getMessage(), e); + } + model.put(SimpleResponseView.CONTENT_KEY, String.valueOf(size)); + } + + return new ModelAndView(SimpleResponseView.getInstance(), model); + } + + /** + * Get all statements and export them as RDF. + * + * @return a model and view for exporting the statements. + */ + private ModelAndView getExportStatementsResult(Transaction transaction, HttpServletRequest request, + HttpServletResponse response) throws ClientHTTPException { + ProtocolUtil.logRequestParameters(request); + + ValueFactory vf = SimpleValueFactory.getInstance(); + + Resource subj = ProtocolUtil.parseResourceParam(request, SUBJECT_PARAM_NAME, vf); + IRI pred = ProtocolUtil.parseURIParam(request, PREDICATE_PARAM_NAME, vf); + Value obj = ProtocolUtil.parseValueParam(request, OBJECT_PARAM_NAME, vf); + Resource[] contexts = ProtocolUtil.parseContextParam(request, CONTEXT_PARAM_NAME, vf); + boolean useInferencing = ProtocolUtil.parseBooleanParam(request, INCLUDE_INFERRED_PARAM_NAME, true); + + RDFWriterFactory rdfWriterFactory = ProtocolUtil.getAcceptableService(request, response, + RDFWriterRegistry.getInstance()); + + Map model = new HashMap<>(); + model.put(TransactionExportStatementsView.SUBJECT_KEY, subj); + model.put(TransactionExportStatementsView.PREDICATE_KEY, pred); + model.put(TransactionExportStatementsView.OBJECT_KEY, obj); + model.put(TransactionExportStatementsView.CONTEXTS_KEY, contexts); + model.put(TransactionExportStatementsView.USE_INFERENCING_KEY, Boolean.valueOf(useInferencing)); + model.put(TransactionExportStatementsView.FACTORY_KEY, rdfWriterFactory); + model.put(TransactionExportStatementsView.HEADERS_ONLY, METHOD_HEAD.equals(request.getMethod())); + + model.put(TransactionExportStatementsView.TRANSACTION_KEY, transaction); + return new ModelAndView(TransactionExportStatementsView.getInstance(), model); + } + + /** + * Evaluates a query on the given connection and returns the resulting {@link QueryResultView}. The + * {@link QueryResultView} will take care of correctly releasing the connection back to the + * {@link ActiveTransactionRegistry}, after fully rendering the query result for sending over the wire. + */ + private ModelAndView processQuery(Transaction txn, HttpServletRequest request, HttpServletResponse response) + throws IOException, HTTPException { + String queryStr; + final String contentType = request.getContentType(); + if (contentType != null && contentType.contains(Protocol.SPARQL_QUERY_MIME_TYPE)) { + Charset charset = getCharset(request); + queryStr = IOUtils.toString(request.getInputStream(), charset); + } else { + queryStr = request.getParameter(QUERY_PARAM_NAME); + } + + View view; + Object queryResult; + FileFormatServiceRegistry registry; + + try { + Query query = getQuery(txn, queryStr, request, response); + + if (query instanceof TupleQuery) { + TupleQuery tQuery = (TupleQuery) query; + + queryResult = txn.evaluate(tQuery); + registry = TupleQueryResultWriterRegistry.getInstance(); + view = TupleQueryResultView.getInstance(); + } else if (query instanceof GraphQuery) { + GraphQuery gQuery = (GraphQuery) query; + + queryResult = txn.evaluate(gQuery); + registry = RDFWriterRegistry.getInstance(); + view = GraphQueryResultView.getInstance(); + } else if (query instanceof BooleanQuery) { + BooleanQuery bQuery = (BooleanQuery) query; + + queryResult = txn.evaluate(bQuery); + registry = BooleanQueryResultWriterRegistry.getInstance(); + view = BooleanQueryResultView.getInstance(); + } else { + throw new ClientHTTPException(SC_BAD_REQUEST, "Unsupported query type: " + query.getClass().getName()); + } + } catch (QueryInterruptedException | InterruptedException | ExecutionException e) { + if (e.getCause() != null && e.getCause() instanceof MalformedQueryException) { + ErrorInfo errInfo = new ErrorInfo(ErrorType.MALFORMED_QUERY, e.getCause().getMessage()); + throw new ClientHTTPException(SC_BAD_REQUEST, errInfo.toString()); + } else { + logger.info("Query interrupted", e); + throw new ServerHTTPException(SC_SERVICE_UNAVAILABLE, "Query execution interrupted"); + } + } catch (QueryEvaluationException e) { + logger.info("Query evaluation error", e); + if (e.getCause() != null && e.getCause() instanceof HTTPException) { + // custom signal from the backend, throw as HTTPException + // directly (see SES-1016). + throw (HTTPException) e.getCause(); + } else { + throw new ServerHTTPException("Query evaluation error: " + e.getMessage()); + } + } + Object factory = ProtocolUtil.getAcceptableService(request, response, registry); + + Map model = new HashMap<>(); + model.put(QueryResultView.FILENAME_HINT_KEY, "query-result"); + model.put(QueryResultView.QUERY_RESULT_KEY, queryResult); + model.put(QueryResultView.FACTORY_KEY, factory); + model.put(QueryResultView.HEADERS_ONLY, false); // TODO needed for HEAD + // requests. + return new ModelAndView(view, model); + } + + private static Charset getCharset(HttpServletRequest request) { + return request.getCharacterEncoding() != null ? Charset.forName(request.getCharacterEncoding()) + : StandardCharsets.UTF_8; + } + + private Query getQuery(Transaction txn, String queryStr, HttpServletRequest request, HttpServletResponse response) + throws IOException, ClientHTTPException, InterruptedException, ExecutionException { + Query result = null; + + // default query language is SPARQL + QueryLanguage queryLn = QueryLanguage.SPARQL; + + String queryLnStr = request.getParameter(QUERY_LANGUAGE_PARAM_NAME); + logger.debug("query language param = {}", queryLnStr); + + if (queryLnStr != null) { + queryLn = QueryLanguage.valueOf(queryLnStr); + + if (queryLn == null) { + throw new ClientHTTPException(SC_BAD_REQUEST, "Unknown query language: " + queryLnStr); + } + } + + String baseURI = request.getParameter(Protocol.BASEURI_PARAM_NAME); + + // determine if inferred triples should be included in query evaluation + boolean includeInferred = ProtocolUtil.parseBooleanParam(request, INCLUDE_INFERRED_PARAM_NAME, true); + + String timeout = request.getParameter(Protocol.TIMEOUT_PARAM_NAME); + int maxQueryTime = 0; + if (timeout != null) { + try { + maxQueryTime = Integer.parseInt(timeout); + } catch (NumberFormatException e) { + throw new ClientHTTPException(SC_BAD_REQUEST, "Invalid timeout value: " + timeout); + } + } + + // build a dataset, if specified + String[] defaultGraphURIs = request.getParameterValues(DEFAULT_GRAPH_PARAM_NAME); + String[] namedGraphURIs = request.getParameterValues(NAMED_GRAPH_PARAM_NAME); + + SimpleDataset dataset = null; + if (defaultGraphURIs != null || namedGraphURIs != null) { + dataset = new SimpleDataset(); + + if (defaultGraphURIs != null) { + for (String defaultGraphURI : defaultGraphURIs) { + try { + IRI uri = null; + if (!"null".equals(defaultGraphURI)) { + uri = SimpleValueFactory.getInstance().createIRI(defaultGraphURI); + } + dataset.addDefaultGraph(uri); + } catch (IllegalArgumentException e) { + throw new ClientHTTPException(SC_BAD_REQUEST, + "Illegal URI for default graph: " + defaultGraphURI); + } + } + } + + if (namedGraphURIs != null) { + for (String namedGraphURI : namedGraphURIs) { + try { + IRI uri = null; + if (!"null".equals(namedGraphURI)) { + uri = SimpleValueFactory.getInstance().createIRI(namedGraphURI); + } + dataset.addNamedGraph(uri); + } catch (IllegalArgumentException e) { + throw new ClientHTTPException(SC_BAD_REQUEST, "Illegal URI for named graph: " + namedGraphURI); + } + } + } + } + + try { + result = txn.prepareQuery(queryLn, queryStr, baseURI); + result.setIncludeInferred(includeInferred); + + if (maxQueryTime > 0) { + result.setMaxExecutionTime(maxQueryTime); + } + + if (dataset != null) { + result.setDataset(dataset); + } + + // determine if any variable bindings have been set on this query. + @SuppressWarnings("unchecked") + Enumeration parameterNames = request.getParameterNames(); + + while (parameterNames.hasMoreElements()) { + String parameterName = parameterNames.nextElement(); + + if (parameterName.startsWith(BINDING_PREFIX) && parameterName.length() > BINDING_PREFIX.length()) { + String bindingName = parameterName.substring(BINDING_PREFIX.length()); + Value bindingValue = ProtocolUtil.parseValueParam(request, parameterName, + SimpleValueFactory.getInstance()); + result.setBinding(bindingName, bindingValue); + } + } + } catch (UnsupportedQueryLanguageException e) { + ErrorInfo errInfo = new ErrorInfo(ErrorType.UNSUPPORTED_QUERY_LANGUAGE, queryLn.getName()); + throw new ClientHTTPException(SC_BAD_REQUEST, errInfo.toString()); + } catch (MalformedQueryException e) { + ErrorInfo errInfo = new ErrorInfo(ErrorType.MALFORMED_QUERY, e.getMessage()); + throw new ClientHTTPException(SC_BAD_REQUEST, errInfo.toString()); + } catch (RepositoryException e) { + logger.error("Repository error", e); + response.sendError(SC_INTERNAL_SERVER_ERROR); + } + + return result; + } + + private ModelAndView getSparqlUpdateResult(Transaction transaction, HttpServletRequest request, + HttpServletResponse response) throws ServerHTTPException, ClientHTTPException, HTTPException { + String sparqlUpdateString; + final String contentType = request.getContentType(); + if (contentType != null && contentType.contains(Protocol.SPARQL_UPDATE_MIME_TYPE)) { + try { + Charset charset = getCharset(request); + sparqlUpdateString = IOUtils.toString(request.getInputStream(), charset); + } catch (IOException e) { + logger.warn("error reading sparql update string from request body", e); + throw new ClientHTTPException(SC_BAD_REQUEST, + "could not read SPARQL update string from body: " + e.getMessage()); + } + } else { + sparqlUpdateString = request.getParameter(Protocol.UPDATE_PARAM_NAME); + } + + if (null == sparqlUpdateString) { + throw new ClientHTTPException(SC_NOT_ACCEPTABLE, "Could not read SPARQL update string from body."); + } + + logger.debug("SPARQL update string: {}", sparqlUpdateString); + + // default query language is SPARQL + QueryLanguage queryLn = QueryLanguage.SPARQL; + + String queryLnStr = request.getParameter(QUERY_LANGUAGE_PARAM_NAME); + logger.debug("query language param = {}", queryLnStr); + + if (queryLnStr != null) { + queryLn = QueryLanguage.valueOf(queryLnStr); + + if (queryLn == null) { + throw new ClientHTTPException(SC_BAD_REQUEST, "Unknown query language: " + queryLnStr); + } + } + + String baseURI = request.getParameter(Protocol.BASEURI_PARAM_NAME); + + // determine if inferred triples should be included in query evaluation + boolean includeInferred = ProtocolUtil.parseBooleanParam(request, INCLUDE_INFERRED_PARAM_NAME, true); + + // build a dataset, if specified + String[] defaultRemoveGraphURIs = request.getParameterValues(REMOVE_GRAPH_PARAM_NAME); + String[] defaultInsertGraphURIs = request.getParameterValues(INSERT_GRAPH_PARAM_NAME); + String[] defaultGraphURIs = request.getParameterValues(USING_GRAPH_PARAM_NAME); + String[] namedGraphURIs = request.getParameterValues(USING_NAMED_GRAPH_PARAM_NAME); + + SimpleDataset dataset = new SimpleDataset(); + + if (defaultRemoveGraphURIs != null) { + for (String graphURI : defaultRemoveGraphURIs) { + try { + IRI uri = null; + if (!"null".equals(graphURI)) { + uri = SimpleValueFactory.getInstance().createIRI(graphURI); + } + dataset.addDefaultRemoveGraph(uri); + } catch (IllegalArgumentException e) { + throw new ClientHTTPException(SC_BAD_REQUEST, "Illegal URI for default remove graph: " + graphURI); + } + } + } + + if (defaultInsertGraphURIs != null && defaultInsertGraphURIs.length > 0) { + String graphURI = defaultInsertGraphURIs[0]; + try { + IRI uri = null; + if (!"null".equals(graphURI)) { + uri = SimpleValueFactory.getInstance().createIRI(graphURI); + } + dataset.setDefaultInsertGraph(uri); + } catch (IllegalArgumentException e) { + throw new ClientHTTPException(SC_BAD_REQUEST, "Illegal URI for default insert graph: " + graphURI); + } + } + + if (defaultGraphURIs != null) { + for (String defaultGraphURI : defaultGraphURIs) { + try { + IRI uri = null; + if (!"null".equals(defaultGraphURI)) { + uri = SimpleValueFactory.getInstance().createIRI(defaultGraphURI); + } + dataset.addDefaultGraph(uri); + } catch (IllegalArgumentException e) { + throw new ClientHTTPException(SC_BAD_REQUEST, "Illegal URI for default graph: " + defaultGraphURI); + } + } + } + + if (namedGraphURIs != null) { + for (String namedGraphURI : namedGraphURIs) { + try { + IRI uri = null; + if (!"null".equals(namedGraphURI)) { + uri = SimpleValueFactory.getInstance().createIRI(namedGraphURI); + } + dataset.addNamedGraph(uri); + } catch (IllegalArgumentException e) { + throw new ClientHTTPException(SC_BAD_REQUEST, "Illegal URI for named graph: " + namedGraphURI); + } + } + } + + try { + // determine if any variable bindings have been set on this update. + @SuppressWarnings("unchecked") + Enumeration parameterNames = request.getParameterNames(); + + Map bindings = new HashMap<>(); + while (parameterNames.hasMoreElements()) { + String parameterName = parameterNames.nextElement(); + + if (parameterName.startsWith(BINDING_PREFIX) && parameterName.length() > BINDING_PREFIX.length()) { + String bindingName = parameterName.substring(BINDING_PREFIX.length()); + Value bindingValue = ProtocolUtil.parseValueParam(request, parameterName, + SimpleValueFactory.getInstance()); + bindings.put(bindingName, bindingValue); + } + } + + transaction.executeUpdate(queryLn, sparqlUpdateString, baseURI, includeInferred, dataset, bindings); + + return new ModelAndView(EmptySuccessView.getInstance()); + } catch (UpdateExecutionException | InterruptedException | ExecutionException | RepositoryException e) { + if (e.getCause() != null && e.getCause() instanceof HTTPException) { + // custom signal from the backend, throw as HTTPException directly + // (see SES-1016). + throw (HTTPException) e.getCause(); + } else { + throw new ServerHTTPException("Repository update error: " + e.getMessage(), e); + } + } + // custom signal from the backend, throw as HTTPException directly + // (see SES-1016). + catch (MalformedQueryException e) { + ErrorInfo errInfo = new ErrorInfo(ErrorType.MALFORMED_QUERY, e.getMessage()); + throw new ClientHTTPException(SC_BAD_REQUEST, errInfo.toString()); + } + } + + // Comes from disposableBean interface so to be able to stop the ActiveTransactionRegistry scheduler + @Override + public void destroy() + throws Exception { + ActiveTransactionRegistry.INSTANCE.destroyScheduler(); + } + +} diff --git a/tools/server/src/main/webapp/WEB-INF/rdf4j-http-server-servlet.xml b/tools/server/src/main/webapp/WEB-INF/rdf4j-http-server-servlet.xml index 2407de03367..3c769426d65 100644 --- a/tools/server/src/main/webapp/WEB-INF/rdf4j-http-server-servlet.xml +++ b/tools/server/src/main/webapp/WEB-INF/rdf4j-http-server-servlet.xml @@ -81,6 +81,16 @@ rdf4jRepositoryGraphController rdf4jRepositorySizeController rdf4jRepositoryTransactionStartController + rdf4jTransactionAddController + rdf4jTransactionCommitController + rdf4jTransactionDeleteController + rdf4jTransactionExportController + rdf4jTransactionPingController + rdf4jTransactionPrepareController + rdf4jTransactionQueryController + rdf4jTransactionRollbackController + rdf4jTransactionSizeController + rdf4jTransactionUpdateController rdf4jRepositoryTransactionController rdf4jRepositoryController @@ -126,8 +136,18 @@ - + + + + + + + + + + +