Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GH-4492 new transaction endpoints #4519

Draft
wants to merge 3 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -121,6 +122,8 @@ public class RDF4JProtocolSession extends SPARQLProtocolSession {

private long pingDelay = PINGDELAY;

private int serverProtocolVersion = 0;

/**
* @deprecated Use {@link #RDF4JProtocolSession(HttpClient, ExecutorService)} instead
*/
Expand Down Expand Up @@ -165,6 +168,7 @@ public void setServerURL(String serverURL) {
}

this.serverURL = serverURL;
this.serverProtocolVersion = 0; // side effect
}

public String getServerURL() {
Expand Down Expand Up @@ -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 *
*-------------------------*/
Expand All @@ -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++) {
Expand Down Expand Up @@ -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));
Expand All @@ -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);
Expand Down Expand Up @@ -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();

Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1217,4 +1249,9 @@ private <T extends HttpUriRequest> T applyAdditionalHeaders(T method) {
}
return method;
}

private boolean useDeprecatedTxnActions()
throws UnauthorizedException, NumberFormatException, RepositoryException, IOException {
return Integer.parseInt(getServerProtocol()) < 14;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,13 +104,15 @@ public enum TIMEOUT {
* Protocol version.
*
* <ul>
* <li>14: since RDF4J 4.3.0</li>
* <li>13: since RDF4J 4.0.0</li>
* <li>12: since RDF4J 3.5.0</li>
* <li>11: since RDF4J 3.3.0</li>
* <li>10: since RDF4J 3.1.0</li>
* <li>9: since RDF4J 3.0.0</li>
* </ul>
*/
public static final String VERSION = "12";
public static final String VERSION = "14";

/**
* Parameter name for the 'subject' parameter of a statement query.
Expand Down
37 changes: 37 additions & 0 deletions site/content/documentation/reference/rest-api/changelog.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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/
Expand Down
Loading