001/* 002 * Licensed to DuraSpace under one or more contributor license agreements. 003 * See the NOTICE file distributed with this work for additional information 004 * regarding copyright ownership. 005 * 006 * DuraSpace licenses this file to you under the Apache License, 007 * Version 2.0 (the "License"); you may not use this file except in 008 * compliance with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018package org.fcrepo.integration.http.api; 019 020import static java.lang.Math.min; 021import static java.lang.Thread.sleep; 022import static java.nio.charset.StandardCharsets.UTF_8; 023import static javax.ws.rs.core.HttpHeaders.CACHE_CONTROL; 024import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE; 025import static javax.ws.rs.core.HttpHeaders.LINK; 026import static javax.ws.rs.core.MediaType.TEXT_PLAIN; 027import static javax.ws.rs.core.Response.Status.CONFLICT; 028import static javax.ws.rs.core.Response.Status.CREATED; 029import static javax.ws.rs.core.Response.Status.GONE; 030import static javax.ws.rs.core.Response.Status.NOT_FOUND; 031import static javax.ws.rs.core.Response.Status.NO_CONTENT; 032import static javax.ws.rs.core.Response.Status.OK; 033import static javax.ws.rs.core.Response.Status.PRECONDITION_FAILED; 034import static org.apache.http.util.EntityUtils.consume; 035import static org.apache.jena.graph.Node.ANY; 036import static org.apache.jena.graph.NodeFactory.createLiteral; 037import static org.apache.jena.graph.NodeFactory.createURI; 038import static org.apache.jena.vocabulary.DC_11.title; 039import static org.fcrepo.http.commons.session.TransactionConstants.ATOMIC_EXPIRES_HEADER; 040import static org.fcrepo.http.commons.session.TransactionConstants.ATOMIC_ID_HEADER; 041import static org.fcrepo.http.commons.session.TransactionConstants.EXPIRES_RFC_1123_FORMATTER; 042import static org.fcrepo.http.commons.session.TransactionConstants.TX_COMMIT_REL; 043import static org.fcrepo.http.commons.session.TransactionConstants.TX_ENDPOINT_REL; 044import static org.fcrepo.http.commons.session.TransactionConstants.TX_PREFIX; 045import static org.fcrepo.kernel.api.FedoraTypes.FCR_TOMBSTONE; 046import static org.fcrepo.kernel.api.RdfLexicon.ARCHIVAL_GROUP; 047import static org.junit.Assert.assertEquals; 048import static org.junit.Assert.assertFalse; 049import static org.junit.Assert.assertNotNull; 050import static org.junit.Assert.assertTrue; 051import static org.junit.Assert.fail; 052import static org.mockito.Mockito.mock; 053import static org.mockito.Mockito.when; 054 055import java.io.IOException; 056import java.io.UncheckedIOException; 057import java.nio.file.Files; 058import java.time.Duration; 059import java.time.format.DateTimeParseException; 060import java.util.UUID; 061import java.util.regex.Matcher; 062import java.util.regex.Pattern; 063 064import javax.sql.DataSource; 065import javax.ws.rs.core.Response.Status; 066 067import org.fcrepo.common.lang.CheckedRunnable; 068import org.fcrepo.config.OcflPropsConfig; 069import org.fcrepo.http.commons.test.util.CloseableDataset; 070import org.fcrepo.kernel.api.ContainmentIndex; 071import org.fcrepo.kernel.api.Transaction; 072import org.fcrepo.kernel.api.identifiers.FedoraId; 073import org.fcrepo.storage.ocfl.CommitType; 074import org.fcrepo.storage.ocfl.DefaultOcflObjectSessionFactory; 075 076import org.apache.commons.lang3.StringUtils; 077import org.apache.http.Header; 078import org.apache.http.HttpEntity; 079import org.apache.http.client.HttpResponseException; 080import org.apache.http.client.methods.CloseableHttpResponse; 081import org.apache.http.client.methods.HttpDelete; 082import org.apache.http.client.methods.HttpGet; 083import org.apache.http.client.methods.HttpHead; 084import org.apache.http.client.methods.HttpPatch; 085import org.apache.http.client.methods.HttpPost; 086import org.apache.http.client.methods.HttpPut; 087import org.apache.http.entity.StringEntity; 088import org.apache.http.util.EntityUtils; 089import org.junit.After; 090import org.junit.Before; 091import org.junit.Test; 092import org.springframework.jdbc.core.JdbcTemplate; 093import org.springframework.test.context.TestExecutionListeners; 094 095/** 096 * <p>TransactionsIT class.</p> 097 * 098 * @author awoods 099 */ 100@TestExecutionListeners( 101 listeners = { TestIsolationExecutionListener.class }, 102 mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS) 103public class TransactionsIT extends AbstractResourceIT { 104 105 public static final long REAP_INTERVAL = 1000; 106 107 public static final Pattern TX_ID_PATTERN = Pattern.compile(".+/" + TX_PREFIX + "([0-9a-f\\-]+)$"); 108 109 private static final String ARCHIVAL_GROUP_TYPE = "<" + ARCHIVAL_GROUP + ">;rel=\"type\""; 110 111 private DefaultOcflObjectSessionFactory objectSessionFactory; 112 private ContainmentIndex containmentIndex; 113 private OcflPropsConfig ocflConfig; 114 private JdbcTemplate jdbcTemplate; 115 116 @Before 117 public void setup() { 118 objectSessionFactory = getBean(DefaultOcflObjectSessionFactory.class); 119 containmentIndex = getBean("containmentIndex", ContainmentIndex.class); 120 ocflConfig = getBean(OcflPropsConfig.class); 121 final var dataSource = getBean(DataSource.class); 122 jdbcTemplate = new JdbcTemplate(dataSource); 123 } 124 125 @After 126 public void after() { 127 objectSessionFactory.setDefaultCommitType(CommitType.NEW_VERSION); 128 } 129 130 private void dropContainment() { 131 jdbcTemplate.execute("DROP TABLE containment"); 132 } 133 134 @Test 135 public void testRootHasTxEndpoint() throws Exception { 136 final var getRoot = new HttpGet(serverAddress); 137 try (final CloseableHttpResponse response = execute(getRoot)) { 138 final String txEndpointUri = serverAddress + TX_PREFIX; 139 checkForLinkHeader(response, txEndpointUri, TX_ENDPOINT_REL); 140 } 141 } 142 143 @Test 144 public void testCreateTransaction() throws IOException { 145 final HttpPost createTx = new HttpPost(serverAddress + "fcr:tx"); 146 try (final CloseableHttpResponse response = execute(createTx)) { 147 assertEquals(CREATED.getStatusCode(), getStatus(response)); 148 final var location = getLocation(response); 149 String txId = null; 150 final Matcher txMatcher = TX_ID_PATTERN.matcher(location); 151 if (txMatcher.matches()) { 152 txId = txMatcher.group(1); 153 } 154 155 assertNotNull("Expected Location header to send us to root node path within the transaction", 156 txId); 157 158 final String commitUri = serverAddress + TX_PREFIX + txId; 159 checkForLinkHeader(response, commitUri, TX_COMMIT_REL); 160 161 assertHeaderIsRfc1123Date(response, "Expires"); 162 } 163 } 164 165 @Test 166 public void testRequestsInTransactionThatDoestExist() { 167 /* create a tx */ 168 assertEquals(NOT_FOUND.getStatusCode(), getStatus(new HttpPost(serverAddress + "fcr:tx/123idontexist"))); 169 } 170 171 @Test 172 public void testCreateAndTimeoutTransaction() throws IOException, InterruptedException { 173 174 /* create a short-lived tx */ 175 final long testTimeout = min(500, REAP_INTERVAL / 2); 176 propsConfig.setSessionTimeout(Duration.ofMillis(testTimeout)); 177 178 /* create a tx */ 179 final String location = createTransaction(); 180 181 try (final CloseableHttpResponse resp = execute(new HttpGet(location))) { 182 assertEquals(Status.NO_CONTENT.getStatusCode(), getStatus(resp)); 183 assertHeaderIsRfc1123Date(resp, ATOMIC_EXPIRES_HEADER); 184 consume(resp.getEntity()); 185 } 186 187 sleep(REAP_INTERVAL * 2); 188 try { 189 assertEquals("Transaction did not expire", GONE.getStatusCode(), getStatus(new HttpGet(location))); 190 } finally { 191 propsConfig.setSessionTimeout(Duration.ofMillis(180000)); 192 } 193 } 194 195 private void assertHeaderIsRfc1123Date(final CloseableHttpResponse response, final String headerName) { 196 final Header header = response.getFirstHeader(headerName); 197 assertNotNull("Header " + headerName + " was not set", header); 198 try { 199 EXPIRES_RFC_1123_FORMATTER.parse(header.getValue()); 200 } catch (final DateTimeParseException e) { 201 fail("Expected header " + headerName + " to be an RFC1123 date, but was " + header.getValue()); 202 } 203 } 204 205 @Test 206 public void testCreateDoStuffAndRollbackTransaction() throws IOException { 207 /* create a tx */ 208 final String txLocation = createTransaction(); 209 210 /* create a new object inside the tx */ 211 final String newLocation; 212 final HttpPost postNew = new HttpPost(serverAddress); 213 postNew.addHeader(ATOMIC_ID_HEADER, txLocation); 214 try (final CloseableHttpResponse resp = execute(postNew)) { 215 assertEquals(CREATED.getStatusCode(), getStatus(resp)); 216 newLocation = getLocation(resp); 217 } 218 219 /* fetch the created tx from the endpoint */ 220 try (final CloseableDataset dataset = getDataset(addTxTo(new HttpGet(newLocation), txLocation))) { 221 assertTrue(dataset.asDatasetGraph().contains(ANY, createURI(newLocation), ANY, ANY)); 222 } 223 /* fetch the created tx from the endpoint */ 224 assertEquals("Expected to not find our object within the scope of the transaction", 225 NOT_FOUND.getStatusCode(), getStatus(new HttpGet(newLocation))); 226 227 /* and rollback */ 228 assertEquals(NO_CONTENT.getStatusCode(), getStatus(new HttpDelete(txLocation))); 229 230 assertEquals("Rolled back transaction should be gone", 231 GONE.getStatusCode(), getStatus(new HttpGet(txLocation))); 232 233 assertEquals("Expected to not find our object after rollback", 234 NOT_FOUND.getStatusCode(), getStatus(new HttpGet(newLocation))); 235 236 assertEquals("Expected to not find our object in transaction after rollback", 237 CONFLICT.getStatusCode(), getStatus(addTxTo(new HttpGet(newLocation), txLocation))); 238 } 239 240 @Test 241 public void rejectPutWhenIfMatchDoesNotMatch() throws IOException { 242 final String txLocation = createTransaction(); 243 244 final String newLocation; 245 final HttpPost postNew = new HttpPost(serverAddress); 246 postNew.addHeader(ATOMIC_ID_HEADER, txLocation); 247 try (final CloseableHttpResponse resp = execute(postNew)) { 248 assertEquals(CREATED.getStatusCode(), getStatus(resp)); 249 newLocation = getLocation(resp); 250 } 251 252 final var request = new HttpPut(newLocation); 253 request.addHeader(ATOMIC_ID_HEADER, txLocation); 254 request.addHeader("If-Match", "\"doesnt-match\""); 255 assertEquals(PRECONDITION_FAILED.getStatusCode(), getStatus(request)); 256 } 257 258 @Test 259 public void allowPutWhenIfMatchMatches() throws IOException { 260 final String txLocation = createTransaction(); 261 262 final String newLocation; 263 final String etag; 264 final HttpPost postNew = new HttpPost(serverAddress); 265 postNew.addHeader(ATOMIC_ID_HEADER, txLocation); 266 try (final CloseableHttpResponse resp = execute(postNew)) { 267 assertEquals(CREATED.getStatusCode(), getStatus(resp)); 268 newLocation = getLocation(resp); 269 etag = resp.getFirstHeader("ETag").getValue(); 270 } 271 272 final var request = new HttpPut(newLocation); 273 request.addHeader(ATOMIC_ID_HEADER, txLocation); 274 request.addHeader("If-Match", etag.substring(2)); 275 assertEquals(NO_CONTENT.getStatusCode(), getStatus(request)); 276 } 277 278 @Test 279 public void testRollbackShouldNotLeaveDbInPartiallyUpdatedState() throws IOException { 280 /* create a tx */ 281 final String txLocation = createTransaction(); 282 283 /* create a new object inside the tx */ 284 final String newLocation; 285 final HttpPost postNew = new HttpPost(serverAddress); 286 postNew.addHeader(ATOMIC_ID_HEADER, txLocation); 287 try (final CloseableHttpResponse resp = execute(postNew)) { 288 assertEquals(CREATED.getStatusCode(), getStatus(resp)); 289 newLocation = getLocation(resp); 290 } 291 292 final var resourceId = StringUtils.substringAfterLast(newLocation, "/"); 293 294 /* fetch the created tx from the endpoint */ 295 try (final CloseableDataset dataset = getDataset(addTxTo(new HttpGet(newLocation), txLocation))) { 296 assertTrue(dataset.asDatasetGraph().contains(ANY, createURI(newLocation), ANY, ANY)); 297 } 298 /* fetch the created tx from the endpoint */ 299 assertEquals("Expected to not find our object within the scope of the transaction", 300 NOT_FOUND.getStatusCode(), getStatus(new HttpGet(newLocation))); 301 302 // Drop the entire containment table in order to cause an error. 303 dropContainment(); 304 305 // Commit transaction -- should fail 306 assertEquals(CONFLICT.getStatusCode(), getStatus(new HttpPut(txLocation))); 307 308 assertEquals("Rolled back transaction should be gone", 309 GONE.getStatusCode(), getStatus(new HttpGet(txLocation))); 310 311 assertEquals("Expected to not find our object after rollback", 312 NOT_FOUND.getStatusCode(), getStatus(new HttpGet(newLocation))); 313 314 assertObjectDoesNotExistOnDisk(FedoraId.create(resourceId)); 315 } 316 317 @Test 318 public void conflictingContainmentOverwritesObjectModifiedInTransactionRdf() throws IOException { 319 /* create a tx */ 320 final String txLocation = createTransaction(); 321 322 /* create a new object inside the tx */ 323 final String newLocation; 324 final HttpPost postNew = new HttpPost(serverAddress); 325 postNew.addHeader(ATOMIC_ID_HEADER, txLocation); 326 try (final CloseableHttpResponse resp = execute(postNew)) { 327 assertEquals(CREATED.getStatusCode(), getStatus(resp)); 328 newLocation = getLocation(resp); 329 } 330 331 final var resourceId = StringUtils.substringAfterLast(newLocation, "/"); 332 333 /* fetch the created tx from the endpoint */ 334 try (final CloseableDataset dataset = getDataset(addTxTo(new HttpGet(newLocation), txLocation))) { 335 assertTrue(dataset.asDatasetGraph().contains(ANY, createURI(newLocation), ANY, ANY)); 336 } 337 /* fetch the created tx from the endpoint */ 338 assertEquals("Expected to not find our object within the scope of the transaction", 339 NOT_FOUND.getStatusCode(), getStatus(new HttpGet(newLocation))); 340 341 addConflictingContainmentRecord(resourceId); 342 343 // Commit transaction -- doesn't fail as the conflicting record overwrites the old one. 344 assertEquals(NO_CONTENT.getStatusCode(), getStatus(new HttpPut(txLocation))); 345 346 assertEquals("Committed transaction should be gone", 347 GONE.getStatusCode(), getStatus(new HttpGet(txLocation))); 348 349 assertEquals("Expected to not find our object after rollback", 350 OK.getStatusCode(), getStatus(new HttpGet(newLocation))); 351 } 352 353 @Test 354 public void conflictingContainmentOverwritesObjectModifiedInTransactionBinary() throws IOException { 355 final String txLocation1 = createTransaction(); 356 357 final var bin1 = UUID.randomUUID().toString(); 358 final var bin2 = UUID.randomUUID().toString(); 359 360 putBinary(bin1, txLocation1, "test 1"); 361 362 assertEquals(NO_CONTENT.getStatusCode(), getStatus(new HttpPut(txLocation1))); 363 364 assertBinaryContent("test 1", bin1, null); 365 366 final String txLocation2 = createTransaction(); 367 368 putBinary(bin1, txLocation2, "test 1 -- updated!"); 369 putBinary(bin2, txLocation2, "test 2 -- I'm new!"); 370 371 assertBinaryContent("test 1 -- updated!", bin1, txLocation2); 372 assertBinaryContent("test 2 -- I'm new!", bin2, txLocation2); 373 374 addConflictingContainmentRecord(bin2); 375 376 // Commit transaction 377 assertEquals(NO_CONTENT.getStatusCode(), getStatus(new HttpPut(txLocation2))); 378 379 assertEquals("Committed transaction should be gone", 380 GONE.getStatusCode(), getStatus(new HttpGet(txLocation2))); 381 382 assertBinaryContent("test 1 -- updated!", bin1, null); 383 assertBinaryContent("test 2 -- I'm new!", bin2, null); 384 } 385 386 @Test 387 public void rollbackFailsWhenAutoVersioningNotUsedAndFailureInOcflCommit() throws IOException { 388 objectSessionFactory.setDefaultCommitType(CommitType.UNVERSIONED); 389 390 final String txLocation1 = createTransaction(); 391 392 // need prefix so they're ordered deterministically 393 final var bin1 = "1" + UUID.randomUUID().toString(); 394 final var bin2 = "2" + UUID.randomUUID().toString(); 395 final var bin3 = "3" + UUID.randomUUID().toString(); 396 397 putBinary(bin1, txLocation1, "test 1"); 398 putBinary(bin3, txLocation1, "test 3"); 399 400 assertEquals(NO_CONTENT.getStatusCode(), getStatus(new HttpPut(txLocation1))); 401 402 assertBinaryContent("test 1", bin1, null); 403 assertBinaryContent("test 3", bin3, null); 404 405 final String txLocation2 = createTransaction(); 406 407 putBinary(bin1, txLocation2, "test 1 -- updated!"); 408 putBinary(bin2, txLocation2, "test 2 -- I'm new!"); 409 putBinary(bin3, txLocation2, "test 3 -- updated!"); 410 411 assertBinaryContent("test 1 -- updated!", bin1, txLocation2); 412 assertBinaryContent("test 2 -- I'm new!", bin2, txLocation2); 413 414 corruptStagedBinary(bin3); 415 416 // Commit transaction -- should fail 417 assertEquals(CONFLICT.getStatusCode(), getStatus(new HttpPut(txLocation2))); 418 419 assertEquals("Rolled back transaction should be gone", 420 GONE.getStatusCode(), getStatus(new HttpGet(txLocation2))); 421 422 // bin1 was not rolled back 423 assertBinaryContent("test 1 -- updated!", bin1, null); 424 425 // bin2 was rolled back 426 assertEquals("Expected to not find our object after rollback", 427 NOT_FOUND.getStatusCode(), getStatus(new HttpGet(serverAddress + bin2))); 428 assertObjectDoesNotExistOnDisk(FedoraId.create(bin2)); 429 430 // bin1 was not committed 431 assertBinaryContent("test 3", bin3, null); 432 } 433 434 @Test 435 public void rollbackSucceedsWhenAutoVersioningUsedAndFailureInOcflCommit() throws IOException { 436 final String txLocation1 = createTransaction(); 437 438 // need prefix so they're ordered deterministically 439 final var bin1 = "1" + UUID.randomUUID().toString(); 440 final var bin2 = "2" + UUID.randomUUID().toString(); 441 final var bin3 = "3" + UUID.randomUUID().toString(); 442 443 putBinary(bin1, txLocation1, "test 1"); 444 putBinary(bin3, txLocation1, "test 3"); 445 446 assertEquals(NO_CONTENT.getStatusCode(), getStatus(new HttpPut(txLocation1))); 447 448 assertBinaryContent("test 1", bin1, null); 449 assertBinaryContent("test 3", bin3, null); 450 451 final String txLocation2 = createTransaction(); 452 453 putBinary(bin1, txLocation2, "test 1 -- updated!"); 454 putBinary(bin2, txLocation2, "test 2 -- I'm new!"); 455 putBinary(bin3, txLocation2, "test 3 -- updated!"); 456 457 assertBinaryContent("test 1 -- updated!", bin1, txLocation2); 458 assertBinaryContent("test 2 -- I'm new!", bin2, txLocation2); 459 460 corruptStagedBinary(bin3); 461 462 // Commit transaction -- should fail 463 assertEquals(CONFLICT.getStatusCode(), getStatus(new HttpPut(txLocation2))); 464 465 assertEquals("Rolled back transaction should be gone", 466 GONE.getStatusCode(), getStatus(new HttpGet(txLocation2))); 467 468 // bin1 was rolled back 469 assertBinaryContent("test 1", bin1, null); 470 471 // bin2 was rolled back 472 assertEquals("Expected to not find our object after rollback", 473 NOT_FOUND.getStatusCode(), getStatus(new HttpGet(serverAddress + bin2))); 474 assertObjectDoesNotExistOnDisk(FedoraId.create(bin2)); 475 476 // bin1 was not committed 477 assertBinaryContent("test 3", bin3, null); 478 } 479 480 @Test 481 public void testTransactionKeepAlive() throws IOException { 482 /* create a tx */ 483 final String txLocation = createTransaction(); 484 assertEquals(NO_CONTENT.getStatusCode(), getStatus(new HttpPost(txLocation))); 485 } 486 487 @Test 488 public void testCreateDoStuffAndCommitTransaction() throws IOException { 489 /* create a tx */ 490 final String txLocation = createTransaction(); 491 /* create a new object inside the tx */ 492 final HttpPost postNew = new HttpPost(serverAddress); 493 postNew.addHeader(ATOMIC_ID_HEADER, txLocation); 494 495 final String datasetLoc; 496 try (final CloseableHttpResponse resp = execute(postNew)) { 497 assertEquals(CREATED.getStatusCode(), resp.getStatusLine().getStatusCode()); 498 assertHasAtomicId(txLocation, resp); 499 datasetLoc = getLocation(resp); 500 } 501 502 // Retrieve the object inside of the transaction 503 final HttpGet getRequest = new HttpGet(datasetLoc); 504 getRequest.addHeader(ATOMIC_ID_HEADER, txLocation); 505 try (final CloseableDataset dataset = getDataset(getRequest)) { 506 assertTrue(dataset.asDatasetGraph().contains(ANY, 507 createURI(datasetLoc), ANY, ANY)); 508 } 509 510 /* fetch the object-in-tx outside of the tx */ 511 assertEquals("Expected to not find our object within the scope of the transaction", 512 NOT_FOUND.getStatusCode(), getStatus(new HttpGet(datasetLoc))); 513 /* and commit */ 514 assertEquals(NO_CONTENT.getStatusCode(), getStatus(new HttpPut(txLocation))); 515 516 /* fetch the object-in-tx outside of the tx after it has been committed */ 517 try (final CloseableDataset dataset = getDataset(new HttpGet(datasetLoc))) { 518 assertTrue("Expected to find our object after the transaction was committed", 519 dataset.asDatasetGraph().contains(ANY, createURI(datasetLoc), ANY, ANY)); 520 } 521 522 assertEquals("Expect conflict when trying to retrieve from committed transaction", 523 CONFLICT.getStatusCode(), getStatus(addTxTo(new HttpGet(datasetLoc), txLocation))); 524 } 525 526 @Test 527 public void transactionShouldNotBeAbleToBeCommittedWhenARequestFails() throws IOException { 528 final var agId = getRandomUniqueId(); 529 final var childId = agId + "/child"; 530 531 // create a tx 532 final String txLocation = createTransaction(); 533 534 putAg(agId, txLocation); 535 getResource(agId, txLocation); 536 537 var put = putObjMethod(childId); 538 put.addHeader(ATOMIC_ID_HEADER, txLocation); 539 put.setHeader("Link", ARCHIVAL_GROUP_TYPE); 540 try (final CloseableHttpResponse response = execute(put)) { 541 assertEquals(CONFLICT.getStatusCode(), getStatus(response)); 542 } 543 544 // subsequent request should fail 545 put = putObjMethod(childId); 546 put.addHeader(ATOMIC_ID_HEADER, txLocation); 547 try (final CloseableHttpResponse response = execute(put)) { 548 assertEquals(CONFLICT.getStatusCode(), getStatus(response)); 549 } 550 551 // and commit should fail 552 assertEquals(CONFLICT.getStatusCode(), getStatus(new HttpPut(txLocation))); 553 554 // ag should not exist 555 try (final CloseableHttpResponse response = execute(new HttpGet(serverAddress + agId))) { 556 assertEquals(NOT_FOUND.getStatusCode(), response.getStatusLine().getStatusCode()); 557 } 558 } 559 560 private void assertHasAtomicId(final String txId, final CloseableHttpResponse resp) { 561 final Header header = resp.getFirstHeader(ATOMIC_ID_HEADER); 562 assertNotNull("No atomic id header present in response", header); 563 564 assertEquals("Header did not match the expected atomic id", txId, header.getValue()); 565 } 566 567 /** 568 * Tests whether a Sparql update is visible within a transaction and if the update is made persistent along with 569 * the commit. 570 * 571 * @throws IOException exception thrown during this function 572 */ 573 @Test 574 public void testIngestNewWithSparqlPatchWithinTransaction() throws IOException { 575 /* create new tx */ 576 final String txLocation = createTransaction(); 577 578 final HttpPost postNew = new HttpPost(serverAddress); 579 final String newObjectLocation; 580 try (final CloseableHttpResponse resp = execute(postNew)) { 581 assertEquals(CREATED.getStatusCode(), getStatus(resp)); 582 newObjectLocation = getLocation(resp); 583 } 584 585 /* update sparql */ 586 final HttpPatch method = addTxTo(new HttpPatch(newObjectLocation), txLocation); 587 method.addHeader(CONTENT_TYPE, "application/sparql-update"); 588 final String newTitle = "this is a new title"; 589 method.setEntity(new StringEntity("INSERT { <> <http://purl.org/dc/elements/1.1/title> \"" + newTitle + 590 "\" } WHERE {}")); 591 assertEquals("Didn't get a NO CONTENT status!", NO_CONTENT.getStatusCode(), getStatus(method)); 592 593 /* make sure the change was made within the tx */ 594 try (final CloseableDataset dataset = getDataset(addTxTo(new HttpGet(newObjectLocation), txLocation))) { 595 assertTrue("The sparql update did not succeed within a transaction", dataset.asDatasetGraph().contains(ANY, 596 createURI(newObjectLocation), title.asNode(), createLiteral(newTitle))); 597 } 598 599 // Verify that the change is not visible outside the TX 600 try (final CloseableDataset dataset = getDataset(new HttpGet(newObjectLocation))) { 601 assertFalse("Sparql update changes must not be visible out of tx", dataset.asDatasetGraph().contains(ANY, 602 createURI(newObjectLocation), title.asNode(), createLiteral(newTitle))); 603 } 604 605 /* commit */ 606 assertEquals(NO_CONTENT.getStatusCode(), getStatus(new HttpPut(txLocation))); 607 608 /* it must exist after commit */ 609 try (final CloseableDataset dataset = getDataset(new HttpGet(newObjectLocation))) { 610 assertTrue("The inserted triple does not exist after the transaction has committed", 611 dataset.asDatasetGraph().contains(ANY, ANY, title.asNode(), createLiteral(newTitle))); 612 } 613 } 614 615 @Test 616 public void testGetNonExistingObject() throws IOException { 617 final String txLocation = createTransaction(); 618 final String newObjectLocation = serverAddress + "idontexist"; 619 assertEquals("Status should be NOT FOUND", NOT_FOUND.getStatusCode(), 620 getStatus(addTxTo(new HttpGet(newObjectLocation), txLocation))); 621 } 622 623 /* 624 * Caching headers should now be present during transactions. They were not in previous modeshape based versions. 625 */ 626 @Test 627 public void testCachingHeadersDuringTransaction() throws IOException { 628 final String txLocation = createTransaction(); 629 final String location; 630 try (final CloseableHttpResponse resp = execute(addTxTo(new HttpPost(serverAddress), txLocation))) { 631 assertTrue("Last-Modified must be present during a transaction", resp.containsHeader("Last-Modified")); 632 assertTrue("ETag must be present during a transaction", resp.containsHeader("ETag")); 633 assertTrue("Expected an X-State-Token header", resp.getHeaders("X-State-Token").length > 0); 634 // Assert Cache-Control headers are present to invalidate caches 635 location = getLocation(resp); 636 } 637 try (final CloseableHttpResponse resp = execute(addTxTo(new HttpGet(location), txLocation))) { 638 assertTrue("Last-Modified must be present during a transaction", resp.containsHeader("Last-Modified")); 639 assertTrue("ETag must be present during a transaction", resp.containsHeader("ETag")); 640 assertTrue("Expected an X-State-Token header", resp.getHeaders("X-State-Token").length > 0); 641 final Header[] headers = resp.getHeaders(CACHE_CONTROL); 642 assertEquals("Two cache control headers expected: ", 2, headers.length); 643 assertEquals("must-revalidate expected", "must-revalidate", headers[0].getValue()); 644 assertEquals("max-age=0 expected", "max-age=0", headers[1].getValue()); 645 consume(resp.getEntity()); 646 } 647 } 648 649 /** 650 * Test for issue https://jira.duraspace.org/browse/FCREPO-2975 651 * @throws java.lang.Exception exception thrown during this function 652 */ 653 @Test 654 public void testHeadAndDeleteInTransaction() throws Exception { 655 final String id = getRandomUniqueId(); 656 createObject(id); 657 final String objUri = serverAddress + "/" + id; 658 659 try (final CloseableHttpResponse resp = execute(new HttpHead(objUri))) { 660 assertEquals(OK.getStatusCode(), resp.getStatusLine().getStatusCode()); 661 } 662 663 final String txLocation = createTransaction(); 664 665 // Make a head request against the object within the transaction 666 try (final CloseableHttpResponse resp = execute(addTxTo(new HttpHead(objUri), txLocation))) { 667 assertEquals(OK.getStatusCode(), resp.getStatusLine().getStatusCode()); 668 } 669 670 // Delete the binary within the transaction 671 try (final CloseableHttpResponse resp = execute(addTxTo(new HttpDelete(objUri), txLocation))) { 672 assertEquals(NO_CONTENT.getStatusCode(), resp.getStatusLine().getStatusCode()); 673 } 674 675 // Commit the transaction containing deletion 676 assertEquals(NO_CONTENT.getStatusCode(), getStatus(new HttpPut(txLocation))); 677 } 678 679 /** 680 * Tests that transactions are treated as atomic with regards to nodes. A common use case for applications written 681 * against fedora is that an operation checks some property of a fedora object and acts on it accordingly. In 682 * order for this to work in a multi-client or multi-threaded environment that comparison+action combination needs 683 * to be atomic. Imagine a scenario where we have one process that deletes all objects in the repository that 684 * don't have a "preserve" property set to the literal "true", and we have any number of other clients that add 685 * such a property. We want to ensure that there is no way for a client to successfully add this property between 686 * when the "deleter" process has determined that no such property exists and when it deletes the object. In other 687 * words, if there are only clients adding properties and the "deleter" deleting objects it should not be possible 688 * for an object to be deleted if a client has added a title and received a successful http response code. 689 * 690 * @throws IOException exception thrown during this function 691 */ 692 @Test 693 public void testTransactionAndConcurrentConflictingUpdate() throws IOException { 694 final String preserveProperty = "preserve"; 695 final String preserveValue = "true"; 696 697 /* create the object in question */ 698 final String objectLocation; 699 try (final var response = execute(new HttpPost(serverAddress))) { 700 assertEquals(CREATED.getStatusCode(), getStatus(response)); 701 objectLocation = getLocation(response); 702 } 703 704 /* create the deleter transaction */ 705 final String deleterTxLocation = createTransaction(); 706 707 /* assert that the object is eligible for delete in the transaction */ 708 verifyProperty("No preserve property should be set!", objectLocation, deleterTxLocation, preserveProperty, 709 preserveValue, false); 710 711 /* delete that object in the transaction */ 712 final var delete = new HttpDelete(objectLocation); 713 addTxTo(delete, deleterTxLocation); 714 assertEquals(NO_CONTENT.getStatusCode(), getStatus(delete)); 715 716 /* fetch the object-deleted-in-tx outside of the tx */ 717 assertEquals("Expected to find our object outside the scope of the tx," 718 + " despite it being deleted in the uncommitted transaction.", 719 OK.getStatusCode(), getStatus(new HttpGet(objectLocation))); 720 721 /* Try to mark the object as not deletable outside the context of the transaction */ 722 final HttpPatch postProp = new HttpPatch(objectLocation); 723 postProp.setHeader(CONTENT_TYPE, "application/sparql-update"); 724 final String updateString = 725 "INSERT { <" + objectLocation + 726 "> <" + preserveProperty + "> " + preserveValue + " } WHERE { }"; 727 postProp.setEntity(new StringEntity(updateString, UTF_8)); 728 assertEquals(CONFLICT.getStatusCode(), getStatus(postProp)); 729 730 /* commit that transaction */ 731 assertEquals("Transaction is still atomic with regards to the object!", 732 NO_CONTENT.getStatusCode(), getStatus(new HttpPut(deleterTxLocation))); 733 } 734 735 @Test 736 public void testRequestResourceInvalidTx() throws Exception { 737 /* create a tx */ 738 final String txLocation = createTransaction(); 739 740 // Commit tx 741 assertEquals(NO_CONTENT.getStatusCode(), getStatus(new HttpPut(txLocation))); 742 743 // Attempt to create object inside completed tx 744 final HttpPost postNew = new HttpPost(serverAddress); 745 postNew.addHeader(ATOMIC_ID_HEADER, txLocation); 746 assertEquals(Status.CONFLICT.getStatusCode(), getStatus(postNew)); 747 } 748 749 @Test 750 public void testRequestWithBareTxUuid() throws Exception { 751 final String txLocation = createTransaction(); 752 final String uuid = txLocation.substring(txLocation.lastIndexOf("/") + 1); 753 754 final String newLocation; 755 // Attempt to create object in tx using just the uuid 756 final HttpPost postNew = addTxTo(new HttpPost(serverAddress), uuid); 757 try (final CloseableHttpResponse resp = execute(postNew)) { 758 assertEquals(CREATED.getStatusCode(), getStatus(resp)); 759 newLocation = getLocation(resp); 760 } 761 762 // Retrieve in tx using uuid 763 assertEquals(OK.getStatusCode(), getStatus(addTxTo(new HttpGet(newLocation), uuid))); 764 765 // Commit tx 766 assertEquals(NO_CONTENT.getStatusCode(), getStatus(new HttpPut(txLocation))); 767 768 // Retrieve outside of tx 769 assertEquals(OK.getStatusCode(), getStatus(new HttpGet(newLocation))); 770 } 771 772 @Test 773 public void testRequestWitMadeUpTxUuid() throws Exception { 774 // Attempt to create object inside completed tx 775 final HttpPost postNew = new HttpPost(serverAddress); 776 postNew.addHeader(ATOMIC_ID_HEADER, UUID.randomUUID().toString()); 777 assertEquals(Status.CONFLICT.getStatusCode(), getStatus(postNew)); 778 } 779 780 /** 781 * Test creating and deleting an object in a single transaction. 782 * @throws Exception http client might throw an exception. 783 */ 784 @Test 785 public void testCreateAndDeleteInSingleTransaction() throws Exception { 786 // Do an RDF Container 787 final String txLocation = createTransaction(); 788 final HttpPost post = postObjMethod(); 789 addTxTo(post, txLocation); 790 final String containerUri; 791 try (final CloseableHttpResponse response = execute(post)) { 792 assertEquals(CREATED.getStatusCode(), getStatus(response)); 793 containerUri = getLocation(response); 794 } 795 final HttpDelete delete = new HttpDelete(containerUri); 796 addTxTo(delete, txLocation); 797 assertEquals(NO_CONTENT.getStatusCode(), getStatus(delete)); 798 799 // Container was never committed, so we get a 404 instead of 410. 800 final HttpGet getContainer = new HttpGet(containerUri); 801 addTxTo(getContainer, txLocation); 802 assertEquals(NOT_FOUND.getStatusCode(), getStatus(getContainer)); 803 804 // Now do a binary 805 final HttpPost postBin = postObjMethod(); 806 addTxTo(postBin, txLocation); 807 postBin.setEntity(new StringEntity("Some test text")); 808 final String binaryUri; 809 try (final CloseableHttpResponse response = execute(postBin)) { 810 assertEquals(CREATED.getStatusCode(), getStatus(response)); 811 binaryUri = getLocation(response); 812 } 813 final HttpDelete deleteBin = new HttpDelete(binaryUri); 814 addTxTo(deleteBin, txLocation); 815 assertEquals(NO_CONTENT.getStatusCode(), getStatus(deleteBin)); 816 817 // Container was never committed, so we get a 404 instead of 410. 818 final HttpGet getBinary = new HttpGet(binaryUri); 819 addTxTo(getBinary, txLocation); 820 assertEquals(NOT_FOUND.getStatusCode(), getStatus(getBinary)); 821 } 822 823 /** 824 * Test creating an AG and child container in a transaction and deleting the child container. 825 * @throws Exception http client might throw an exception. 826 */ 827 @Test 828 public void testCreateAndDeleteInSingleTransactionSubPathFull() throws Exception { 829 final String txLocation = createTransaction(); 830 // Create an Archival Group. 831 final HttpPost httpPost = postObjMethod(); 832 addTxTo(httpPost, txLocation); 833 httpPost.setHeader("Link", ARCHIVAL_GROUP_TYPE); 834 final String parent; 835 try (final CloseableHttpResponse response = execute(httpPost)) { 836 assertEquals(CREATED.getStatusCode(), getStatus(response)); 837 parent = getLocation(response); 838 } 839 // Test GET the parent. 840 final HttpGet parentGet = new HttpGet(parent); 841 addTxTo(parentGet, txLocation); 842 assertEquals(OK.getStatusCode(), getStatus(parentGet)); 843 844 // Create a container child of the AG. 845 final HttpPost postChild = new HttpPost(parent); 846 addTxTo(postChild, txLocation); 847 final String id; 848 try (final CloseableHttpResponse response = execute(postChild)) { 849 assertEquals(CREATED.getStatusCode(), getStatus(response)); 850 id = getLocation(response); 851 } 852 // Test GET the child. 853 final HttpGet childGetContainer = new HttpGet(id); 854 addTxTo(childGetContainer, txLocation); 855 assertEquals(OK.getStatusCode(), getStatus(childGetContainer)); 856 // Delete the child container. 857 final HttpDelete deleteContainer = new HttpDelete(id); 858 addTxTo(deleteContainer, txLocation); 859 assertEquals(NO_CONTENT.getStatusCode(), getStatus(deleteContainer)); 860 // Test GET the child again. 861 final HttpGet childGetContainer2 = new HttpGet(id); 862 addTxTo(childGetContainer2, txLocation); 863 assertEquals(NOT_FOUND.getStatusCode(), getStatus(childGetContainer2)); 864 865 // Create a binary child of the AG. 866 final HttpPost postBinary = new HttpPost(parent); 867 addTxTo(postBinary, txLocation); 868 final String binaryId; 869 try (final CloseableHttpResponse response = execute(postBinary)) { 870 assertEquals(CREATED.getStatusCode(), getStatus(response)); 871 binaryId = getLocation(response); 872 } 873 // Test GET the child. 874 final HttpGet childGetBinary = new HttpGet(binaryId); 875 addTxTo(childGetBinary, txLocation); 876 assertEquals(OK.getStatusCode(), getStatus(childGetBinary)); 877 // Delete the child container. 878 final HttpDelete deleteBinary = new HttpDelete(binaryId); 879 addTxTo(deleteBinary, txLocation); 880 assertEquals(NO_CONTENT.getStatusCode(), getStatus(deleteBinary)); 881 // Test GET the child again. 882 final HttpGet childGetBinary2 = new HttpGet(binaryId); 883 addTxTo(childGetBinary2, txLocation); 884 assertEquals(NOT_FOUND.getStatusCode(), getStatus(childGetBinary2)); 885 } 886 887 888 /** 889 * Test creating an AG. Then create a child container and delete it in the same transaction. 890 * @throws Exception http client might throw an exception. 891 */ 892 @Test 893 public void testCreateAndDeleteInSingleTransactionSubPathPartial() throws Exception { 894 // Create an Archival Group. 895 final HttpPost httpPost = postObjMethod(); 896 httpPost.setHeader("Link", ARCHIVAL_GROUP_TYPE); 897 final String parent; 898 try (final CloseableHttpResponse response = execute(httpPost)) { 899 assertEquals(CREATED.getStatusCode(), getStatus(response)); 900 parent = getLocation(response); 901 } 902 assertEquals(OK.getStatusCode(), getStatus(new HttpGet(parent))); 903 904 final String txLocation = createTransaction(); 905 // Create a child RDF container. 906 final HttpPost postContainer = new HttpPost(parent); 907 addTxTo(postContainer, txLocation); 908 final String containerId; 909 try (final CloseableHttpResponse response = execute(postContainer)) { 910 assertEquals(CREATED.getStatusCode(), getStatus(response)); 911 containerId = getLocation(response); 912 } 913 // Test GET the container. 914 final HttpGet getContainer = new HttpGet(containerId); 915 addTxTo(getContainer, txLocation); 916 assertEquals(OK.getStatusCode(), getStatus(getContainer)); 917 // Delete the container. 918 final HttpDelete deleteContainer = new HttpDelete(containerId); 919 addTxTo(deleteContainer, txLocation); 920 assertEquals(NO_CONTENT.getStatusCode(), getStatus(deleteContainer)); 921 // Test GET the container again. 922 final HttpGet childGetContainer2 = new HttpGet(containerId); 923 addTxTo(childGetContainer2, txLocation); 924 assertEquals(NOT_FOUND.getStatusCode(), getStatus(childGetContainer2)); 925 926 // Create a child binary. 927 final HttpPost postBinary = new HttpPost(parent); 928 addTxTo(postBinary, txLocation); 929 postBinary.setEntity(new StringEntity("Some test text")); 930 final String binaryId; 931 try (final CloseableHttpResponse response = execute(postBinary)) { 932 assertEquals(CREATED.getStatusCode(), getStatus(response)); 933 binaryId = getLocation(response); 934 } 935 // Test GET the binary. 936 final HttpGet getBinary = new HttpGet(binaryId); 937 addTxTo(getBinary, txLocation); 938 assertEquals(OK.getStatusCode(), getStatus(getBinary)); 939 // Delete the binary. 940 final HttpDelete deleteBinary = new HttpDelete(binaryId); 941 addTxTo(deleteBinary, txLocation); 942 assertEquals(NO_CONTENT.getStatusCode(), getStatus(deleteBinary)); 943 // Test GET the binary again. 944 final HttpGet childGetBinary2 = new HttpGet(binaryId); 945 addTxTo(childGetBinary2, txLocation); 946 assertEquals(NOT_FOUND.getStatusCode(), getStatus(childGetBinary2)); 947 } 948 949 @Test 950 public void lockAgResourceInTxWhenAgPartIsUpdated() throws Exception { 951 final var agId = getRandomUniqueId(); 952 final var childId = agId + "/child"; 953 final var binaryId = agId + "/child/bin"; 954 955 putAg(agId, null); 956 putContainer(childId, null); 957 putBinary(binaryId, null, "binary"); 958 959 final String txLocation = createTransaction(); 960 961 putBinary(binaryId, txLocation, "binary - updated!"); 962 963 assertConcurrentUpdate(() -> putBinary(binaryId, null, "concurrent update!")); 964 assertConcurrentUpdate(() -> updateContainerTitle(childId, "concurrent update!", null)); 965 assertConcurrentUpdate(() -> updateContainerTitle(agId, "concurrent update!", null)); 966 assertConcurrentUpdate(() -> putContainer(agId + "/child2", null)); 967 968 commitTransaction(txLocation); 969 970 assertEquals("binary - updated!", getResource(binaryId, null)); 971 972 putBinary(binaryId, null, "unlocked!"); 973 assertEquals("unlocked!", getResource(binaryId, null)); 974 } 975 976 @Test 977 public void lockBothBinaryAndDescWhenEitherIsUpdatedInTx() throws Exception { 978 final var binaryId = getRandomUniqueId(); 979 980 putBinary(binaryId, null, "binary"); 981 982 final String txLocation = createTransaction(); 983 984 putBinary(binaryId, txLocation, "binary - updated!"); 985 986 assertConcurrentUpdate(() -> putBinary(binaryId, null, "concurrent update!")); 987 assertConcurrentUpdate(() -> updateContainerTitle(binaryId + "/fcr:metadata", "concurrent update!", null)); 988 989 commitTransaction(txLocation); 990 991 assertEquals("binary - updated!", getResource(binaryId, null)); 992 } 993 994 @Test 995 public void concurrentSparqlUpdatesShouldNotBeAllowed() throws Exception { 996 final var containerId = getRandomUniqueId(); 997 998 putContainer(containerId, null); 999 1000 final String txLocation = createTransaction(); 1001 1002 updateContainerTitle(containerId, "new title", txLocation); 1003 1004 assertConcurrentUpdate(() -> updateContainerTitle(containerId, "concurrent update!", null)); 1005 1006 commitTransaction(txLocation); 1007 1008 final var response = getResource(containerId, null); 1009 assertTrue("title should have been updated", response.contains("new title")); 1010 assertFalse("concurrent update should not have been applied", response.contains("concurrent update!")); 1011 } 1012 1013 @Test 1014 public void testDeleteAndRecreateInTransaction() throws Exception { 1015 final String container; 1016 // Create a container 1017 try (final var response = execute(postObjMethod())) { 1018 assertEquals(CREATED.getStatusCode(), getStatus(response)); 1019 container = getLocation(response); 1020 } 1021 final String txLocation = createTransaction(); 1022 // Delete in a transaction 1023 final var deleteInTx = new HttpDelete(container); 1024 addTxTo(deleteInTx, txLocation); 1025 assertEquals(NO_CONTENT.getStatusCode(), getStatus(deleteInTx)); 1026 // Ensure it is gone in the transaction 1027 final var getInTx = new HttpDelete(container); 1028 addTxTo(getInTx, txLocation); 1029 assertEquals(GONE.getStatusCode(), getStatus(getInTx)); 1030 // Delete the tombstone in the transaction 1031 final var deleteTombInTx = new HttpDelete(container + "/" + FCR_TOMBSTONE); 1032 addTxTo(deleteTombInTx, txLocation); 1033 assertEquals(NO_CONTENT.getStatusCode(), getStatus(deleteTombInTx)); 1034 // Ensure the container is completely gone 1035 assertEquals(NOT_FOUND.getStatusCode(), getStatus(getInTx)); 1036 // Inside the transaction create the same container 1037 final var putInTx = new HttpPut(container); 1038 addTxTo(putInTx, txLocation); 1039 assertEquals(CREATED.getStatusCode(), getStatus(putInTx)); 1040 // Commit the transaction 1041 assertEquals(NO_CONTENT.getStatusCode(), getStatus(new HttpPut(txLocation))); 1042 } 1043 1044 private void assertConcurrentUpdate(final CheckedRunnable runnable) throws Exception { 1045 try { 1046 runnable.run(); 1047 fail("Request should fail because the resource should be locked by another transaction."); 1048 } catch (final HttpResponseException e) { 1049 assertEquals(CONFLICT.getStatusCode(), e.getStatusCode()); 1050 assertTrue("concurrent update exception", 1051 e.getReasonPhrase().contains("updated by another transaction")); 1052 } 1053 } 1054 1055 private void verifyProperty(final String assertionMessage, final String uri, final String txId, 1056 final String propertyUri, final String propertyValue, final boolean shouldExist) throws IOException { 1057 final HttpGet getObjCommitted = new HttpGet(uri); 1058 if (txId != null) { 1059 addTxTo(getObjCommitted, txId); 1060 } 1061 try (final CloseableDataset dataset = getDataset(getObjCommitted)) { 1062 final boolean exists = dataset.asDatasetGraph().contains(ANY, 1063 createURI(serverAddress + uri), createURI(propertyUri), createLiteral(propertyValue)); 1064 if (shouldExist) { 1065 assertTrue(assertionMessage, exists); 1066 } else { 1067 assertFalse(assertionMessage, exists); 1068 } 1069 } 1070 } 1071 1072 private void assertObjectDoesNotExistOnDisk(final FedoraId fedoraId) { 1073 assertFalse(String.format("Expected %s to not exist on disk", fedoraId), objectExistsOnDisk(fedoraId)); 1074 } 1075 1076 private void assertObjectExistsOnDisk(final FedoraId fedoraId) { 1077 assertTrue(String.format("Expected %s to exist on disk", fedoraId), objectExistsOnDisk(fedoraId)); 1078 } 1079 1080 private boolean objectExistsOnDisk(final FedoraId fedoraId) { 1081 try (final var session = objectSessionFactory.newSession(fedoraId.getResourceId())) { 1082 return session.containsResource(fedoraId.getResourceId()); 1083 } 1084 } 1085 1086 private void assertBinaryContent(final String expected, 1087 final String id, 1088 final String txLocation) throws IOException { 1089 final var actual = getResource(id, txLocation); 1090 assertEquals("Expected binary content for " + id, expected, actual); 1091 } 1092 1093 private void putAg(final String id, final String txLocation) throws IOException { 1094 final var put = putObjMethod(id); 1095 1096 if (txLocation != null) { 1097 put.addHeader(ATOMIC_ID_HEADER, txLocation); 1098 } 1099 1100 put.setHeader("Link", ARCHIVAL_GROUP_TYPE); 1101 try (final CloseableHttpResponse response = execute(put)) { 1102 assertEquals(CREATED.getStatusCode(), getStatus(response)); 1103 } 1104 } 1105 1106 private void putContainer(final String id, final String txLocation) throws IOException { 1107 final var put = putObjMethod(id); 1108 if (txLocation != null) { 1109 put.addHeader(ATOMIC_ID_HEADER, txLocation); 1110 } 1111 try (final CloseableHttpResponse resp = execute(put)) { 1112 final var code = getStatus(resp); 1113 if (code != CREATED.getStatusCode()) { 1114 throw new HttpResponseException(code, EntityUtils.toString(resp.getEntity())); 1115 } 1116 } 1117 } 1118 1119 private void updateContainerTitle(final String id, final String title, final String txLocation) throws IOException { 1120 final var patch = patchObjMethod(id); 1121 patch.addHeader(CONTENT_TYPE, "application/sparql-update"); 1122 patch.setEntity(new StringEntity("INSERT { <> <http://purl.org/dc/elements/1.1/title> \"" + title + 1123 "\" } WHERE {}")); 1124 if (txLocation != null) { 1125 patch.addHeader(ATOMIC_ID_HEADER, txLocation); 1126 } 1127 try (final CloseableHttpResponse resp = execute(patch)) { 1128 final var code = getStatus(resp); 1129 if (code != NO_CONTENT.getStatusCode()) { 1130 throw new HttpResponseException(code, EntityUtils.toString(resp.getEntity())); 1131 } 1132 } 1133 } 1134 1135 private void putBinary(final String id, final String txLocation, final String content) throws IOException { 1136 final HttpPut put = new HttpPut(serverAddress + id); 1137 put.setEntity(new StringEntity(content == null ? "" : content)); 1138 put.setHeader(CONTENT_TYPE, TEXT_PLAIN); 1139 put.setHeader(LINK, NON_RDF_SOURCE_LINK_HEADER); 1140 if (txLocation != null) { 1141 put.addHeader(ATOMIC_ID_HEADER, txLocation); 1142 } 1143 try (final CloseableHttpResponse resp = execute(put)) { 1144 final var code = getStatus(resp); 1145 if (code != CREATED.getStatusCode() && code != NO_CONTENT.getStatusCode()) { 1146 throw new HttpResponseException(code, EntityUtils.toString(resp.getEntity())); 1147 } 1148 } 1149 } 1150 1151 private String getResource(final String id, final String txLocation) throws IOException { 1152 final var get = new HttpGet(serverAddress + id); 1153 if (txLocation != null) { 1154 get.addHeader(ATOMIC_ID_HEADER, txLocation); 1155 } 1156 try (final CloseableHttpResponse response = execute(get)) { 1157 final HttpEntity entity = response.getEntity(); 1158 final String content = EntityUtils.toString(entity); 1159 assertEquals(OK.getStatusCode(), response.getStatusLine().getStatusCode()); 1160 return content; 1161 } 1162 } 1163 1164 private void commitTransaction(final String txLocation) throws IOException { 1165 try (final CloseableHttpResponse resp = execute(new HttpPut(txLocation))) { 1166 final var code = getStatus(resp); 1167 if (code != NO_CONTENT.getStatusCode()) { 1168 throw new HttpResponseException(code, EntityUtils.toString(resp.getEntity())); 1169 } 1170 } 1171 } 1172 1173 private void addConflictingContainmentRecord(final String resourceId) { 1174 final var txId = UUID.randomUUID().toString(); 1175 final var tx = mock(Transaction.class); 1176 when(tx.getId()).thenReturn(txId); 1177 when(tx.isShortLived()).thenReturn(true); 1178 containmentIndex.addContainedBy(tx, FedoraId.getRepositoryRootId(), FedoraId.create(resourceId)); 1179 containmentIndex.commitTransaction(tx); 1180 } 1181 1182 private void corruptStagedBinary(final String resourceId) { 1183 final var lastPart = resourceId.contains("/") ? 1184 StringUtils.substringAfterLast(resourceId, "/") : resourceId; 1185 final var stagingRoot = ocflConfig.getFedoraOcflStaging(); 1186 try { 1187 final var binary = Files.find(stagingRoot, 10, (file, attrs) -> { 1188 return attrs.isRegularFile() && 1189 file.getFileName().toString().equals(lastPart); 1190 }).findFirst().get(); 1191 1192 Files.writeString(binary, "corrupted!"); 1193 } catch (final IOException e) { 1194 throw new UncheckedIOException(e); 1195 } 1196 } 1197 1198}