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}