diff --git a/src/main/java/org/fcrepo/client/DeleteBuilder.java b/src/main/java/org/fcrepo/client/DeleteBuilder.java index 80b3122..22d6ba5 100644 --- a/src/main/java/org/fcrepo/client/DeleteBuilder.java +++ b/src/main/java/org/fcrepo/client/DeleteBuilder.java @@ -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); + } + } diff --git a/src/main/java/org/fcrepo/client/FcrepoClient.java b/src/main/java/org/fcrepo/client/FcrepoClient.java index c77d44b..19ebd7c 100644 --- a/src/main/java/org/fcrepo/client/FcrepoClient.java +++ b/src/main/java/org/fcrepo/client/FcrepoClient.java @@ -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; @@ -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); /** @@ -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; } /** @@ -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 * @@ -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); } } } diff --git a/src/main/java/org/fcrepo/client/FcrepoResponse.java b/src/main/java/org/fcrepo/client/FcrepoResponse.java index 26f16db..0b2f122 100644 --- a/src/main/java/org/fcrepo/client/FcrepoResponse.java +++ b/src/main/java/org/fcrepo/client/FcrepoResponse.java @@ -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; @@ -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; @@ -56,6 +59,8 @@ public class FcrepoResponse implements Closeable { private InputStream body; + private URI transactionUri; + private String contentType; private boolean closed = false; @@ -313,4 +318,26 @@ public Map 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; + } + } diff --git a/src/main/java/org/fcrepo/client/FedoraHeaderConstants.java b/src/main/java/org/fcrepo/client/FedoraHeaderConstants.java index 76ea4d4..3c13dfd 100644 --- a/src/main/java/org/fcrepo/client/FedoraHeaderConstants.java +++ b/src/main/java/org/fcrepo/client/FedoraHeaderConstants.java @@ -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() { } } diff --git a/src/main/java/org/fcrepo/client/GetBuilder.java b/src/main/java/org/fcrepo/client/GetBuilder.java index 72b230d..a2be428 100644 --- a/src/main/java/org/fcrepo/client/GetBuilder.java +++ b/src/main/java/org/fcrepo/client/GetBuilder.java @@ -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); + } } diff --git a/src/main/java/org/fcrepo/client/HeadBuilder.java b/src/main/java/org/fcrepo/client/HeadBuilder.java index 8c11c7f..5f04af9 100644 --- a/src/main/java/org/fcrepo/client/HeadBuilder.java +++ b/src/main/java/org/fcrepo/client/HeadBuilder.java @@ -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); + } } diff --git a/src/main/java/org/fcrepo/client/HistoricMementoBuilder.java b/src/main/java/org/fcrepo/client/HistoricMementoBuilder.java index 3b67fa5..4c463fe 100644 --- a/src/main/java/org/fcrepo/client/HistoricMementoBuilder.java +++ b/src/main/java/org/fcrepo/client/HistoricMementoBuilder.java @@ -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 @@ -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; + } + } diff --git a/src/main/java/org/fcrepo/client/OptionsBuilder.java b/src/main/java/org/fcrepo/client/OptionsBuilder.java index 8be45ff..b498aac 100644 --- a/src/main/java/org/fcrepo/client/OptionsBuilder.java +++ b/src/main/java/org/fcrepo/client/OptionsBuilder.java @@ -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); + } } diff --git a/src/main/java/org/fcrepo/client/OriginalMementoBuilder.java b/src/main/java/org/fcrepo/client/OriginalMementoBuilder.java index 2dcad50..a9f0597 100644 --- a/src/main/java/org/fcrepo/client/OriginalMementoBuilder.java +++ b/src/main/java/org/fcrepo/client/OriginalMementoBuilder.java @@ -40,4 +40,5 @@ public OriginalMementoBuilder addHeader(final String name, final String value) { public OriginalMementoBuilder addLinkHeader(final FcrepoLink linkHeader) { return (OriginalMementoBuilder) super.addLinkHeader(linkHeader); } + } diff --git a/src/main/java/org/fcrepo/client/PatchBuilder.java b/src/main/java/org/fcrepo/client/PatchBuilder.java index 7a0fca7..ec2c60f 100644 --- a/src/main/java/org/fcrepo/client/PatchBuilder.java +++ b/src/main/java/org/fcrepo/client/PatchBuilder.java @@ -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); + } } diff --git a/src/main/java/org/fcrepo/client/PostBuilder.java b/src/main/java/org/fcrepo/client/PostBuilder.java index 5034171..84b37d1 100644 --- a/src/main/java/org/fcrepo/client/PostBuilder.java +++ b/src/main/java/org/fcrepo/client/PostBuilder.java @@ -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); diff --git a/src/main/java/org/fcrepo/client/PutBuilder.java b/src/main/java/org/fcrepo/client/PutBuilder.java index 3eec260..302bea9 100644 --- a/src/main/java/org/fcrepo/client/PutBuilder.java +++ b/src/main/java/org/fcrepo/client/PutBuilder.java @@ -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 * diff --git a/src/main/java/org/fcrepo/client/RequestBuilder.java b/src/main/java/org/fcrepo/client/RequestBuilder.java index 517b4d1..3db1242 100644 --- a/src/main/java/org/fcrepo/client/RequestBuilder.java +++ b/src/main/java/org/fcrepo/client/RequestBuilder.java @@ -5,6 +5,7 @@ */ package org.fcrepo.client; +import static org.fcrepo.client.FedoraHeaderConstants.ATOMIC_ID; import static org.slf4j.LoggerFactory.getLogger; import static org.fcrepo.client.FedoraHeaderConstants.LINK; @@ -89,4 +90,15 @@ protected RequestBuilder addLinkHeader(final FcrepoLink linkHeader) { request.addHeader(LINK, linkHeader.toString()); return this; } + + /** + * Add a transaction atomic id header to the request + * + * @param transaction transaction atomic id + * @return this builder + */ + protected RequestBuilder addTransaction(final URI transaction) { + request.addHeader(ATOMIC_ID, transaction.toString()); + return this; + } } diff --git a/src/main/java/org/fcrepo/client/TransactionalFcrepoClient.java b/src/main/java/org/fcrepo/client/TransactionalFcrepoClient.java new file mode 100644 index 0000000..fe204ac --- /dev/null +++ b/src/main/java/org/fcrepo/client/TransactionalFcrepoClient.java @@ -0,0 +1,125 @@ +/* + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree. + */ +package org.fcrepo.client; + +import java.net.URI; + +/** + * A Transaction aware client which adds the Atomic_ID header to requests and provides functionality for interacting + * with the Fedora Transaction API + * + * @author mikejritter + */ +public class TransactionalFcrepoClient extends FcrepoClient { + + private final URI transactionURI; + + /** + * @param transactionURI the transaction to append to all requests + * @param httpClientBuilder the httpclient + * @param throwExceptionOnFailure whether to throw an exception on any non-2xx or 3xx HTTP responses + */ + public TransactionalFcrepoClient(final URI transactionURI, + final FcrepoHttpClientBuilder httpClientBuilder, + final Boolean throwExceptionOnFailure) { + super(httpClientBuilder, throwExceptionOnFailure); + + if (transactionURI == null) { + throw new IllegalArgumentException("TransactionURI cannot be null"); + } + this.transactionURI = transactionURI; + } + + public URI getTransactionURI() { + return transactionURI; + } + + /** + * Commit a transaction by performing a PUT + * + * @return the commit RequestBuilder + */ + public PutBuilder commit() { + return put(transactionURI); + } + + /** + * Retrieve the status of a transaction by performing a GET + * + * @return the status RequestBuilder + */ + public GetBuilder status() { + return get(transactionURI); + } + + /** + * Keep a transaction alive by performing a POST + * + * @return the keep alive RequestBuilder + */ + public PostBuilder keepAlive() { + return post(transactionURI); + } + + /** + * Rollback a transaction by performing a DELETE + * + * @return the rollback RequestBuilder + */ + public DeleteBuilder rollback() { + return delete(transactionURI); + } + + @Override + public GetBuilder get(final URI url) { + final var builder = super.get(url); + builder.addTransaction(transactionURI); + return builder; + } + + @Override + public HeadBuilder head(final URI url) { + final var builder = super.head(url); + builder.addTransaction(transactionURI); + return builder; + } + + @Override + public DeleteBuilder delete(final URI url) { + final var builder = super.delete(url); + builder.addTransaction(transactionURI); + return builder; + } + + @Override + public OptionsBuilder options(final URI url) { + final var builder = super.options(url); + builder.addTransaction(transactionURI); + return builder; + } + + @Override + public PatchBuilder patch(final URI url) { + final var builder = super.patch(url); + builder.addTransaction(transactionURI); + return builder; + } + + @Override + public PostBuilder post(final URI url) { + final var builder = super.post(url); + builder.addTransaction(transactionURI); + return builder; + } + + @Override + public PutBuilder put(final URI url) { + final var builder = super.put(url); + builder.addTransaction(transactionURI); + return builder; + } + +} diff --git a/src/test/java/org/fcrepo/client/integration/FcrepoTransactionIT.java b/src/test/java/org/fcrepo/client/integration/FcrepoTransactionIT.java new file mode 100644 index 0000000..be1562c --- /dev/null +++ b/src/test/java/org/fcrepo/client/integration/FcrepoTransactionIT.java @@ -0,0 +1,143 @@ +/* + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree. + */ +package org.fcrepo.client.integration; + +import static java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME; +import static javax.ws.rs.core.Response.Status.CREATED; +import static javax.ws.rs.core.Response.Status.NO_CONTENT; +import static org.fcrepo.client.FcrepoClient.TRANSACTION_ENDPOINT; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.net.URI; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.UUID; + + +import org.fcrepo.client.FcrepoClient; +import org.fcrepo.client.FedoraHeaderConstants; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +/** + * Tests against the Fedora Transaction API + * + * @author mikejritter + */ +public class FcrepoTransactionIT extends AbstractResourceIT { + + private static FcrepoClient client; + + private static final String FEDORA_ADMIN = "fedoraAdmin"; + private static final DateTimeFormatter FORMATTER = RFC_1123_DATE_TIME.withZone(ZoneId.of("UTC")); + + @BeforeClass + public static void beforeClass() { + client = FcrepoClient.client() + .credentials(FEDORA_ADMIN, FEDORA_ADMIN) + .authScope(HOSTNAME) + .build(); + } + + @AfterClass + public static void afterClass() throws IOException { + client.close(); + } + + @Test + public void testStartWithTxEndpoint() throws Exception { + try (final var txClient = client.startTransactionClient(new URI(SERVER_ADDRESS + TRANSACTION_ENDPOINT))) { + final var txURI = txClient.getTransactionURI(); + assertNotNull(txURI); + } + } + + @Test + public void testTransactionCommit() throws Exception { + try (final var txClient = client.startTransactionClient(new URI(SERVER_ADDRESS))) { + final var txURI = txClient.getTransactionURI(); + + // create a container + final var container = UUID.randomUUID().toString(); + try (final var response = txClient.put(new URI(SERVER_ADDRESS + container)).perform()) { + assertEquals(CREATED.getStatusCode(), response.getStatusCode()); + assertNotNull(response.getTransactionUri()); + assertEquals(txURI, response.getTransactionUri()); + } + + // commit tx + try (final var response = txClient.commit().perform()) { + assertEquals(NO_CONTENT.getStatusCode(), response.getStatusCode()); + } + } + } + + @Test + public void testTransactionKeepAlive() throws Exception { + final String expiry; + final URI location; + + try (final var txClient = client.startTransactionClient(new URI(SERVER_ADDRESS))) { + location = txClient.getTransactionURI(); + assertNotNull(location); + + // the initial transaction currently returns Expires rather than Atomic-Expires + try (var response = txClient.status().perform()) { + expiry = response.getHeaderValue(FedoraHeaderConstants.ATOMIC_EXPIRES); + assertNotNull(expiry); + } + + // perform the keep-alive + try (final var response = txClient.keepAlive().perform()) { + assertEquals(NO_CONTENT.getStatusCode(), response.getStatusCode()); + + final var expiryFromStatus = response.getHeaderValue(FedoraHeaderConstants.ATOMIC_EXPIRES); + assertNotNull(expiryFromStatus); + + final var initialExpiration = Instant.from(FORMATTER.parse(expiry)); + final var updatedExpiration = Instant.from(FORMATTER.parse(expiryFromStatus)); + assertTrue(initialExpiration.isBefore(updatedExpiration)); + } + } + } + + @Test + public void testTransactionStatus() throws Exception { + final URI location; + + try (final var txClient = client.startTransactionClient(new URI(SERVER_ADDRESS))) { + location = txClient.getTransactionURI(); + assertNotNull(location); + + try (final var response = txClient.status().perform()) { + assertEquals(NO_CONTENT.getStatusCode(), response.getStatusCode()); + + final var expiryFromStatus = response.getHeaderValue(FedoraHeaderConstants.ATOMIC_EXPIRES); + assertNotNull(expiryFromStatus); + } + } + } + + @Test + public void testTransactionRollback() throws Exception { + final URI location; + + try (final var txClient = client.startTransactionClient(new URI(SERVER_ADDRESS))) { + location = txClient.getTransactionURI(); + assertNotNull(location); + + try (final var response = txClient.rollback().perform()) { + assertEquals(NO_CONTENT.getStatusCode(), response.getStatusCode()); + } + } + } + +}