Skip to content

Commit

Permalink
Transaction Support (#67)
Browse files Browse the repository at this point in the history
* Add startTransaction for creating transactions

* Add transaction header constants

* Add method to retrieve transaction uri

* Add method to add Atomic-Id header for transactions

* Use transaction endpoint constant

* Override addTransaction for all builders

* Initial TransactionBuilder idea

* Checkstyle updates

* Create RequestBuilders per transaction endpoint

* Move start and TRANSACTION_ENDPOINT to TransactionBuilder

* Add uri validation methods

* Add simple tests for tx uri validation

* Split start valid into separate tests

* Use regex from fcrepo for validation help on tx ids

* Create TransactionURI for more strict api definitions

* Update ATOMIC_ID constant

* Try to retrieve the Atomic-ID from a response when getting the transaction uri

* Add TransactionalFcrepoClient for automatically adding Atomic-IDs to requests

* Add builder option for a TransactionFcrepoClient

* Create a container to test the transactional client

* Use DateTimeFormatter when checking all Atomic-Expires headers

* Add a method for creating transaction clients from a fcrepo client

Updates the FcrepoClient constructor to take a HttpClientBuilder in order to construct new clients

* Extend BodyRequestBuilder to allow for removal of addTransaction

* Make addTransaction protected to hide it in unused builders

* Add function to start a tx and create a client

* Test for startTransactionalClient

* Drop Optional from getTransactionUri

* Add transaction helpers to client

* Move startTransactionClient to FcrepoClient

* Update tests to be only through TransactionalFcrepoClient

* Create RequestBuilders for tx endpoints in TransactionalFcrepoClient

* Update import for TRANSACTION_ENDPOINT

* Remove unused classes

* Shorten transaction methods

* Keep functionality for base or transaction endpoints

* Add test creating a client using the full transaction endpoint

* Drop need for TransactionURI

* Use Get/Post/etc builders for the transaction api
  • Loading branch information
mikejritter committed Nov 3, 2022
1 parent 641b4ca commit 3393573
Show file tree
Hide file tree
Showing 15 changed files with 515 additions and 3 deletions.
6 changes: 6 additions & 0 deletions src/main/java/org/fcrepo/client/DeleteBuilder.java
Expand Up @@ -35,4 +35,10 @@ protected HttpRequestBase createRequest() {
public DeleteBuilder addHeader(final String name, final String value) {
return (DeleteBuilder) super.addHeader(name, value);
}

@Override
public DeleteBuilder addTransaction(final URI transaction) {
return (DeleteBuilder) super.addTransaction(transaction);
}

}
63 changes: 61 additions & 2 deletions src/main/java/org/fcrepo/client/FcrepoClient.java
Expand Up @@ -16,6 +16,7 @@
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Pattern;

import org.apache.http.Header;
import org.apache.http.HttpEntity;
Expand Down Expand Up @@ -48,9 +49,12 @@
public class FcrepoClient implements Closeable {

private CloseableHttpClient httpclient;
private FcrepoHttpClientBuilder httpClientBuilder;

private Boolean throwExceptionOnFailure = true;

public static final String TRANSACTION_ENDPOINT = "fcr:tx";

private static final Logger LOGGER = getLogger(FcrepoClient.class);

/**
Expand All @@ -72,7 +76,19 @@ public static FcrepoClientBuilder client() {
*/
protected FcrepoClient(final String username, final String password, final String host,
final Boolean throwExceptionOnFailure) {
this(new FcrepoHttpClientBuilder(username, password, host).build(), throwExceptionOnFailure);
this(new FcrepoHttpClientBuilder(username, password, host), throwExceptionOnFailure);
}

/**
* Create a FcrepoClient which uses the given {@link FcrepoHttpClientBuilder} to manage its http client.
* FcrepoClient will close the httpClient when {@link #close()} is called.
* @param httpClientBuilder http client builder to use to connect to the repository
* @param throwExceptionOnFailure whether to throw an exception on any non-2xx or 3xx HTTP responses
*/
protected FcrepoClient(final FcrepoHttpClientBuilder httpClientBuilder, final Boolean throwExceptionOnFailure) {
this.throwExceptionOnFailure = throwExceptionOnFailure;
this.httpclient = httpClientBuilder.build();
this.httpClientBuilder = httpClientBuilder;
}

/**
Expand Down Expand Up @@ -152,6 +168,49 @@ public HistoricMementoBuilder createMemento(final URI url, final String mementoD
return new HistoricMementoBuilder(url, this, mementoDatetime);
}

/**
* Start a transaction and create a new {@link TransactionalFcrepoClient}
*
* @param uri the base rest endpoint or the transaction endpoint
* @return the TransactionalFcrepoClient
* @throws IOException if there's an error with the http request
* @throws IllegalArgumentException if the uri is not the Fedora transaction endpoint
* @throws FcrepoOperationFailedException if there's an error in the fcrepo operation
*/
public TransactionalFcrepoClient startTransactionClient(final URI uri)
throws IOException, FcrepoOperationFailedException {
final var target = getTxEndpoint(uri);
try (final var response = post(target).perform()) {
return transactionalClient(response);
}
}

private URI getTxEndpoint(final URI uri) {
final var isRoot = Pattern.compile("rest/?$").asPredicate();
final var isTx = Pattern.compile("rest/" + TRANSACTION_ENDPOINT + "/?$").asPredicate();
final var base = uri.toString();
if (isRoot.test(base)) {
LOGGER.debug("Start transaction request matches root, appending {}", TRANSACTION_ENDPOINT);
// preface with ./ so fcr:tx isn't interpreted as a scheme
return uri.resolve("./" + TRANSACTION_ENDPOINT);
} else if (isTx.test(base)) {
return uri;
} else {
throw new IllegalArgumentException("Uri is not the base rest endpoint or the transaction endpoint");
}
}

/**
* Create a new {@link TransactionalFcrepoClient} which adds the transaction {@link URI} to each request
*
* @param response the FcrepoResponse with an Atomic-ID Header
* @return a TransactionFcrepoClient
* @throws IllegalArgumentException if the FcrepoResponse does not contain a transaction location
*/
public TransactionalFcrepoClient transactionalClient(final FcrepoResponse response) {
return new TransactionalFcrepoClient(response.getTransactionUri(), httpClientBuilder, throwExceptionOnFailure);
}

/**
* Make a DELETE request to delete a resource
*
Expand Down Expand Up @@ -354,7 +413,7 @@ public FcrepoClientBuilder throwExceptionOnFailure() {
*/
public FcrepoClient build() {
final FcrepoHttpClientBuilder httpClient = new FcrepoHttpClientBuilder(authUser, authPassword, authHost);
return new FcrepoClient(httpClient.build(), throwExceptionOnFailure);
return new FcrepoClient(httpClient, throwExceptionOnFailure);
}
}
}
27 changes: 27 additions & 0 deletions src/main/java/org/fcrepo/client/FcrepoResponse.java
Expand Up @@ -7,6 +7,8 @@

import static java.util.Collections.emptyList;
import static java.util.stream.Collectors.toList;
import static org.fcrepo.client.FcrepoClient.TRANSACTION_ENDPOINT;
import static org.fcrepo.client.FedoraHeaderConstants.ATOMIC_ID;
import static org.fcrepo.client.FedoraHeaderConstants.CONTENT_DISPOSITION;
import static org.fcrepo.client.FedoraHeaderConstants.CONTENT_TYPE;
import static org.fcrepo.client.FedoraHeaderConstants.LINK;
Expand All @@ -20,6 +22,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.http.HeaderElement;
import org.apache.http.NameValuePair;
import org.apache.http.message.BasicHeader;
Expand Down Expand Up @@ -56,6 +59,8 @@ public class FcrepoResponse implements Closeable {

private InputStream body;

private URI transactionUri;

private String contentType;

private boolean closed = false;
Expand Down Expand Up @@ -313,4 +318,26 @@ public Map<String, String> getContentDisposition() {
}
return contentDisposition;
}

/**
* Get the transaction location. If the location is not for a transaction, check for the Atomic-ID,
* otherwise return null.
*
* @return the transaction location or null
*/
public URI getTransactionUri() {
if (transactionUri == null) {
final var location = getHeaderValue(LOCATION);
final var atomicId = getHeaderValue(ATOMIC_ID);

if (location != null && location.contains(TRANSACTION_ENDPOINT)) {
transactionUri = URI.create(location);
} else if (atomicId != null) {
transactionUri = URI.create(atomicId);
}
}

return transactionUri;
}

}
4 changes: 4 additions & 0 deletions src/main/java/org/fcrepo/client/FedoraHeaderConstants.java
Expand Up @@ -67,6 +67,10 @@ public class FedoraHeaderConstants {

public static final String ACCEPT_DATETIME = "Accept-Datetime";

public static final String ATOMIC_ID = "Atomic-ID";

public static final String ATOMIC_EXPIRES = "Atomic-Expires";

private FedoraHeaderConstants() {
}
}
5 changes: 5 additions & 0 deletions src/main/java/org/fcrepo/client/GetBuilder.java
Expand Up @@ -182,4 +182,9 @@ public GetBuilder addHeader(final String name, final String value) {
public GetBuilder addLinkHeader(final FcrepoLink linkHeader) {
return (GetBuilder) super.addLinkHeader(linkHeader);
}

@Override
public GetBuilder addTransaction(final URI transaction) {
return (GetBuilder) super.addTransaction(transaction);
}
}
5 changes: 5 additions & 0 deletions src/main/java/org/fcrepo/client/HeadBuilder.java
Expand Up @@ -67,4 +67,9 @@ public HeadBuilder addHeader(final String name, final String value) {
public HeadBuilder addLinkHeader(final FcrepoLink linkHeader) {
return (HeadBuilder) super.addLinkHeader(linkHeader);
}

@Override
public HeadBuilder addTransaction(final URI transaction) {
return (HeadBuilder) super.addTransaction(transaction);
}
}
107 changes: 106 additions & 1 deletion src/main/java/org/fcrepo/client/HistoricMementoBuilder.java
Expand Up @@ -5,19 +5,27 @@
*/
package org.fcrepo.client;

import static org.fcrepo.client.FedoraHeaderConstants.CONTENT_DISPOSITION;
import static org.fcrepo.client.FedoraHeaderConstants.SLUG;
import static org.fcrepo.client.HeaderHelpers.UTC_RFC_1123_FORMATTER;
import static org.fcrepo.client.FedoraHeaderConstants.MEMENTO_DATETIME;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.time.Instant;

import org.apache.http.client.methods.HttpRequestBase;
import org.springframework.http.ContentDisposition;

/**
* Builds a POST request for creating a memento (LDPRm) with the state given in the request body
* and the datetime given in the Memento-Datetime request header.
*
* @author bbpennel
*/
public class HistoricMementoBuilder extends PostBuilder {
public class HistoricMementoBuilder extends BodyRequestBuilder {

/**
* Instantiate builder
Expand Down Expand Up @@ -45,4 +53,101 @@ public HistoricMementoBuilder(final URI uri, final FcrepoClient client, final St
UTC_RFC_1123_FORMATTER.parse(mementoDatetime);
request.setHeader(MEMENTO_DATETIME, mementoDatetime);
}

@Override
protected HttpRequestBase createRequest() {
return HttpMethods.POST.createRequest(targetUri);
}

@Override
public HistoricMementoBuilder body(final InputStream stream, final String contentType) {
return (HistoricMementoBuilder) super.body(stream, contentType);
}

@Override
public HistoricMementoBuilder body(final File file, final String contentType) throws IOException {
return (HistoricMementoBuilder) super.body(file, contentType);
}

@Override
public HistoricMementoBuilder body(final InputStream stream) {
return (HistoricMementoBuilder) super.body(stream);
}

@Override
public HistoricMementoBuilder externalContent(final URI contentURI,
final String contentType,
final String handling) {
return (HistoricMementoBuilder) super.externalContent(contentURI, contentType, handling);
}

@Override
public HistoricMementoBuilder digest(final String digest, final String alg) {
return (HistoricMementoBuilder) super.digest(digest, alg);
}

@Override
public HistoricMementoBuilder digestMd5(final String digest) {
return (HistoricMementoBuilder) super.digestMd5(digest);
}

@Override
public HistoricMementoBuilder digestSha1(final String digest) {
return (HistoricMementoBuilder) super.digestSha1(digest);
}

@Override
public HistoricMementoBuilder digestSha256(final String digest) {
return (HistoricMementoBuilder) super.digestSha256(digest);
}

@Override
public HistoricMementoBuilder addInteractionModel(final String interactionModelUri) {
return (HistoricMementoBuilder) super.addInteractionModel(interactionModelUri);
}

@Override
public HistoricMementoBuilder linkAcl(final String aclUri) {
return (HistoricMementoBuilder) super.linkAcl(aclUri);
}

@Override
public HistoricMementoBuilder addHeader(final String name, final String value) {
return (HistoricMementoBuilder) super.addHeader(name, value);
}

@Override
public HistoricMementoBuilder addLinkHeader(final FcrepoLink linkHeader) {
return (HistoricMementoBuilder) super.addLinkHeader(linkHeader);
}

/**
* Provide a content disposition header which will be used as the filename
*
* @param filename the name of the file being provided in the body of the request
* @return this builder
* @throws FcrepoOperationFailedException if unable to encode filename
*/
public HistoricMementoBuilder filename(final String filename) throws FcrepoOperationFailedException {
final ContentDisposition.Builder builder = ContentDisposition.builder("attachment");
if (filename != null) {
builder.filename(filename);
}
request.addHeader(CONTENT_DISPOSITION, builder.build().toString());
return this;
}

/**
* Provide a suggested name for the new child resource, which the repository may ignore.
*
* @param slug value to supply as the slug header
* @return this builder
*/
public HistoricMementoBuilder slug(final String slug) {
if (slug != null) {
request.addHeader(SLUG, slug);
}
return this;
}

}
5 changes: 5 additions & 0 deletions src/main/java/org/fcrepo/client/OptionsBuilder.java
Expand Up @@ -40,4 +40,9 @@ public OptionsBuilder addHeader(final String name, final String value) {
public OptionsBuilder addLinkHeader(final FcrepoLink linkHeader) {
return (OptionsBuilder) super.addLinkHeader(linkHeader);
}

@Override
public OptionsBuilder addTransaction(final URI transaction) {
return (OptionsBuilder) super.addTransaction(transaction);
}
}
Expand Up @@ -40,4 +40,5 @@ public OriginalMementoBuilder addHeader(final String name, final String value) {
public OriginalMementoBuilder addLinkHeader(final FcrepoLink linkHeader) {
return (OriginalMementoBuilder) super.addLinkHeader(linkHeader);
}

}
5 changes: 5 additions & 0 deletions src/main/java/org/fcrepo/client/PatchBuilder.java
Expand Up @@ -101,4 +101,9 @@ public PatchBuilder addHeader(final String name, final String value) {
public PatchBuilder addLinkHeader(final FcrepoLink linkHeader) {
return (PatchBuilder) super.addLinkHeader(linkHeader);
}

@Override
public PatchBuilder addTransaction(final URI transaction) {
return (PatchBuilder) super.addTransaction(transaction);
}
}
5 changes: 5 additions & 0 deletions src/main/java/org/fcrepo/client/PostBuilder.java
Expand Up @@ -101,6 +101,11 @@ public PostBuilder addHeader(final String name, final String value) {
return (PostBuilder) super.addHeader(name, value);
}

@Override
public PostBuilder addTransaction(final URI transaction) {
return (PostBuilder) super.addTransaction(transaction);
}

@Override
public PostBuilder addLinkHeader(final FcrepoLink linkHeader) {
return (PostBuilder) super.addLinkHeader(linkHeader);
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/org/fcrepo/client/PutBuilder.java
Expand Up @@ -121,6 +121,11 @@ public PutBuilder addLinkHeader(final FcrepoLink linkHeader) {
return (PutBuilder) super.addLinkHeader(linkHeader);
}

@Override
public PutBuilder addTransaction(final URI transaction) {
return (PutBuilder) super.addTransaction(transaction);
}

/**
* Provide a content disposition header which will be used as the filename
*
Expand Down

0 comments on commit 3393573

Please sign in to comment.