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.nio.charset.StandardCharsets.UTF_8; 021import static java.time.format.DateTimeFormatter.ISO_INSTANT; 022import static java.util.Arrays.asList; 023import static java.util.Arrays.sort; 024import static javax.ws.rs.core.HttpHeaders.ACCEPT; 025import static javax.ws.rs.core.HttpHeaders.CONTENT_LENGTH; 026import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE; 027import static javax.ws.rs.core.HttpHeaders.LINK; 028import static javax.ws.rs.core.HttpHeaders.LOCATION; 029import static javax.ws.rs.core.Response.Status.BAD_REQUEST; 030import static javax.ws.rs.core.Response.Status.CREATED; 031import static javax.ws.rs.core.Response.Status.FOUND; 032import static javax.ws.rs.core.Response.Status.GONE; 033import static javax.ws.rs.core.Response.Status.METHOD_NOT_ALLOWED; 034import static javax.ws.rs.core.Response.Status.NOT_ACCEPTABLE; 035import static javax.ws.rs.core.Response.Status.NOT_FOUND; 036import static javax.ws.rs.core.Response.Status.NO_CONTENT; 037import static javax.ws.rs.core.Response.Status.OK; 038import static org.apache.http.HttpStatus.SC_BAD_REQUEST; 039import static org.apache.jena.graph.Node.ANY; 040import static org.apache.jena.graph.NodeFactory.createLiteral; 041import static org.apache.jena.graph.NodeFactory.createURI; 042import static org.apache.jena.rdf.model.ModelFactory.createDefaultModel; 043import static org.apache.jena.rdf.model.ResourceFactory.createProperty; 044import static org.apache.jena.rdf.model.ResourceFactory.createResource; 045import static org.apache.jena.vocabulary.DC_11.title; 046import static org.apache.jena.vocabulary.RDF.type; 047import static org.fcrepo.http.api.FedoraLdp.ACCEPT_DATETIME; 048import static org.fcrepo.http.api.FedoraVersioning.MEMENTO_DATETIME_HEADER; 049import static org.fcrepo.http.commons.domain.RDFMediaType.APPLICATION_LINK_FORMAT; 050import static org.fcrepo.http.commons.domain.RDFMediaType.N3; 051import static org.fcrepo.http.commons.domain.RDFMediaType.POSSIBLE_RDF_RESPONSE_VARIANTS_STRING; 052import static org.fcrepo.http.commons.domain.RDFMediaType.TURTLE; 053import static org.fcrepo.kernel.api.FedoraTypes.FCR_ACL; 054import static org.fcrepo.kernel.api.FedoraTypes.FCR_VERSIONS; 055import static org.fcrepo.kernel.api.RdfLexicon.ARCHIVAL_GROUP; 056import static org.fcrepo.kernel.api.RdfLexicon.BASIC_CONTAINER; 057import static org.fcrepo.kernel.api.RdfLexicon.CONTAINER; 058import static org.fcrepo.kernel.api.RdfLexicon.CONTAINS; 059import static org.fcrepo.kernel.api.RdfLexicon.EMBED_CONTAINED; 060import static org.fcrepo.kernel.api.RdfLexicon.FEDORA_BINARY; 061import static org.fcrepo.kernel.api.RdfLexicon.MEMENTO_TYPE; 062import static org.fcrepo.kernel.api.RdfLexicon.NON_RDF_SOURCE; 063import static org.fcrepo.kernel.api.RdfLexicon.RDF_SOURCE; 064import static org.fcrepo.kernel.api.RdfLexicon.RESOURCE; 065import static org.fcrepo.kernel.api.RdfLexicon.VERSIONED_RESOURCE; 066import static org.fcrepo.kernel.api.RdfLexicon.VERSIONING_TIMEMAP_TYPE; 067import static org.fcrepo.kernel.api.services.VersionService.MEMENTO_LABEL_FORMATTER; 068import static org.fcrepo.kernel.api.services.VersionService.MEMENTO_RFC_1123_FORMATTER; 069import static org.junit.Assert.assertArrayEquals; 070import static org.junit.Assert.assertEquals; 071import static org.junit.Assert.assertFalse; 072import static org.junit.Assert.assertNotEquals; 073import static org.junit.Assert.assertNotNull; 074import static org.junit.Assert.assertNull; 075import static org.junit.Assert.assertTrue; 076 077import java.io.IOException; 078import java.io.StringWriter; 079import java.time.Duration; 080import java.time.Instant; 081import java.time.OffsetDateTime; 082import java.time.ZoneOffset; 083import java.time.temporal.ChronoUnit; 084import java.time.temporal.TemporalAccessor; 085import java.util.ArrayList; 086import java.util.Comparator; 087import java.util.Iterator; 088import java.util.List; 089import java.util.concurrent.TimeUnit; 090import java.util.stream.Collectors; 091 092import javax.ws.rs.core.Link; 093 094import org.fcrepo.http.commons.test.util.CloseableDataset; 095import org.fcrepo.storage.ocfl.CommitType; 096import org.fcrepo.storage.ocfl.DefaultOcflObjectSessionFactory; 097 098import org.apache.commons.io.IOUtils; 099import org.apache.http.HttpResponse; 100import org.apache.http.client.methods.CloseableHttpResponse; 101import org.apache.http.client.methods.HttpDelete; 102import org.apache.http.client.methods.HttpGet; 103import org.apache.http.client.methods.HttpHead; 104import org.apache.http.client.methods.HttpOptions; 105import org.apache.http.client.methods.HttpPatch; 106import org.apache.http.client.methods.HttpPost; 107import org.apache.http.client.methods.HttpPut; 108import org.apache.http.client.methods.HttpUriRequest; 109import org.apache.http.entity.StringEntity; 110import org.apache.http.impl.client.CloseableHttpClient; 111import org.apache.http.util.EntityUtils; 112import org.apache.jena.graph.Node; 113import org.apache.jena.query.Dataset; 114import org.apache.jena.rdf.model.Model; 115import org.apache.jena.rdf.model.Property; 116import org.apache.jena.rdf.model.Resource; 117import org.apache.jena.riot.Lang; 118import org.apache.jena.riot.RDFDataMgr; 119import org.apache.jena.riot.RDFFormat; 120import org.apache.jena.riot.RDFLanguages; 121import org.apache.jena.sparql.core.DatasetGraph; 122import org.apache.jena.sparql.core.Quad; 123import org.apache.jena.vocabulary.DC; 124import org.apache.jena.vocabulary.RDF; 125import org.junit.After; 126import org.junit.Before; 127import org.junit.Rule; 128import org.junit.Test; 129import org.junit.rules.TemporaryFolder; 130import org.springframework.test.context.TestExecutionListeners; 131 132import com.google.common.collect.ImmutableList; 133 134/** 135 * @author lsitu 136 * @author bbpennel 137 */ 138@TestExecutionListeners( 139 listeners = { TestIsolationExecutionListener.class }, 140 mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS) 141public class FedoraVersioningIT extends AbstractResourceIT { 142 143 private static final String BINARY_CONTENT = "binary content"; 144 private static final String BINARY_UPDATED = "updated content"; 145 146 private static final String OCTET_STREAM_TYPE = "application/octet-stream"; 147 148 private static final Node MEMENTO_TYPE_NODE = createURI(MEMENTO_TYPE); 149 150 private static final Property TEST_PROPERTY = createProperty("info:test#label"); 151 152 private static final Resource TEST_TYPE_RESOURCE = createResource("http://example.com/custom_type"); 153 154 private final List<String> rdfTypes = new ArrayList<>(asList(POSSIBLE_RDF_RESPONSE_VARIANTS_STRING)); 155 156 private String subjectUri; 157 private String id; 158 159 @Rule 160 public TemporaryFolder tmpDir = new TemporaryFolder(); 161 162 private DefaultOcflObjectSessionFactory objectSessionFactory; 163 164 @Before 165 public void init() { 166 id = getRandomUniqueId(); 167 subjectUri = serverAddress + id; 168 objectSessionFactory = getBean(DefaultOcflObjectSessionFactory.class); 169 } 170 171 @After 172 public void after() { 173 objectSessionFactory.setDefaultCommitType(CommitType.NEW_VERSION); 174 } 175 176 @Test 177 public void testDeleteTimeMapNotAllowed() throws Exception { 178 createVersionedContainer(id); 179 final String timeMapUri = subjectUri + "/" + FCR_VERSIONS; 180 assertEquals(200, getStatus(new HttpGet(timeMapUri))); 181 // disabled versioning to delete TimeMap 182 assertEquals(METHOD_NOT_ALLOWED.getStatusCode(), 183 getStatus(new HttpDelete(serverAddress + id + "/" + FCR_VERSIONS))); 184 } 185 186 @Test 187 public void createMementoOnResourceWithNoUnversionedChanges() throws Exception { 188 final var v1 = now(); 189 createVersionedContainer(id); 190 TimeUnit.SECONDS.sleep(1); 191 192 final var v2 = now(); 193 createMemento(subjectUri); 194 195 verifyTimemapResponse(subjectUri, id, new String[]{v1, v2}, v1, v2); 196 } 197 198 @Test 199 public void getTimeMapForContainerWithSingleVersion() throws Exception { 200 createVersionedContainer(id); 201 verifyTimemapResponse(subjectUri, id, now()); 202 } 203 204 @Test 205 public void getTimeMapFromBinaryWithMultipleVersions() throws Exception { 206 final var v1 = now(); 207 createVersionedBinary(id, OCTET_STREAM_TYPE, "v1"); 208 TimeUnit.SECONDS.sleep(1); 209 210 final var v2 = now(); 211 putVersionedBinary(id, OCTET_STREAM_TYPE, "v2", true); 212 TimeUnit.SECONDS.sleep(1); 213 214 final var v3 = now(); 215 putVersionedBinary(id, OCTET_STREAM_TYPE, "v3", true); 216 TimeUnit.SECONDS.sleep(1); 217 218 verifyTimemapResponse(subjectUri, id, new String[]{v1, v2, v3}, v1, v3); 219 } 220 221 @Test 222 public void getTimeMapFromAgWithChildrenWithDifferentVersions() throws Exception { 223 final var childId1 = id + "/child1"; 224 final var childId2 = id + "/child2"; 225 226 final var v1 = now(); 227 createVersionedArchivalGroup(id); 228 TimeUnit.SECONDS.sleep(1); 229 230 final var v2 = now(); 231 putVersionedBinary(childId1, OCTET_STREAM_TYPE, "v2", false); 232 TimeUnit.SECONDS.sleep(1); 233 234 final var v3 = now(); 235 putVersionedBinary(childId2, OCTET_STREAM_TYPE, "v3", false); 236 TimeUnit.SECONDS.sleep(1); 237 238 final var v4 = now(); 239 putVersionedBinary(childId1, OCTET_STREAM_TYPE, "v4", true); 240 TimeUnit.SECONDS.sleep(1); 241 242 verifyTimemapResponse(subjectUri, id, new String[]{v1, v2, v3, v4}, v1, v4); 243 verifyTimemapResponse(subjectUri + "/child1", childId1, new String[]{v2, v4}, v2, v4); 244 verifyTimemapResponse(subjectUri + "/child2", childId2, v3); 245 } 246 247 @Test 248 public void getMementoFromAgChild() throws Exception { 249 objectSessionFactory.setDefaultCommitType(CommitType.UNVERSIONED); 250 final var childId1 = id + "/child1"; 251 252 createVersionedArchivalGroup(id); 253 createMemento(subjectUri); 254 TimeUnit.SECONDS.sleep(1); 255 256 putVersionedBinary(childId1, OCTET_STREAM_TYPE, "v2", false); 257 final var mementoUri = createMemento(subjectUri); 258 final var mementoTime = mementoUri.substring(mementoUri.lastIndexOf("/")); 259 260 final HttpGet httpGet = new HttpGet(subjectUri + "/child1/fcr:versions" + mementoTime); 261 try (final CloseableHttpResponse response = execute(httpGet)) { 262 assertMementoDatetimeHeaderMatches(response, now()); 263 assertEquals("Binary content of memento must match original content", 264 "v2", EntityUtils.toString(response.getEntity())); 265 } 266 } 267 268 @Test 269 public void testGetTimeMapResponseMultipleMementos() throws Exception { 270 createVersionedContainer(id); 271 final var v1 = now(); 272 TimeUnit.SECONDS.sleep(1); 273 274 putVersionedContainer(id, "2", true); 275 final var v2 = now(); 276 TimeUnit.SECONDS.sleep(1); 277 278 putVersionedContainer(id, "3", true); 279 final var v3 = now(); 280 281 verifyTimemapResponse(subjectUri, id, new String[] {v1, v2, v3}, v1, v3); 282 } 283 284 @Test 285 public void testGetTimeMapRDFSubject() throws Exception { 286 createVersionedContainer(id); 287 288 final HttpGet httpGet = getObjMethod(id + "/" + FCR_VERSIONS); 289 290 try (final CloseableDataset dataset = getDataset(httpGet)) { 291 final DatasetGraph results = dataset.asDatasetGraph(); 292 final Node subject = createURI(subjectUri + "/" + FCR_VERSIONS); 293 assertTrue("Did not find correct subject", results.contains(ANY, subject, ANY, ANY)); 294 } 295 } 296 297 @Test 298 public void testCreateVersion() throws Exception { 299 createVersionedContainer(id); 300 301 final String mementoUri = createMemento(subjectUri); 302 assertMementoUri(mementoUri, subjectUri); 303 304 try (final CloseableDataset dataset = getDataset(new HttpGet(mementoUri))) { 305 final DatasetGraph results = dataset.asDatasetGraph(); 306 307 final Node mementoSubject = createURI(mementoUri); 308 309 assertFalse("Memento type should not be visible", 310 results.contains(ANY, mementoSubject, RDF.type.asNode(), MEMENTO_TYPE_NODE)); 311 312 assertMementoEqualsOriginal(mementoUri); 313 } 314 } 315 316 @Test 317 public void testCreateVersionWithAcceptDatetime() throws Exception { 318 final String mementoDateTime = 319 MEMENTO_RFC_1123_FORMATTER.format(ISO_INSTANT.parse("2017-06-10T11:41:00Z", Instant::from)); 320 createVersionedContainer(id); 321 final var post = postObjMethod(id + "/fcr:versions"); 322 post.addHeader(MEMENTO_DATETIME_HEADER, mementoDateTime); 323 try (final CloseableHttpResponse response = execute(post)) { 324 assertEquals("Operation should return " + SC_BAD_REQUEST + " status", SC_BAD_REQUEST, 325 response.getStatusLine().getStatusCode()); 326 assertConstrainedByPresent(response); 327 } 328 } 329 330 331 @Test 332 public void testCreateVersionFromResourceWithHashURI() throws Exception { 333 final HttpPost createMethod = postObjMethod(); 334 createMethod.addHeader("Slug", id); 335 createMethod.addHeader(CONTENT_TYPE, N3); 336 createMethod.setEntity(new StringEntity("<#test> <info:test#label> \"foo\"")); 337 338 assertEquals("Didn't get a CREATED response!", CREATED.getStatusCode(), getStatus(createMethod)); 339 340 final String mementoUri = createMemento(subjectUri); 341 assertMementoUri(mementoUri, subjectUri); 342 343 try (final CloseableDataset dataset = getDataset(new HttpGet(mementoUri))) { 344 final DatasetGraph results = dataset.asDatasetGraph(); 345 346 final Node mementoSubject = createURI(mementoUri); 347 348 assertFalse("Memento type should not be visible", 349 results.contains(ANY, mementoSubject, RDF.type.asNode(), MEMENTO_TYPE_NODE)); 350 351 assertMementoEqualsOriginal(mementoUri); 352 } 353 } 354 355 @Test 356 public void testCreateVersionFromResourceWithBlankNode() throws Exception { 357 final HttpPost createMethod = postObjMethod(); 358 createMethod.addHeader("Slug", id); 359 createMethod.addHeader(CONTENT_TYPE, TURTLE); 360 createMethod.setEntity(new StringEntity("<> <http://purl.org/dc/terms/subject> " + 361 "[ a <info:test#Something> ]")); 362 363 try (final CloseableHttpResponse response = execute(createMethod)) { 364 assertEquals("Didn't get a CREATED response!", CREATED.getStatusCode(), getStatus(response)); 365 } 366 367 final String mementoUri = createMemento(subjectUri); 368 assertMementoUri(mementoUri, subjectUri); 369 370 try (final CloseableDataset dataset = getDataset(new HttpGet(mementoUri))) { 371 final DatasetGraph results = dataset.asDatasetGraph(); 372 373 // Expect triple with Blank Node as Object 374 final Iterator<Quad> quads = 375 results.find(ANY, createURI(subjectUri), createURI("http://purl.org/dc/terms/subject"), ANY); 376 377 final List<Quad> quadList = ImmutableList.copyOf(quads); 378 assertEquals("Should only be one element: " + quadList.size(), 1, quadList.size()); 379 380 final Quad quad = quadList.get(0); 381 // The quad:Object is the subject of the Blank Node triple we are expecting 382 assertTrue("Should have found blank node triple", 383 results.contains(ANY, quad.getObject(), RDF.type.asNode(), createURI("info:test#Something"))); 384 } 385 } 386 387 @Test 388 public void testCreateVersionWithSlugHeader() throws Exception { 389 createVersionedContainer(id); 390 391 // Bad request with Slug header to create memento 392 final HttpPost post = postObjMethod(id + "/" + FCR_VERSIONS); 393 post.addHeader("Slug", "version_label"); 394 395 assertEquals("Created memento with Slug!", 396 BAD_REQUEST.getStatusCode(), getStatus(post)); 397 } 398 399 @Test 400 public void testCreateVersionWithMementoDateTimeHeader() throws Exception { 401 createVersionedContainer(id); 402 403 // Bad request with Memento-Datetime header to create memento 404 final String mementoDateTime = "Tue, 3 Jun 2008 11:05:30 GMT"; 405 final String body = createContainerMementoBodyContent(subjectUri, N3); 406 final HttpPost post = postObjMethod(id + "/" + FCR_VERSIONS); 407 408 post.addHeader(MEMENTO_DATETIME_HEADER, mementoDateTime); 409 post.addHeader(CONTENT_TYPE, N3); 410 post.setEntity(new StringEntity(body)); 411 412 assertEquals("Created memento with Memento-Datetime!", 413 BAD_REQUEST.getStatusCode(), getStatus(post)); 414 } 415 416 @Test 417 public void testMementoContainmentReferences() throws Exception { 418 createVersionedContainer(id); 419 TimeUnit.SECONDS.sleep(1); 420 421 final String childUri = subjectUri + "/x"; 422 createObjectAndClose(id + "/x"); 423 424 // create memento 425 final String mementoUri = createMemento(subjectUri); 426 427 TimeUnit.SECONDS.sleep(1); 428 // Remove the child resource 429 assertEquals("Expected delete to succeed", 430 NO_CONTENT.getStatusCode(), getStatus(new HttpDelete(childUri))); 431 432 // Ensure that the resource reference is gone 433 try (final CloseableHttpResponse getResponse1 = execute(new HttpGet(subjectUri)); 434 final CloseableDataset dataset = getDataset(getResponse1)) { 435 final DatasetGraph graph = dataset.asDatasetGraph(); 436 assertFalse("Expected NOT to have child resource: " + graph, graph.contains(ANY, 437 ANY, createURI(CONTAINS.getURI()), createURI(childUri))); 438 } 439 440 // Ensure that the resource reference is still in memento 441 try (final CloseableHttpResponse getResponse1 = execute(new HttpGet(mementoUri)); 442 final CloseableDataset dataset = getDataset(getResponse1)) { 443 final DatasetGraph graph = dataset.asDatasetGraph(); 444 assertTrue("Expected child resource NOT found: " + graph, graph.contains(ANY, 445 ANY, createURI(CONTAINS.getURI()), createURI(childUri))); 446 } 447 } 448 449 @Test 450 public void testHeadOnMemento() throws Exception { 451 452 createVersionedContainer(id); 453 final String mementoUri = createMemento(subjectUri); 454 455 // Status 200: HEAD request on existing memento 456 final HttpHead headMethod = new HttpHead(mementoUri); 457 assertEquals("Expected memento is NOT found: " + mementoUri, OK.getStatusCode(), getStatus(headMethod)); 458 459 // Status 404: HEAD request on absent memento 460 final HttpHead headMementoAbsent = headObjMethod(id + "/" + FCR_VERSIONS + "/20000101000001"); 461 assertEquals("Didn't get status 404 on absent memento!", 462 NOT_FOUND.getStatusCode(), getStatus(headMementoAbsent)); 463 464 // Status 400: HEAD request with invalid memento path 465 final HttpHead headMethodInvalid = headObjMethod(id + "/" + FCR_VERSIONS + "/any"); 466 checkResponseWithInvalidMementoID(headMethodInvalid); 467 } 468 469 @Test 470 public void testGetOnMemento() throws Exception { 471 472 createVersionedContainer(id); 473 final String mementoUri = createMemento(subjectUri); 474 475 // Status 200: GET request on existing memento 476 final HttpGet getMemento = new HttpGet(mementoUri); 477 assertEquals("Expected memento is NOT found: " + mementoUri, OK.getStatusCode(), getStatus(getMemento)); 478 479 // Status 404: GET request on absent memento 480 final HttpGet getMementoAbsent = getObjMethod(id + "/" + FCR_VERSIONS + "/20000101000001"); 481 assertEquals("Didn't get status 404 on absent memento!", 482 NOT_FOUND.getStatusCode(), getStatus(getMementoAbsent)); 483 484 // Status 400: GET request with invalid memento path 485 final HttpGet getMementoInvalid = getObjMethod(id + "/" + FCR_VERSIONS + "/any"); 486 checkResponseWithInvalidMementoID(getMementoInvalid); 487 } 488 489 @Test 490 public void testGetOnMementoWithAcceptDatetimePresent() throws Exception { 491 createVersionedContainer(id); 492 final String mementoDateTime = 493 MEMENTO_RFC_1123_FORMATTER.format(ISO_INSTANT.parse("2017-06-10T11:41:00Z", Instant::from)); 494 final String mementoUri = createMemento(subjectUri); 495 // Status 200: GET request on existing memento 496 final HttpGet getMemento = new HttpGet(mementoUri); 497 getMemento.addHeader(ACCEPT_DATETIME, mementoDateTime); 498 assertEquals("Expected memento could not be retrieved when Accept-Datetime header is present: " + mementoUri, 499 OK.getStatusCode(), 500 getStatus(getMemento)); 501 } 502 503 @Test 504 public void testHeadOnMementoWithAcceptDatetimePresent() throws Exception { 505 createVersionedContainer(id); 506 final String mementoDateTime = 507 MEMENTO_RFC_1123_FORMATTER.format(ISO_INSTANT.parse("2017-06-10T11:41:00Z", Instant::from)); 508 final String mementoUri = createMemento(subjectUri); 509 // Status 200: HEAD request on existing memento 510 final HttpHead headMemento = new HttpHead(mementoUri); 511 headMemento.addHeader(ACCEPT_DATETIME, mementoDateTime); 512 assertEquals("Expected memento could not be retrieved when Accept-Datetime header is present: " + mementoUri, 513 OK.getStatusCode(), 514 getStatus(headMemento)); 515 } 516 517 @Test 518 public void testOptionsOnMemento() throws Exception { 519 520 createVersionedContainer(id); 521 final String mementoUri = createMemento(subjectUri); 522 523 // Status 200: OPTIONS request on existing memento 524 final HttpOptions optionsMemento = new HttpOptions(mementoUri); 525 assertEquals("Expected memento is NOT found: " + mementoUri, OK.getStatusCode(), getStatus(optionsMemento)); 526 527 // Status 404: OPTIONS request on absent memento 528 final String absentMementoPath = serverAddress + id + "/" + FCR_VERSIONS + "/20000101000001"; 529 final HttpOptions optionsMementoAbsent = new HttpOptions(absentMementoPath); 530 assertEquals("Didn't get status 404 on absent memento!", 531 NOT_FOUND.getStatusCode(), getStatus(optionsMementoAbsent)); 532 533 // Status 400: OPTIONS request with invalid memento path 534 final HttpOptions optionsMementoInvalid = new HttpOptions(serverAddress + id + "/" + FCR_VERSIONS + "/any"); 535 checkResponseWithInvalidMementoID(optionsMementoInvalid); 536 } 537 538 @Test 539 public void testMementoExternalReference() throws Exception { 540 createVersionedContainer(id); 541 542 final String pid = getRandomUniqueId(); 543 final String resource = serverAddress + pid; 544 createObjectAndClose(pid); 545 546 final HttpPatch updateObjectGraphMethod = patchObjMethod(id); 547 updateObjectGraphMethod.addHeader(CONTENT_TYPE, "application/sparql-update"); 548 updateObjectGraphMethod.setEntity(new StringEntity("INSERT {" 549 + " <> <http://pcdm.org/models#hasMember> <" + resource + "> } WHERE {}")); 550 executeAndClose(updateObjectGraphMethod); 551 552 // create memento 553 final String mementoUri = createMemento(subjectUri); 554 555 // Remove the referencing resource 556 assertEquals("Expected delete to succeed", 557 NO_CONTENT.getStatusCode(), getStatus(new HttpDelete(resource))); 558 559 // Ensure that the resource reference remains (no referential integrity) 560 try (final CloseableHttpResponse getResponse1 = execute(new HttpGet(subjectUri)); 561 final CloseableDataset dataset = getDataset(getResponse1)) { 562 final DatasetGraph graph = dataset.asDatasetGraph(); 563 assertTrue("Expected to see resource: " + graph, graph.contains(ANY, 564 ANY, createURI("http://pcdm.org/models#hasMember"), createURI(resource))); 565 } 566 567 try (final CloseableHttpResponse getResponse1 = execute(new HttpGet(mementoUri)); 568 final CloseableDataset dataset = getDataset(getResponse1)) { 569 570 final DatasetGraph graph = dataset.asDatasetGraph(); 571 572 // Ensure that the resource reference is still in memento 573 assertTrue("Expected resource NOT found: " + graph, graph.contains(ANY, 574 ANY, createURI("http://pcdm.org/models#hasMember"), createURI(resource))); 575 576 // Ensure that the subject of the memento is the original reosurce 577 assertFalse("Subjects should be the original resource, not the memento: " + graph, 578 graph.contains(ANY, createURI(mementoUri), ANY, ANY)); 579 } 580 } 581 582 @Test 583 public void testDescriptionMementoReference() throws Exception { 584 // Create binary with description referencing other resource 585 createVersionedBinary(id); 586 587 final String referencedPid = getRandomUniqueId(); 588 final String referencedResource = serverAddress + referencedPid; 589 createObjectAndClose(referencedPid); 590 591 final String metadataId = id + "/fcr:metadata"; 592 final String metadataUri = serverAddress + metadataId; 593 594 final String relation = "http://purl.org/dc/elements/1.1/relation"; 595 final HttpPatch updateObjectGraphMethod = patchObjMethod(metadataId); 596 updateObjectGraphMethod.addHeader(CONTENT_TYPE, "application/sparql-update"); 597 updateObjectGraphMethod.setEntity(new StringEntity( 598 "INSERT {" + " <> <" + relation + "> <" + referencedResource + "> } WHERE {}")); 599 executeAndClose(updateObjectGraphMethod); 600 601 // Create memento 602 final String mementoUri = createMemento(subjectUri); 603 assertMementoUri(mementoUri, subjectUri); 604 605 // Delete referenced resource 606 assertEquals("Expected delete to succeed", 607 NO_CONTENT.getStatusCode(), getStatus(new HttpDelete(referencedResource))); 608 609 final Node originalBinaryNode = createURI(serverAddress + id); 610 // Ensure that the resource reference remains 611 try (final CloseableHttpResponse getResponse1 = execute(new HttpGet(metadataUri)); 612 final CloseableDataset dataset = getDataset(getResponse1)) { 613 final DatasetGraph graph = dataset.asDatasetGraph(); 614 assertTrue("Expected TO have resource: " + graph, graph.contains(ANY, 615 originalBinaryNode, createURI(relation), createURI(referencedResource))); 616 } 617 618 final String descMementoUrl = mementoUri.replace(FCR_VERSIONS, "fcr:metadata/fcr:versions"); 619 // Ensure that the resource reference is still in memento 620 try (final CloseableHttpResponse getResponse1 = execute(new HttpGet(descMementoUrl)); 621 final CloseableDataset dataset = getDataset(getResponse1)) { 622 final DatasetGraph graph = dataset.asDatasetGraph(); 623 assertTrue("Expected resource NOT found: " + graph, graph.contains(ANY, 624 originalBinaryNode, createURI(relation), createURI(referencedResource))); 625 } 626 } 627 628 @Test 629 public void testPutOnTimeMapContainer() throws Exception { 630 createVersionedContainer(id); 631 632 // status 405: PUT On LPDCv is disallowed. 633 assertEquals(405, getStatus(new HttpPut(serverAddress + id + "/" + FCR_VERSIONS))); 634 } 635 636 @Test 637 public void testPatchOnTimeMapContainer() throws Exception { 638 createVersionedContainer(id); 639 640 // status 405: PATCH On LPDCv is disallowed. 641 assertEquals(405, getStatus(new HttpPatch(serverAddress + id + "/" + FCR_VERSIONS))); 642 } 643 644 @Test 645 public void testGetTimeMapResponseForBinary() throws Exception { 646 objectSessionFactory.setDefaultCommitType(CommitType.UNVERSIONED); 647 648 createVersionedBinary(id); 649 650 verifyTimemapResponse(subjectUri, id); 651 } 652 653 @Test 654 public void testGetTimeMapResponseWithBadAcceptHeader() throws Exception { 655 createVersionedContainer(id); 656 657 final HttpGet httpGet = getObjMethod(id + "/" + FCR_VERSIONS); 658 httpGet.setHeader("Accept", "application/arbitrary"); 659 try (final CloseableHttpResponse response = execute(httpGet)) { 660 assertEquals("Should get a 'Not Acceptable' response!", NOT_ACCEPTABLE.getStatusCode(), getStatus( 661 response)); 662 } 663 664 } 665 666 @Test 667 public void testGetTimeMapResponseForBinaryDescription() throws Exception { 668 objectSessionFactory.setDefaultCommitType(CommitType.UNVERSIONED); 669 670 createVersionedBinary(id); 671 672 final String descriptionUri = subjectUri + "/fcr:metadata"; 673 final String descriptionId = id + "/fcr:metadata"; 674 675 verifyTimemapResponse(descriptionUri, descriptionId); 676 } 677 678 /** 679 * Verify an application/link-format TimeMap response. 680 * 681 * @param uri The full URI of the Original Resource. 682 * @param id The path of the Original Resource. 683 * @throws Exception on HTTP request error 684 */ 685 private void verifyTimemapResponse(final String uri, final String id) throws Exception { 686 verifyTimemapResponse(uri, id, null, null, null); 687 } 688 689 /** 690 * Verify an application/link-format TimeMap response. 691 * 692 * @param uri The full URI of the Original Resource. 693 * @param id The path of the Original Resource. 694 * @param mementoDateTime a RFC-1123 datetime 695 * @throws Exception on HTTP request error 696 */ 697 private void verifyTimemapResponse(final String uri, final String id, final String mementoDateTime) 698 throws Exception { 699 final String[] mementoDateTimes = { mementoDateTime }; 700 verifyTimemapResponse(uri, id, mementoDateTimes, null, null); 701 } 702 703 /** 704 * Verify an application/link-format TimeMap response. 705 * 706 * @param uri The full URI of the Original Resource. 707 * @param id The path of the Original Resource. 708 * @param mementoDateTime Array of all the RFC-1123 datetimes for all the mementos. 709 * @param rangeStart RFC-1123 datetime of the first memento. 710 * @param rangeEnd RFC-1123 datetime of the last memento. 711 * @throws Exception on HTTP request error 712 */ 713 private void verifyTimemapResponse(final String uri, final String id, final String[] mementoDateTime, 714 final String rangeStart, final String rangeEnd) 715 throws Exception { 716 final String ldpcvUri = uri + "/" + FCR_VERSIONS; 717 final var expectedLinksOther = new ArrayList<Link>(); 718 final var expectedLinksMemento = new ArrayList<Link>(); 719 expectedLinksOther.add(Link.fromUri(uri).rel("original").build()); 720 expectedLinksOther.add(Link.fromUri(uri).rel("timegate").build()); 721 expectedLinksOther.sort(Comparator.comparing(Link::toString)); 722 723 final var expectedSelfLinkBuilder = Link.fromUri(ldpcvUri).rel("self") 724 .type(APPLICATION_LINK_FORMAT); 725 if (rangeStart != null && rangeEnd != null) { 726 expectedSelfLinkBuilder.param("from", rangeStart).param("until", 727 rangeEnd); 728 } 729 final var expectedSelfLink = expectedSelfLinkBuilder.build(); 730 731 if (mementoDateTime != null) { 732 for (final String memento : mementoDateTime) { 733 final TemporalAccessor instant = MEMENTO_RFC_1123_FORMATTER.parse(memento); 734 expectedLinksMemento.add(Link.fromUri(ldpcvUri + "/" + MEMENTO_LABEL_FORMATTER.format(instant)) 735 .rel("memento") 736 .param("datetime", memento) 737 .build()); 738 } 739 } 740 expectedLinksMemento.sort(Comparator.comparing(Link::toString)); 741 742 final HttpGet httpGet = getObjMethod(id + "/" + FCR_VERSIONS); 743 httpGet.setHeader("Accept", APPLICATION_LINK_FORMAT); 744 try (final CloseableHttpResponse response = execute(httpGet)) { 745 assertEquals("Didn't get a OK response!", OK.getStatusCode(), getStatus(response)); 746 // verify headers in link format. 747 verifyTimeMapHeaders(response, uri); 748 final var responseBody = EntityUtils.toString(response.getEntity()); 749 final List<String> bodyList = asList(responseBody.split("," + System.lineSeparator())); 750 //the links from the body are not 751 752 Link selfLink = null; 753 final var mementoLinks = new ArrayList<Link>(); 754 final var otherLinks = new ArrayList<Link>(); 755 756 final var allLinks = bodyList.stream().map(String::trim).filter(t -> !t.isEmpty()) 757 .sorted(Comparator.naturalOrder()) 758 .map(Link::valueOf).collect(Collectors.toList()); 759 760 for (final var link : allLinks) { 761 if ("memento".equals(link.getRel())) { 762 mementoLinks.add(link); 763 } else if ("self".equals(link.getRel())) { 764 selfLink = link; 765 } else { 766 otherLinks.add(link); 767 } 768 } 769 770 assertSelfLink(expectedSelfLink, selfLink); 771 assertEquals(expectedLinksOther, otherLinks); 772 assertEquals(expectedLinksMemento.size(), mementoLinks.size()); 773 for (var i = 0; i < expectedLinksMemento.size(); i++) { 774 assertMementoLink(expectedLinksMemento.get(i), mementoLinks.get(i)); 775 } 776 777 } 778 } 779 780 private void assertSelfLink(final Link expected, final Link actual) { 781 if (!expected.equals(actual)) { 782 assertEquals(expected.getUri(), actual.getUri()); 783 784 final var expectedFromStr = expected.getParams().get("from"); 785 final var expectedUntilStr = expected.getParams().get("until"); 786 final var actualFromStr = actual.getParams().get("from"); 787 final var actualUntilStr = actual.getParams().get("until"); 788 789 if (expectedFromStr != null) { 790 assertNotNull("link cannot have a null 'from' param", actualFromStr); 791 assertNotNull("link cannot have a null 'until' param", actualUntilStr); 792 assertDuration(expectedFromStr, actualFromStr); 793 assertDuration(expectedUntilStr, actualUntilStr); 794 } else { 795 assertNull("link cannot have a 'from' param", actualFromStr); 796 assertNull("link cannot have a 'until' param", actualUntilStr); 797 } 798 } 799 } 800 801 private void assertMementoLink(final Link expected, final Link actual) { 802 if (!expected.equals(actual)) { 803 // Ensures the timestamps are close to each other 804 assertDuration(expected.getParams().get("datetime"), actual.getParams().get("datetime")); 805 806 final var expectedUri = expected.getUri().toString(); 807 final var actualUri = actual.getUri().toString(); 808 // Reduces the granularity of the timestamp strings to ensure their formatting is the same, even if they're 809 // a few seconds apart. 810 assertEquals(expectedUri.substring(0, expectedUri.length() - 3), 811 actualUri.substring(0, actualUri.length() - 3)); 812 } 813 } 814 815 private static void assertDuration(final String expected, final String actual) { 816 final var expectedInstant = parseToInstant(expected); 817 final var actualInstant = parseToInstant(actual); 818 819 final var diff = Duration.between(expectedInstant, actualInstant); 820 821 assertTrue("Difference in expected and actual times should be less than 5 seconds", 822 diff.abs().getSeconds() < 5); 823 } 824 825 private static Instant parseToInstant(final String value) { 826 return Instant.from(MEMENTO_RFC_1123_FORMATTER.parse(value)); 827 } 828 829 /** 830 * Allow a one second on each side allowance when comparing memento URIs as tests can vary. 831 * @param message Message to return. 832 * @param expected The expected URI. 833 * @param actual The actual URI. 834 */ 835 private static void verifyMementoUri(final String message, final String expected, final String actual) { 836 if (!expected.equals(actual)) { 837 // Make 2 additional URIs by parsing the memento label and adding/subtracting 1 second. 838 final String expectedUriPrefix = expected.substring(0, expected.lastIndexOf("/")); 839 final String expectedDateString = expected.substring(expected.lastIndexOf("/") + 1); 840 final Instant expectedInstant = Instant.from(MEMENTO_LABEL_FORMATTER.parse(expectedDateString)); 841 final List<String> allowed = List.of( 842 expectedUriPrefix + MEMENTO_LABEL_FORMATTER.format(expectedInstant.minusSeconds(1)), 843 expected, 844 expectedUriPrefix + MEMENTO_LABEL_FORMATTER.format(expectedInstant.plusSeconds(1)) 845 ); 846 assertTrue(message, allowed.contains(actual)); 847 } 848 } 849 850 /** 851 * Utility function to verify TimeMap headers 852 * 853 * @param response the response 854 * @param uri the URI of the resource. 855 */ 856 private static void verifyTimeMapHeaders(final CloseableHttpResponse response, final String uri) { 857 final String ldpcvUri = uri + "/" + FCR_VERSIONS; 858 checkForLinkHeader(response, RESOURCE.toString(), "type"); 859 checkForLinkHeader(response, CONTAINER.toString(), "type"); 860 checkForLinkHeader(response, RDF_SOURCE.getURI(), "type"); 861 checkForLinkHeader(response, uri, "original"); 862 checkForLinkHeader(response, uri, "timegate"); 863 checkForLinkHeader(response, uri + "/" + FCR_VERSIONS, "timemap"); 864 checkForLinkHeader(response, VERSIONING_TIMEMAP_TYPE, "type"); 865 checkForLinkHeader(response, ldpcvUri + "/" + FCR_ACL, "acl"); 866 assertFalse(response.getFirstHeader("Allow").getValue().contains("DELETE")); 867 assertTrue(response.getFirstHeader("Allow").getValue().contains("GET")); 868 assertTrue(response.getFirstHeader("Allow").getValue().contains("HEAD")); 869 assertTrue(response.getFirstHeader("Allow").getValue().contains("POST")); 870 assertEquals(1, response.getHeaders("Accept-Post").length); 871 } 872 873 @Test 874 public void testCreateVersionOfBinary() throws Exception { 875 createVersionedBinary(id); 876 877 final String mementoUri = createMemento(subjectUri); 878 assertMementoUri(mementoUri, subjectUri); 879 880 final HttpGet httpGet = new HttpGet(mementoUri); 881 try (final CloseableHttpResponse response = execute(httpGet)) { 882 assertMementoDatetimeHeaderMatches(response, now()); 883 assertEquals("Binary content of memento must match original content", 884 BINARY_CONTENT, EntityUtils.toString(response.getEntity())); 885 } 886 887 // Verifying that the associated description memento was created 888 final String descriptionMementoUri = mementoUri.replace("fcr:versions", "fcr:metadata/fcr:versions"); 889 890 final HttpGet descGet = new HttpGet(descriptionMementoUri); 891 try (final CloseableHttpResponse response = execute(descGet)) { 892 assertMementoDatetimeHeaderPresent(response); 893 assertHasLink(response, type, RDF_SOURCE.getURI()); 894 } 895 } 896 897 @Test 898 public void testCreateVersionOfBinaryWithDatetimeAndBody() throws Exception { 899 objectSessionFactory.setDefaultCommitType(CommitType.UNVERSIONED); 900 901 createVersionedBinary(id); 902 903 final String mementoUri = createMemento(subjectUri); 904 final String v1Time = now(); 905 assertMementoUri(mementoUri, subjectUri); 906 907 TimeUnit.SECONDS.sleep(1); 908 909 putVersionedBinary(id, OCTET_STREAM_TYPE, BINARY_UPDATED, true); 910 911 final String mementoUri2 = createMemento(subjectUri); 912 final String v2Time = now(); 913 assertMementoUri(mementoUri2, subjectUri); 914 915 assertNotEquals("mementos should be different", mementoUri, mementoUri2); 916 917 // Verify that the memento has the updated binary 918 try (final CloseableHttpResponse response = execute(new HttpGet(mementoUri))) { 919 assertMementoDatetimeHeaderMatches(response, v1Time); 920 921 assertEquals(OCTET_STREAM_TYPE, response.getFirstHeader(CONTENT_TYPE).getValue()); 922 923 assertEquals("Binary content of memento must match original content", 924 BINARY_CONTENT, EntityUtils.toString(response.getEntity())); 925 } 926 927 try (final CloseableHttpResponse response = execute(new HttpGet(mementoUri2))) { 928 assertMementoDatetimeHeaderMatches(response, v2Time); 929 930 // Content-type is not retained for a binary memento created without description 931 assertEquals(OCTET_STREAM_TYPE, response.getFirstHeader(CONTENT_TYPE).getValue()); 932 933 assertEquals("Binary content of memento must match updated content", 934 BINARY_UPDATED, EntityUtils.toString(response.getEntity())); 935 } 936 } 937 938 @Test 939 public void testCreateVersionOfBinaryDescription() throws Exception { 940 createVersionedBinary(id); 941 942 final String descriptionUri = subjectUri + "/fcr:metadata"; 943 944 final HttpPatch patchProps = new HttpPatch(descriptionUri); 945 patchProps.setHeader(CONTENT_TYPE, "application/sparql-update"); 946 final String updateString = 947 "INSERT { <> <" + DC.title.getURI() + "> \"Original\" ." + 948 " <> <" + RDF.type.getURI() + "> <" + TEST_TYPE_RESOURCE.getURI() + "> } WHERE { }"; 949 patchProps.setEntity(new StringEntity(updateString)); 950 assertEquals(NO_CONTENT.getStatusCode(), getStatus(patchProps)); 951 TimeUnit.SECONDS.sleep(1); 952 final String mementoUri = createMemento(descriptionUri); 953 assertMementoUri(mementoUri, descriptionUri); 954 TimeUnit.SECONDS.sleep(1); 955 956 setDescriptionProperty(id, null, DC.title.getURI(), "Updated"); 957 TimeUnit.SECONDS.sleep(1); 958 try (final CloseableDataset dataset = getDataset(new HttpGet(mementoUri))) { 959 final DatasetGraph results = dataset.asDatasetGraph(); 960 961 final Node mementoSubject = createURI(subjectUri); 962 963 assertTrue("Property added to original before versioning must appear", 964 results.contains(ANY, mementoSubject, DC.title.asNode(), createLiteral("Original"))); 965 assertFalse("Property added after memento created must not appear", 966 results.contains(ANY, mementoSubject, title.asNode(), createLiteral("Updated"))); 967 assertFalse("Memento type should not be visible", 968 results.contains(ANY, mementoSubject, type.asNode(), MEMENTO_TYPE_NODE)); 969 assertTrue("Must have binary type", 970 results.contains(ANY, mementoSubject, RDF.type.asNode(), FEDORA_BINARY.asNode())); 971 assertTrue("Must have custom type", 972 results.contains(ANY, mementoSubject, RDF.type.asNode(), TEST_TYPE_RESOURCE.asNode())); 973 } 974 } 975 976 @Test 977 public void testAddAndRetrieveVersion() throws Exception { 978 createVersionedContainer(id); 979 980 logger.debug("Setting a title"); 981 patchLiteralProperty(serverAddress + id, title.getURI(), "First Title"); 982 983 try (final CloseableDataset dataset = getContent(serverAddress + id)) { 984 assertTrue("Should find original title", dataset.asDatasetGraph().contains(ANY, 985 ANY, title.asNode(), createLiteral("First Title"))); 986 } 987 logger.debug("Posting version v0.0.1"); 988 final String mementoUri = createMemento(subjectUri); 989 assertMementoUri(mementoUri, subjectUri); 990 TimeUnit.SECONDS.sleep(1); 991 logger.debug("Replacing the title"); 992 patchLiteralProperty(serverAddress + id, title.getURI(), "Second Title"); 993 TimeUnit.SECONDS.sleep(1); 994 995 final var subjectUri = createURI(serverAddress + id); 996 try (final CloseableDataset dataset = getContent(mementoUri)) { 997 logger.debug("Got version profile:"); 998 final DatasetGraph versionResults = dataset.asDatasetGraph(); 999 1000 assertTrue("Should find a title in historic version", versionResults.contains(ANY, 1001 subjectUri, title.asNode(), createLiteral("First Title"))); 1002 assertTrue("Should find original title in historic version", versionResults.contains(ANY, 1003 subjectUri, title.asNode(), createLiteral("First Title"))); 1004 assertFalse("Should not find the updated title in historic version", 1005 versionResults.contains(ANY, subjectUri, title.asNode(), 1006 createLiteral("Second Title"))); 1007 } 1008 } 1009 1010 @Test 1011 public void testTimeMapResponseContentTypes() throws Exception { 1012 createVersionedContainer(id); 1013 1014 final String[] timeMapResponseTypes = getTimeMapResponseTypes(); 1015 for (final String type : timeMapResponseTypes) { 1016 final HttpGet method = new HttpGet(serverAddress + id + "/fcr:versions"); 1017 method.addHeader(ACCEPT, type); 1018 assertEquals(type, getContentType(method)); 1019 } 1020 } 1021 1022 @Test 1023 public void testGetVersionResponseContentTypes() throws Exception { 1024 createVersionedContainer(id); 1025 final String versionUri = createMemento(subjectUri); 1026 1027 final String[] rdfResponseTypes = rdfTypes.toArray(new String[0]); 1028 for (final String type : rdfResponseTypes) { 1029 final HttpGet method = new HttpGet(versionUri); 1030 method.addHeader(ACCEPT, type); 1031 assertEquals(type, getContentType(method)); 1032 } 1033 } 1034 1035 @Test 1036 public void testDatetimeNegotiationLDPRv() throws Exception { 1037 final String startDatetime = MEMENTO_RFC_1123_FORMATTER.format(Instant.now().atZone(ZoneOffset.UTC)); 1038 1039 // Make sure the start time is before the first memento 1040 TimeUnit.SECONDS.sleep(1); 1041 1042 final CloseableHttpClient customClient = createClient(true); 1043 1044 final String version1Uri = createVersionedContainerReturnMementoUri(id); 1045 1046 TimeUnit.SECONDS.sleep(1); 1047 1048 // Request datetime between memento1 and memento2 1049 final String betweenDatetime = MEMENTO_RFC_1123_FORMATTER.format(Instant.now().atZone(ZoneOffset.UTC)); 1050 1051 TimeUnit.SECONDS.sleep(1); 1052 1053 putVersionedContainer(id, "update", true); 1054 final String version2Uri = createMemento(subjectUri); 1055 1056 assertNotEquals("mementos should be different", version1Uri, version2Uri); 1057 1058 final HttpGet getMemento = getObjMethod(id); 1059 getMemento.addHeader(ACCEPT_DATETIME, betweenDatetime); 1060 1061 try (final CloseableHttpResponse response = customClient.execute(getMemento)) { 1062 assertEquals("Did not get FOUND response", FOUND.getStatusCode(), getStatus(response)); 1063 assertNoMementoDatetimeHeaderPresent(response); 1064 verifyMementoUri("Did not get Location header", version1Uri, response.getFirstHeader(LOCATION).getValue()); 1065 assertEquals("Did not get Content-Length == 0", "0", response.getFirstHeader(CONTENT_LENGTH).getValue()); 1066 } 1067 1068 TimeUnit.SECONDS.sleep(1); 1069 1070 // Request datetime more recent than both mementos 1071 final String afterDatetime = MEMENTO_RFC_1123_FORMATTER.format(Instant.now().atZone(ZoneOffset.UTC)); 1072 1073 final HttpGet getMemento2 = getObjMethod(id); 1074 getMemento2.addHeader(ACCEPT_DATETIME, afterDatetime); 1075 1076 try (final CloseableHttpResponse response = customClient.execute(getMemento2)) { 1077 assertEquals("Did not get FOUND response", FOUND.getStatusCode(), getStatus(response)); 1078 assertNoMementoDatetimeHeaderPresent(response); 1079 verifyMementoUri("Did not get Location header", version2Uri, response.getFirstHeader(LOCATION).getValue()); 1080 assertEquals("Did not get Content-Length == 0", "0", response.getFirstHeader(CONTENT_LENGTH).getValue()); 1081 } 1082 1083 // Request datetime older than either mementos 1084 final HttpGet getMemento3 = getObjMethod(id); 1085 getMemento3.addHeader(ACCEPT_DATETIME, startDatetime); 1086 1087 try (final CloseableHttpResponse response = customClient.execute(getMemento3)) { 1088 assertEquals("Did not get FOUND response", FOUND.getStatusCode(), getStatus(response)); 1089 assertNoMementoDatetimeHeaderPresent(response); 1090 verifyMementoUri("Did not get Location header", version1Uri, response.getFirstHeader(LOCATION).getValue()); 1091 assertEquals("Did not get Content-Length == 0", "0", response.getFirstHeader(CONTENT_LENGTH).getValue()); 1092 } 1093 } 1094 1095 @Test 1096 public void testDatetimeNegotiationExactMatch() throws Exception { 1097 final CloseableHttpClient customClient = createClient(true); 1098 1099 final String originalUri = createVersionedContainer(id); 1100 1101 // Create a first memento 1102 final String version1Uri = createMemento(originalUri); 1103 final HttpHead httpHead = new HttpHead(version1Uri); 1104 final String version1Datetime; 1105 try (final CloseableHttpResponse response = customClient.execute(httpHead)) { 1106 version1Datetime = response.getFirstHeader(MEMENTO_DATETIME_HEADER).getValue(); 1107 } 1108 1109 TimeUnit.SECONDS.sleep(1); 1110 1111 // Create second memento 1112 putVersionedContainer(id, "updated", true); 1113 final String version2Uri = createMemento(subjectUri); 1114 final HttpHead httpHead2 = new HttpHead(version2Uri); 1115 final String version2Datetime; 1116 try (final CloseableHttpResponse response = customClient.execute(httpHead2)) { 1117 version2Datetime = response.getFirstHeader(MEMENTO_DATETIME_HEADER).getValue(); 1118 } 1119 1120 assertNotEquals("mementos should be different", version1Uri, version2Uri); 1121 1122 // Attempt to retrieve newer memento 1123 final HttpGet getVersion2 = getObjMethod(id); 1124 getVersion2.addHeader(ACCEPT_DATETIME, version2Datetime); 1125 1126 try (final CloseableHttpResponse response = customClient.execute(getVersion2)) { 1127 assertEquals("Did not get FOUND response", FOUND.getStatusCode(), getStatus(response)); 1128 assertNoMementoDatetimeHeaderPresent(response); 1129 verifyMementoUri("Did not get expected memento location", 1130 version2Uri, response.getFirstHeader(LOCATION).getValue()); 1131 } 1132 1133 // Attempt to get older memento 1134 final HttpGet getVersion1 = getObjMethod(id); 1135 getVersion1.addHeader(ACCEPT_DATETIME, version1Datetime); 1136 1137 try (final CloseableHttpResponse response = customClient.execute(getVersion1)) { 1138 assertEquals("Did not get FOUND response", FOUND.getStatusCode(), getStatus(response)); 1139 assertNoMementoDatetimeHeaderPresent(response); 1140 verifyMementoUri("Did not get expected memento location", 1141 version1Uri, response.getFirstHeader(LOCATION).getValue()); 1142 } 1143 } 1144 1145 @Test 1146 public void testDatetimeNegotiationNoMementos() throws Exception { 1147 objectSessionFactory.setDefaultCommitType(CommitType.UNVERSIONED); 1148 final CloseableHttpClient customClient = createClient(true); 1149 createVersionedContainer(id); 1150 final String requestDatetime = 1151 MEMENTO_RFC_1123_FORMATTER.format(ISO_INSTANT.parse("2017-01-12T00:00:00Z", Instant::from)); 1152 final HttpGet getMemento = getObjMethod(id); 1153 getMemento.addHeader(ACCEPT_DATETIME, requestDatetime); 1154 1155 try (final CloseableHttpResponse response = customClient.execute(getMemento)) { 1156 assertEquals("Didn't get NOT_ACCEPTABLE response", NOT_ACCEPTABLE.getStatusCode(), getStatus(response)); 1157 assertNull("Didn't expect a Location header", response.getFirstHeader(LOCATION)); 1158 assertNotEquals("Didn't get Content-Length > 0", 0, response.getFirstHeader(CONTENT_LENGTH).getValue()); 1159 } 1160 } 1161 1162 1163 @Test 1164 public void testGetWithDateTimeNegotiation() throws Exception { 1165 objectSessionFactory.setDefaultCommitType(CommitType.UNVERSIONED); 1166 final CloseableHttpClient customClient = createClient(true); 1167 final String mementoDateTime = 1168 MEMENTO_RFC_1123_FORMATTER.format(ISO_INSTANT.parse("2017-08-29T15:47:50Z", Instant::from)); 1169 1170 createVersionedContainer(id); 1171 1172 // Status 406: Get absent memento with datetime negotiation. 1173 final HttpGet getMethod1 = getObjMethod(id); 1174 getMethod1.setHeader(ACCEPT_DATETIME, mementoDateTime); 1175 assertEquals("Didn't get status 406 on absent memento!", 1176 NOT_ACCEPTABLE.getStatusCode(), getStatus(customClient.execute(getMethod1))); 1177 1178 // Create memento 1179 final String mementoUri = createMemento(subjectUri); 1180 1181 // Status 302: GET memento with datetime negotiation 1182 final HttpGet getMethod2 = getObjMethod(id); 1183 getMethod2.setHeader(ACCEPT_DATETIME, mementoDateTime); 1184 assertEquals("Expected memento is NOT found: " + mementoUri, FOUND.getStatusCode(), 1185 getStatus(customClient.execute(getMethod2))); 1186 1187 // Status 400: Get memento with bad Accept-Datetime header value 1188 final String badDataTime = "Wed, 29 Aug 2017 15:47:50 GMT"; // should be TUE, 29 Aug 2017 15:47:50 GMT 1189 final HttpGet getMethod3 = getObjMethod(id); 1190 getMethod3.setHeader(ACCEPT_DATETIME, badDataTime); 1191 assertEquals("Didn't get status 400 on bad Accept-Datetime value!", 1192 BAD_REQUEST.getStatusCode(), getStatus(customClient.execute(getMethod3))); 1193 } 1194 1195 @Test 1196 public void testFixityOnVersionedResource() throws Exception { 1197 createVersionedBinary(id); 1198 1199 final String mementoUri = createMemento(subjectUri); 1200 1201 final HttpGet checkFixity = new HttpGet(mementoUri); 1202 checkFixity.setHeader("Want-Digest", "sha"); 1203 try (final CloseableHttpResponse response = execute(checkFixity)) { 1204 assertEquals("Did not get OK response", OK.getStatusCode(), getStatus(response)); 1205 assertTrue(decodeDigestHeader(response.getFirstHeader("Digest").getValue()).containsKey("sha")); 1206 } 1207 } 1208 1209 @Test 1210 public void testOptionsMemento() throws Exception { 1211 createVersionedContainer(id); 1212 final String mementoUri = createMemento(subjectUri); 1213 1214 final HttpOptions optionsRequest = new HttpOptions(mementoUri); 1215 try (final CloseableHttpResponse optionsResponse = execute(optionsRequest)) { 1216 assertEquals(OK.getStatusCode(), optionsResponse.getStatusLine().getStatusCode()); 1217 assertMementoOptionsHeaders(optionsResponse); 1218 } 1219 } 1220 1221 @Test 1222 public void testPatchOnMemento() throws Exception { 1223 createVersionedContainer(id); 1224 1225 final String mementoUri = createMemento(subjectUri); 1226 final HttpPatch patch = new HttpPatch(mementoUri); 1227 patch.addHeader(CONTENT_TYPE, "application/sparql-update"); 1228 patch.setEntity(new StringEntity( 1229 "INSERT DATA { <> <" + title.getURI() + "> \"Memento title\" } ")); 1230 1231 // status 405: PATCH on memento is not allowed. 1232 assertEquals(405, getStatus(patch)); 1233 } 1234 1235 @Test 1236 public void testPatchOnInvalidMementoPath() throws Exception { 1237 createVersionedContainer(id); 1238 1239 final String anyMementoUri = subjectUri + "/fcr:versions/any"; 1240 final HttpPatch anyPatch = new HttpPatch(anyMementoUri); 1241 anyPatch.addHeader(CONTENT_TYPE, "application/sparql-update"); 1242 anyPatch.setEntity(new StringEntity( 1243 "INSERT DATA { <> <" + title.getURI() + "> \"Memento title\" } ")); 1244 1245 // status 405: PATCH on memento path is not allowed. 1246 assertEquals(405, getStatus(anyPatch)); 1247 } 1248 1249 @Test 1250 public void testPostOnMemento() throws Exception { 1251 createVersionedContainer(id); 1252 1253 final String mementoUri = createMemento(subjectUri); 1254 final String body = createContainerMementoBodyContent(subjectUri, N3); 1255 final HttpPost post = new HttpPost(mementoUri); 1256 post.addHeader(CONTENT_TYPE, N3); 1257 post.setEntity(new StringEntity(body)); 1258 1259 // status 405: POST on memento is not allowed. 1260 assertEquals(405, getStatus(post)); 1261 } 1262 1263 @Test 1264 public void testPostOnInvalidMementoPath() throws Exception { 1265 createVersionedContainer(id); 1266 1267 final String body = createContainerMementoBodyContent(subjectUri, N3); 1268 final String anyMementoUri = subjectUri + "/fcr:versions/any"; 1269 final HttpPost anyPost = new HttpPost(anyMementoUri); 1270 anyPost.addHeader(CONTENT_TYPE, N3); 1271 anyPost.setEntity(new StringEntity(body)); 1272 1273 // status 405: POST on memento path is not allowed. 1274 assertEquals(405, getStatus(anyPost)); 1275 } 1276 1277 @Test 1278 public void testPutOnMemento() throws Exception { 1279 createVersionedContainer(id); 1280 1281 final String mementoUri = createMemento(subjectUri); 1282 final String body = createContainerMementoBodyContent(subjectUri, N3); 1283 final HttpPut put = new HttpPut(mementoUri); 1284 put.addHeader(CONTENT_TYPE, N3); 1285 put.setEntity(new StringEntity(body)); 1286 1287 // status 405: PUT on memento is not allowed. 1288 assertEquals(405, getStatus(put)); 1289 } 1290 1291 @Test 1292 public void testPutOnInvalidMementoPath() throws Exception { 1293 createVersionedContainer(id); 1294 1295 final String body = createContainerMementoBodyContent(subjectUri, N3); 1296 final String anyMementoUri = subjectUri + "/fcr:versions/any"; 1297 final HttpPut anyPut = new HttpPut(anyMementoUri); 1298 anyPut.addHeader(CONTENT_TYPE, N3); 1299 anyPut.setEntity(new StringEntity(body)); 1300 1301 // status 405: PUT on memento path is not allowed. 1302 assertEquals(405, getStatus(anyPut)); 1303 } 1304 1305 @Test 1306 public void testGetLDPRSMementoHeaders() throws Exception { 1307 createVersionedContainer(id); 1308 1309 final String version1Uri = createMemento(subjectUri); 1310 final HttpGet getRequest = new HttpGet(version1Uri); 1311 1312 try (final CloseableHttpResponse response = execute(getRequest)) { 1313 assertMementoDatetimeHeaderMatches(response, now()); 1314 checkForLinkHeader(response, MEMENTO_TYPE, "type"); 1315 checkForLinkHeader(response, subjectUri, "original"); 1316 checkForLinkHeader(response, subjectUri, "timegate"); 1317 checkForLinkHeader(response, subjectUri + "/" + FCR_VERSIONS, "timemap"); 1318 checkForLinkHeader(response, RESOURCE.toString(), "type"); 1319 checkForLinkHeader(response, RDF_SOURCE.toString(), "type"); 1320 assertNoLinkHeader(response, VERSIONED_RESOURCE.toString(), "type"); 1321 assertNoLinkHeader(response, VERSIONING_TIMEMAP_TYPE, "type"); 1322 assertNoLinkHeader(response, version1Uri + "/" + FCR_ACL, "acl"); 1323 } 1324 } 1325 1326 @Test 1327 public void testGetLDPNRMementoHeaders() throws Exception { 1328 createVersionedBinary(id, "text/plain", "This is some versioned content"); 1329 1330 final String version1Uri = createMemento(subjectUri); 1331 final HttpGet getRequest = new HttpGet(version1Uri); 1332 1333 try (final CloseableHttpResponse response = execute(getRequest)) { 1334 assertMementoDatetimeHeaderMatches(response, now()); 1335 checkForLinkHeader(response, MEMENTO_TYPE, "type"); 1336 checkForLinkHeader(response, subjectUri, "original"); 1337 checkForLinkHeader(response, subjectUri, "timegate"); 1338 checkForLinkHeader(response, subjectUri + "/" + FCR_VERSIONS, "timemap"); 1339 checkForLinkHeader(response, NON_RDF_SOURCE.toString(), "type"); 1340 assertNoLinkHeader(response, VERSIONED_RESOURCE.toString(), "type"); 1341 assertNoLinkHeader(response, VERSIONING_TIMEMAP_TYPE, "type"); 1342 assertNoLinkHeader(response, version1Uri + "/" + FCR_ACL, "acl"); 1343 } 1344 } 1345 1346 /* 1347 * Verify binary description timemap RDF representation can be retrieved with and without 1348 * accompanying binary memento 1349 */ 1350 @Test 1351 public void testFcrepo2792() throws Exception { 1352 // 1. Create versioned resource 1353 createVersionedBinary(id); 1354 1355 final String descriptionUri = subjectUri + "/fcr:metadata"; 1356 final String descTimemapUri = descriptionUri + "/" + FCR_VERSIONS; 1357 1358 // 2. verify that metadata versions endpoint returns 200 1359 assertEquals(OK.getStatusCode(), getStatus(new HttpGet(descTimemapUri))); 1360 1361 // 3. create a binary version against binary timemap 1362 final String mementoUri = createMemento(subjectUri); 1363 final String descMementoUri = mementoUri.replace("fcr:versions", "fcr:metadata/fcr:versions"); 1364 1365 final Node timemapSubject = createURI(descTimemapUri); 1366 final Node descMementoResc = createURI(descMementoUri); 1367 // 4. verify that the binary description timemap RDF is there and contains the new description memento 1368 try (final CloseableDataset dataset = getDataset(new HttpGet(descTimemapUri))) { 1369 final DatasetGraph results = dataset.asDatasetGraph(); 1370 assertTrue("Timemap RDF response must contain description memento", 1371 results.contains(ANY, timemapSubject, CONTAINS.asNode(), descMementoResc)); 1372 } 1373 1374 // Wait a second to avoid timestamp collisions 1375 TimeUnit.SECONDS.sleep(1); 1376 1377 // 5. Create a second binary description memento 1378 final String descMementoUri2 = createMemento(descriptionUri); 1379 1380 // 6. verify that the binary description timemap availabe (returns 404 in fcrepo-2792) 1381 try (final CloseableDataset dataset = getDataset(new HttpGet(descTimemapUri))) { 1382 final DatasetGraph results = dataset.asDatasetGraph(); 1383 final Node descMementoResc2 = createURI(descMementoUri2); 1384 1385 assertTrue("Timemap RDF response must contain first description memento", 1386 results.contains(ANY, timemapSubject, CONTAINS.asNode(), descMementoResc)); 1387 assertTrue("Timemap RDF response must contain second description memento", 1388 results.contains(ANY, timemapSubject, CONTAINS.asNode(), descMementoResc2)); 1389 } 1390 } 1391 1392 @Test 1393 public void testOptionsTimeMap() throws Exception { 1394 createVersionedContainer(id); 1395 final String timemapUri = subjectUri + "/" + FCR_VERSIONS; 1396 1397 try (final CloseableHttpResponse response = execute(new HttpOptions(timemapUri))) { 1398 assertEquals(OK.getStatusCode(), getStatus(response)); 1399 verifyTimeMapHeaders(response, subjectUri); 1400 } 1401 } 1402 1403 @Test 1404 public void testCreateExternalBinaryProxyVersion() throws Exception { 1405 // Create binary to use as content for proxying 1406 final String proxyContent = "proxied content"; 1407 final String proxiedId = getRandomUniqueId(); 1408 final String proxiedUri = serverAddress + proxiedId + "/ds"; 1409 createDatastream(proxiedId, "ds", proxyContent); 1410 1411 // Create the proxied external binary object using the first binary 1412 createVersionedExternalBinaryMemento(id, "proxy", proxiedUri); 1413 1414 // Create a version of the external binary using the second binary as content 1415 final String mementoUri = createMemento(subjectUri); 1416 1417 // Verify that the historic version exists and proxies the old content 1418 final HttpGet httpGet1 = new HttpGet(mementoUri); 1419 try (final CloseableHttpResponse getResponse = execute(httpGet1)) { 1420 assertEquals(OK.getStatusCode(), getStatus(getResponse)); 1421 assertMementoDatetimeHeaderPresent(getResponse); 1422 assertEquals(proxiedUri, getContentLocation(getResponse)); 1423 final String content = EntityUtils.toString(getResponse.getEntity()); 1424 assertEquals("Entity Data doesn't match proxied versioned content!", proxyContent, content); 1425 } 1426 } 1427 1428 @Test 1429 public void versionedResourcesCreatedByDefault() throws Exception { 1430 final String id = getRandomUniqueId(); 1431 createObjectAndClose(id); 1432 final String timemap = id + "/" + FCR_VERSIONS; 1433 final HttpPost versionPost = postObjMethod(timemap); 1434 try (final CloseableHttpResponse response = execute(versionPost)) { 1435 assertEquals(CREATED.getStatusCode(), getStatus(response)); 1436 } 1437 } 1438 1439 @Test 1440 public void testDeletedResourceMementosAreInaccessible() throws Exception { 1441 final String id = getRandomUniqueId(); 1442 // Make a container (by default they are auto-versioned) 1443 createVersionedContainer(id); 1444 1445 // Make sure the container exists 1446 final HttpGet getMethod = getObjMethod(id); 1447 assertEquals(OK.getStatusCode(), getStatus(getMethod)); 1448 1449 // Get the timemap and all the ldp:contains (which are the mementos) 1450 final HttpGet getTimemap = getObjMethod(id + "/" + FCR_VERSIONS); 1451 final List<String> mementos = new ArrayList<>(); 1452 try (final CloseableHttpResponse response = execute(getTimemap)) { 1453 final Dataset data = getDataset(response); 1454 final DatasetGraph graph = data.asDatasetGraph(); 1455 assertEquals(OK.getStatusCode(), getStatus(response)); 1456 final var contains = graph.find(Node.ANY, Node.ANY, CONTAINS.asNode(), Node.ANY); 1457 while (contains.hasNext()) { 1458 final var memento = contains.next(); 1459 mementos.add(memento.getObject().getURI()); 1460 } 1461 } 1462 // There should be only one memento 1463 assertEquals(1, mementos.size()); 1464 // Get the memento 1465 final HttpGet getMemento = new HttpGet(mementos.get(0)); 1466 assertEquals(OK.getStatusCode(), getStatus(getMemento)); 1467 // Delete the original container. 1468 final HttpDelete deleteContainer = deleteObjMethod(id); 1469 assertEquals(NO_CONTENT.getStatusCode(), getStatus(deleteContainer)); 1470 // Check it is gone 1471 final HttpGet getDeletedContainer = getObjMethod(id); 1472 assertEquals(GONE.getStatusCode(), getStatus(getDeletedContainer)); 1473 // Check the timemap is GONE 1474 final HttpGet getDeletedTimemap = getObjMethod(id + "/" + FCR_VERSIONS); 1475 assertEquals(GONE.getStatusCode(), getStatus(getDeletedTimemap)); 1476 // Check the memento is GONE 1477 final HttpGet getDeletedMemento = new HttpGet(mementos.get(0)); 1478 assertEquals(GONE.getStatusCode(), getStatus(getDeletedMemento)); 1479 } 1480 1481 @Test 1482 public void testMementosMaintainContainment() throws Exception { 1483 final String parentUri; 1484 final String child1Uri; 1485 final String child2Uri; 1486 final String child3Uri; 1487 1488 try (final CloseableHttpResponse response = execute(postObjMethod())) { 1489 assertEquals(CREATED.getStatusCode(), getStatus(response)); 1490 parentUri = getLocation(response); 1491 } 1492 TimeUnit.SECONDS.sleep(1); 1493 1494 try (final var response = execute(new HttpPost(parentUri))) { 1495 assertEquals(CREATED.getStatusCode(), getStatus(response)); 1496 child1Uri = getLocation(response); 1497 } 1498 final var httpPatch1 = new HttpPatch(parentUri); 1499 httpPatch1.setHeader(CONTENT_TYPE, "application/sparql-update"); 1500 httpPatch1.setEntity(new StringEntity("INSERT { <> <" + title + "> \"A new title\" . } WHERE {}")); 1501 assertEquals(NO_CONTENT.getStatusCode(), getStatus(httpPatch1)); 1502 final String memento1Uri = createMemento(parentUri); 1503 TimeUnit.SECONDS.sleep(1); 1504 1505 try (final var response = execute(new HttpPost(parentUri))) { 1506 assertEquals(CREATED.getStatusCode(), getStatus(response)); 1507 child2Uri = getLocation(response); 1508 } 1509 final var httpPatch2 = new HttpPatch(parentUri); 1510 httpPatch2.setHeader(CONTENT_TYPE, "application/sparql-update"); 1511 httpPatch2.setEntity(new StringEntity("DELETE { <> <" + title + "> ?o } INSERT { <> <" + title + 1512 "> \"A second title\" . } WHERE { <> <" + title + "> ?o }")); 1513 assertEquals(NO_CONTENT.getStatusCode(), getStatus(httpPatch2)); 1514 final String memento2Uri = createMemento(parentUri); 1515 final Instant memento2Instant = Instant.now(); 1516 TimeUnit.SECONDS.sleep(1); 1517 1518 try (final var response = execute(new HttpPost(parentUri))) { 1519 assertEquals(CREATED.getStatusCode(), getStatus(response)); 1520 child3Uri = getLocation(response); 1521 } 1522 final var httpPatch3 = new HttpPatch(parentUri); 1523 httpPatch3.setHeader(CONTENT_TYPE, "application/sparql-update"); 1524 httpPatch3.setEntity(new StringEntity("DELETE { <> <" + title + "> ?o } INSERT { <> <" + title + 1525 "> \"A third title\" . } WHERE { <> <" + title + "> ?o }")); 1526 assertEquals(NO_CONTENT.getStatusCode(), getStatus(httpPatch3)); 1527 final String memento3Uri = createMemento(parentUri); 1528 TimeUnit.SECONDS.sleep(1); 1529 1530 // Delete child 1 1531 assertEquals(NO_CONTENT.getStatusCode(), getStatus(new HttpDelete(child1Uri))); 1532 // Delete child 2 1533 assertEquals(NO_CONTENT.getStatusCode(), getStatus(new HttpDelete(child2Uri))); 1534 // Delete child 3 1535 assertEquals(NO_CONTENT.getStatusCode(), getStatus(new HttpDelete(child3Uri))); 1536 1537 final Node parentNode = createURI(parentUri); 1538 final var get1 = new HttpGet(memento1Uri); 1539 try (final var dataset = getDataset(get1)) { 1540 final var graph = dataset.asDatasetGraph(); 1541 assertTrue(graph.contains(ANY, parentNode, CONTAINS.asNode(), createURI(child1Uri))); 1542 assertFalse(graph.contains(ANY, parentNode, CONTAINS.asNode(), createURI(child2Uri))); 1543 assertFalse(graph.contains(ANY, parentNode, CONTAINS.asNode(), createURI(child3Uri))); 1544 } 1545 final var get2 = new HttpGet(memento2Uri); 1546 try (final var dataset = getDataset(get2)) { 1547 final var graph = dataset.asDatasetGraph(); 1548 assertTrue(graph.contains(ANY, parentNode, CONTAINS.asNode(), createURI(child1Uri))); 1549 assertTrue(graph.contains(ANY, parentNode, CONTAINS.asNode(), createURI(child2Uri))); 1550 assertFalse(graph.contains(ANY, parentNode, CONTAINS.asNode(), createURI(child3Uri))); 1551 } 1552 final var get3 = new HttpGet(memento3Uri); 1553 try (final var dataset = getDataset(get3)) { 1554 final var graph = dataset.asDatasetGraph(); 1555 assertTrue(graph.contains(ANY, parentNode, CONTAINS.asNode(), createURI(child1Uri))); 1556 assertTrue(graph.contains(ANY, parentNode, CONTAINS.asNode(), createURI(child2Uri))); 1557 assertTrue(graph.contains(ANY, parentNode, CONTAINS.asNode(), createURI(child3Uri))); 1558 } 1559 final var get4 = new HttpGet(parentUri); 1560 try (final var dataset = getDataset(get4)) { 1561 final var graph = dataset.asDatasetGraph(); 1562 assertFalse(graph.contains(ANY, parentNode, CONTAINS.asNode(), createURI(child1Uri))); 1563 assertFalse(graph.contains(ANY, parentNode, CONTAINS.asNode(), createURI(child2Uri))); 1564 assertFalse(graph.contains(ANY, parentNode, CONTAINS.asNode(), createURI(child3Uri))); 1565 } 1566 1567 // Get the original resource with date/time around memento 2 1568 final var getWithDateTime = new HttpGet(parentUri); 1569 getWithDateTime.addHeader(ACCEPT_DATETIME, MEMENTO_RFC_1123_FORMATTER.format(memento2Instant)); 1570 try (final var dataset = getDataset(getWithDateTime)) { 1571 final var graph = dataset.asDatasetGraph(); 1572 assertTrue(graph.contains(ANY, parentNode, CONTAINS.asNode(), createURI(child1Uri))); 1573 assertTrue(graph.contains(ANY, parentNode, CONTAINS.asNode(), createURI(child2Uri))); 1574 assertFalse(graph.contains(ANY, parentNode, CONTAINS.asNode(), createURI(child3Uri))); 1575 } 1576 } 1577 1578 @Test 1579 public void testCreateMementoNotAffectModifiedDate() throws Exception { 1580 final String url; 1581 try (final var response = execute(postObjMethod())) { 1582 assertEquals(CREATED.getStatusCode(), getStatus(response)); 1583 url = getLocation(response); 1584 } 1585 final var get1 = new HttpGet(url); 1586 get1.addHeader(ACCEPT, "application/n-triples"); 1587 final String originalTriples; 1588 try (final var response = execute(get1)) { 1589 assertEquals(OK.getStatusCode(), getStatus(response)); 1590 originalTriples = IOUtils.toString(response.getEntity().getContent(), UTF_8); 1591 } 1592 // Create a new version without changing anything 1593 final var postV = new HttpPost(url + "/" + FCR_VERSIONS); 1594 final String memento; 1595 try (final var response = execute(postV)) { 1596 assertEquals(CREATED.getStatusCode(), getStatus(response)); 1597 memento = getLocation(response); 1598 } 1599 // Get the memento last modified 1600 final String mementoTriples; 1601 final var getMemento = new HttpGet(memento); 1602 getMemento.addHeader(ACCEPT, "application/n-triples"); 1603 try (final var response = execute(getMemento)) { 1604 assertEquals(OK.getStatusCode(), getStatus(response)); 1605 mementoTriples = IOUtils.toString(response.getEntity().getContent(), UTF_8); 1606 } 1607 // Lastly get the resource last modified again 1608 final var get2 = new HttpGet(url); 1609 get2.addHeader(ACCEPT, "application/n-triples"); 1610 final String updatedTriples; 1611 try (final var response = execute(get2)) { 1612 updatedTriples = IOUtils.toString(response.getEntity().getContent(), UTF_8); 1613 } 1614 // Assert they are all the same 1615 confirmResponseBodyNTriplesAreEqual(originalTriples, mementoTriples); 1616 confirmResponseBodyNTriplesAreEqual(updatedTriples, mementoTriples); 1617 } 1618 1619 private void createVersionedExternalBinaryMemento(final String rescId, final String handling, 1620 final String externalUri) throws Exception { 1621 final HttpPut httpPut = putObjMethod(rescId); 1622 httpPut.addHeader(LINK, "<" + NON_RDF_SOURCE.getURI() + ">;rel=\"type\""); 1623 httpPut.addHeader(LINK, getExternalContentLinkHeader(externalUri, handling, null)); 1624 try (final CloseableHttpResponse response = execute(httpPut)) { 1625 assertEquals("Didn't get a CREATED response!", CREATED.getStatusCode(), getStatus(response)); 1626 } 1627 } 1628 1629 private static void assertMementoOptionsHeaders(final HttpResponse httpResponse) { 1630 final List<String> methods = headerValues(httpResponse, "Allow"); 1631 assertTrue("Should allow GET", methods.contains(HttpGet.METHOD_NAME)); 1632 assertTrue("Should allow HEAD", methods.contains(HttpHead.METHOD_NAME)); 1633 assertTrue("Should allow OPTIONS", methods.contains(HttpOptions.METHOD_NAME)); 1634 assertFalse("Should NOT allow DELETE", methods.contains(HttpDelete.METHOD_NAME)); 1635 } 1636 1637 private String createMemento(final String subjectUri) throws Exception { 1638 final HttpPost createVersionMethod = new HttpPost(subjectUri + "/" + FCR_VERSIONS); 1639 1640 // Create new memento of resource with updated body 1641 try (final CloseableHttpResponse response = execute(createVersionMethod)) { 1642 assertEquals("Didn't get a CREATED response!", CREATED.getStatusCode(), getStatus(response)); 1643 assertMementoDatetimeHeaderPresent(response); 1644 1645 return response.getFirstHeader(LOCATION).getValue(); 1646 } 1647 } 1648 1649 private String createContainerMementoBodyContent(final String subjectUri, final String contentType) 1650 throws Exception { 1651 // Produce new body from current body with changed triple 1652 final String body; 1653 final HttpGet httpGet = new HttpGet(subjectUri); 1654 final Model model = createDefaultModel(); 1655 final Lang rdfLang = RDFLanguages.contentTypeToLang(contentType); 1656 try (final CloseableHttpResponse response = execute(httpGet)) { 1657 model.read(response.getEntity().getContent(), "", rdfLang.getName()); 1658 } 1659 final String resourceUri = subjectUri.replace("/fcr:metadata", ""); 1660 final Resource subjectResc = model.getResource(resourceUri); 1661 subjectResc.removeAll(TEST_PROPERTY); 1662 subjectResc.addLiteral(TEST_PROPERTY, "bar"); 1663 subjectResc.addProperty(RDF.type, TEST_TYPE_RESOURCE); 1664 1665 try (final StringWriter stringOut = new StringWriter()) { 1666 RDFDataMgr.write(stringOut, model, RDFFormat.NTRIPLES); 1667 body = stringOut.toString(); 1668 } 1669 1670 return body; 1671 } 1672 1673 /** 1674 * Create a versioned LDP-RS 1675 * 1676 * @param id the desired slug 1677 * @return Location of the new resource 1678 * @throws Exception on error 1679 */ 1680 private String createVersionedContainer(final String id) throws Exception { 1681 final HttpPost createMethod = postObjMethod(); 1682 createMethod.addHeader("Slug", id); 1683 createMethod.addHeader(CONTENT_TYPE, N3); 1684 createMethod.setEntity(new StringEntity("<> <info:test#label> \"foo\"")); 1685 1686 try (final CloseableHttpResponse response = execute(createMethod)) { 1687 assertEquals("Didn't get a CREATED response!", CREATED.getStatusCode(), getStatus(response)); 1688 return response.getFirstHeader(LOCATION).getValue(); 1689 } 1690 } 1691 1692 private String createVersionedContainerReturnMementoUri(final String id) throws Exception { 1693 createVersionedContainer(id); 1694 1695 try (final CloseableDataset dataset = getDataset(getObjMethod(id + "/" + FCR_VERSIONS))) { 1696 final DatasetGraph results = dataset.asDatasetGraph(); 1697 final var contains = results.find(Node.ANY, Node.ANY, CONTAINS.asNode(), Node.ANY); 1698 return contains.next().getObject().getURI(); 1699 } 1700 } 1701 1702 /** 1703 * Create a versioned LDP-RS 1704 * 1705 * @param id the desired slug 1706 * @param label label text 1707 * @param isUpdate whether or not the operation is an update 1708 * @throws Exception on error 1709 */ 1710 private void putVersionedContainer(final String id, final String label, final boolean isUpdate) throws Exception { 1711 final HttpPut createMethod = putObjMethod(id); 1712 createMethod.addHeader("Slug", id); 1713 createMethod.addHeader(CONTENT_TYPE, N3); 1714 createMethod.setEntity(new StringEntity("<> <info:test#label> \"" + label + "\"")); 1715 1716 try (final CloseableHttpResponse response = execute(createMethod)) { 1717 if (isUpdate) { 1718 assertEquals("Didn't get a NO_CONTENT response!", NO_CONTENT.getStatusCode(), getStatus(response)); 1719 } else { 1720 assertEquals("Didn't get a CREATED response!", CREATED.getStatusCode(), getStatus(response)); 1721 logger.info("created object: {}", response.getFirstHeader(LOCATION).getValue()); 1722 } 1723 } 1724 } 1725 1726 /** 1727 * Create a versioned Archival Group 1728 * 1729 * @param id the desired slug 1730 * @return Location of the new resource 1731 * @throws Exception on error 1732 */ 1733 private String createVersionedArchivalGroup(final String id) throws Exception { 1734 final HttpPost createMethod = postObjMethod(); 1735 createMethod.addHeader("Slug", id); 1736 createMethod.addHeader(CONTENT_TYPE, TURTLE); 1737 createMethod.addHeader("Link", Link.fromUri(BASIC_CONTAINER.getURI()).rel("type").build().toString()); 1738 createMethod.addHeader("Link", Link.fromUri(ARCHIVAL_GROUP.getURI()).rel("type").build().toString()); 1739 createMethod.setEntity(new StringEntity("<> <info:test#label> \"foo\"")); 1740 1741 try (final CloseableHttpResponse response = execute(createMethod)) { 1742 assertEquals("Didn't get a CREATED response!", CREATED.getStatusCode(), getStatus(response)); 1743 return response.getFirstHeader(LOCATION).getValue(); 1744 } 1745 } 1746 1747 /** 1748 * Put a versioned LDP-NR. 1749 * 1750 * @param id the desired slug 1751 * @param mimeType the mimeType of the content 1752 * @param content the actual content 1753 * @param isUpdate whether or not the operation is an update 1754 * @throws Exception on error 1755 */ 1756 private void putVersionedBinary(final String id, final String mimeType, final String content, 1757 final boolean isUpdate) 1758 throws Exception { 1759 final HttpPut createMethod = putObjMethod(id); 1760 createMethod.addHeader("Slug", id); 1761 createMethod.addHeader("Link", Link.fromUri(NON_RDF_SOURCE.getURI()).rel("type").build().toString()); 1762 if (mimeType == null && content == null) { 1763 createMethod.addHeader(CONTENT_TYPE, OCTET_STREAM_TYPE); 1764 createMethod.setEntity(new StringEntity(BINARY_CONTENT)); 1765 } else { 1766 createMethod.addHeader(CONTENT_TYPE, mimeType); 1767 createMethod.setEntity(new StringEntity(content)); 1768 } 1769 1770 try (final CloseableHttpResponse response = execute(createMethod)) { 1771 if (isUpdate) { 1772 assertEquals("Didn't get a NO_CONTENT response!", NO_CONTENT.getStatusCode(), getStatus(response)); 1773 } else { 1774 assertEquals("Didn't get a CREATED response!", CREATED.getStatusCode(), getStatus(response)); 1775 logger.info("created object: {}", response.getFirstHeader(LOCATION).getValue()); 1776 } 1777 } 1778 } 1779 1780 /** 1781 * Create a versioned LDP-NR. 1782 * 1783 * @param id the desired slug 1784 * @param mimeType the mimeType of the content 1785 * @param content the actual content 1786 * @return Location of the new resource 1787 * @throws Exception on error 1788 */ 1789 private String createVersionedBinary(final String id, final String mimeType, final String content) 1790 throws Exception { 1791 final HttpPost createMethod = postObjMethod(); 1792 createMethod.addHeader("Slug", id); 1793 if (mimeType == null && content == null) { 1794 createMethod.addHeader(CONTENT_TYPE, OCTET_STREAM_TYPE); 1795 createMethod.setEntity(new StringEntity(BINARY_CONTENT)); 1796 } else { 1797 createMethod.addHeader(CONTENT_TYPE, mimeType); 1798 createMethod.setEntity(new StringEntity(content)); 1799 } 1800 1801 try (final CloseableHttpResponse response = execute(createMethod)) { 1802 assertEquals("Didn't get a CREATED response!", CREATED.getStatusCode(), getStatus(response)); 1803 logger.info("created object: {}", response.getFirstHeader(LOCATION).getValue()); 1804 return response.getFirstHeader(LOCATION).getValue(); 1805 } 1806 } 1807 1808 /** 1809 * Create a versioned LDP-NR. 1810 * 1811 * @param id the desired slug 1812 * @return Location of the new resource 1813 * @throws Exception on error 1814 */ 1815 private String createVersionedBinary(final String id) throws Exception { 1816 return createVersionedBinary(id, null, null); 1817 } 1818 1819 private static void assertNoMementoDatetimeHeaderPresent(final CloseableHttpResponse response) { 1820 assertNull("No memento datetime header set in response", 1821 response.getFirstHeader(MEMENTO_DATETIME_HEADER)); 1822 } 1823 1824 private static void assertMementoDatetimeHeaderPresent(final CloseableHttpResponse response) { 1825 assertNotNull("No memento datetime header set in response", 1826 response.getFirstHeader(MEMENTO_DATETIME_HEADER)); 1827 } 1828 1829 private static void assertMementoDatetimeHeaderMatches(final CloseableHttpResponse response, 1830 final String expected) { 1831 assertMementoDatetimeHeaderPresent(response); 1832 assertDuration(expected, response.getFirstHeader(MEMENTO_DATETIME_HEADER).getValue()); 1833 } 1834 1835 private static void patchLiteralProperty(final String url, final String predicate, final String literal) 1836 throws IOException { 1837 patchLiteralProperty(url, predicate, literal, null); 1838 } 1839 private static void patchLiteralProperty(final String url, final String predicate, final String literal, 1840 final String xsdType) 1841 throws IOException { 1842 final HttpPatch updateObjectGraphMethod = new HttpPatch(url); 1843 final String type = xsdType != null ? "^^" + xsdType : ""; 1844 updateObjectGraphMethod.addHeader(CONTENT_TYPE, "application/sparql-update"); 1845 updateObjectGraphMethod.setEntity(new StringEntity( 1846 "INSERT DATA { <> <" + predicate + "> \"" + literal + "\"" + type + " } ")); 1847 assertEquals(NO_CONTENT.getStatusCode(), getStatus(updateObjectGraphMethod)); 1848 } 1849 1850 private CloseableDataset getContent(final String url) throws IOException { 1851 final HttpGet getVersion = new HttpGet(url); 1852 getVersion.addHeader("Prefer", "return=representation; include=\"" + EMBED_CONTAINED.toString() + "\""); 1853 return getDataset(getVersion); 1854 } 1855 1856 private String[] getTimeMapResponseTypes() { 1857 rdfTypes.add(APPLICATION_LINK_FORMAT); 1858 return rdfTypes.toArray(new String[0]); 1859 } 1860 1861 private static void assertMementoUri(final String mementoUri, final String subjectUri) { 1862 assertTrue(mementoUri.matches(subjectUri + "/fcr:versions/\\d+")); 1863 } 1864 1865 private static void assertMementoEqualsOriginal(final String mementoURI) throws Exception { 1866 1867 final HttpGet getMemento = new HttpGet(mementoURI); 1868 getMemento.addHeader(ACCEPT, "application/n-triples"); 1869 1870 try (final CloseableHttpResponse response = execute(getMemento)) { 1871 final HttpGet getOriginal = new HttpGet(getOriginalResourceUri(response)); 1872 getOriginal.addHeader(ACCEPT, "application/n-triples"); 1873 1874 try (final CloseableHttpResponse origResponse = execute(getOriginal)) { 1875 1876 final String[] mTriples = EntityUtils.toString(response.getEntity()).split("\\.\\r?\\n"); 1877 final String[] oTriples = EntityUtils.toString(origResponse.getEntity()).split("\\.\\r?\\n"); 1878 1879 sort(mTriples); 1880 sort(oTriples); 1881 1882 assertArrayEquals("Memento and Original Resource triples do not match!", mTriples, oTriples); 1883 } 1884 } 1885 } 1886 1887 private static void assertHasLink(final CloseableHttpResponse response, final Property relation, 1888 final String uri) { 1889 final String relName = relation.getLocalName(); 1890 assertTrue("Missing link " + relName + " with value " + uri, getLinkHeaders(response) 1891 .stream().map(Link::valueOf) 1892 .anyMatch(l -> relName.equals(l.getRel()) && uri.equals(l.getUri().toString()))); 1893 } 1894 1895 private void checkResponseWithInvalidMementoID(final HttpUriRequest req) throws IOException { 1896 try (final CloseableHttpResponse response = execute(req)) { 1897 assertEquals("Didn't get status 400 with invalid memento path!", 1898 BAD_REQUEST.getStatusCode(), getStatus(req)); 1899 1900 // Request must fail with constrained exception due to invalid memento ID 1901 assertConstrainedByPresent(response); 1902 } 1903 } 1904 1905 private String now() { 1906 return MEMENTO_RFC_1123_FORMATTER.format(OffsetDateTime.now(ZoneOffset.UTC) 1907 .toInstant().truncatedTo(ChronoUnit.SECONDS)); 1908 } 1909 1910}