org.fcrepo.integration.http.api.FedoraVersioningIT.java Source code

Java tutorial

Introduction

Here is the source code for org.fcrepo.integration.http.api.FedoraVersioningIT.java

Source

/*
 * Licensed to DuraSpace under one or more contributor license agreements.
 * See the NOTICE file distributed with this work for additional information
 * regarding copyright ownership.
 *
 * DuraSpace licenses this file to you under the Apache License,
 * Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.fcrepo.integration.http.api;

import static java.time.format.DateTimeFormatter.ISO_INSTANT;
import static java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME;
import static javax.ws.rs.core.HttpHeaders.ACCEPT;
import static javax.ws.rs.core.HttpHeaders.CONTENT_LENGTH;
import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE;
import static javax.ws.rs.core.HttpHeaders.LINK;
import static javax.ws.rs.core.HttpHeaders.LOCATION;
import static javax.ws.rs.core.Response.Status.BAD_REQUEST;
import static javax.ws.rs.core.Response.Status.CONFLICT;
import static javax.ws.rs.core.Response.Status.CREATED;
import static javax.ws.rs.core.Response.Status.NOT_ACCEPTABLE;
import static javax.ws.rs.core.Response.Status.FOUND;
import static javax.ws.rs.core.Response.Status.NOT_FOUND;
import static javax.ws.rs.core.Response.Status.NO_CONTENT;
import static javax.ws.rs.core.Response.Status.OK;
import static org.apache.jena.graph.Node.ANY;
import static org.apache.jena.graph.NodeFactory.createLiteral;
import static org.apache.jena.graph.NodeFactory.createURI;
import static org.apache.jena.rdf.model.ModelFactory.createDefaultModel;
import static org.apache.jena.rdf.model.ResourceFactory.createProperty;
import static org.apache.jena.vocabulary.DC_11.title;
import static org.apache.jena.vocabulary.RDF.type;
import static org.fcrepo.http.api.FedoraLdp.ACCEPT_DATETIME;
import static org.fcrepo.http.api.FedoraVersioning.MEMENTO_DATETIME_HEADER;
import static org.fcrepo.http.commons.domain.RDFMediaType.APPLICATION_LINK_FORMAT;
import static org.fcrepo.http.commons.domain.RDFMediaType.NTRIPLES;
import static org.fcrepo.http.commons.domain.RDFMediaType.N3;
import static org.fcrepo.http.commons.domain.RDFMediaType.POSSIBLE_RDF_RESPONSE_VARIANTS_STRING;
import static org.fcrepo.kernel.api.FedoraTypes.FCR_FIXITY;
import static org.fcrepo.kernel.api.FedoraTypes.FCR_METADATA;
import static org.fcrepo.kernel.api.FedoraTypes.FCR_VERSIONS;
import static org.fcrepo.kernel.api.FedoraTypes.FCR_ACL;
import static org.fcrepo.kernel.api.RdfLexicon.CONTAINER;
import static org.fcrepo.kernel.api.RdfLexicon.CONTAINS;
import static org.fcrepo.kernel.api.RdfLexicon.DESCRIBED_BY;
import static org.fcrepo.kernel.api.RdfLexicon.EMBED_CONTAINED;
import static org.fcrepo.kernel.api.RdfLexicon.FEDORA_BINARY;
import static org.fcrepo.kernel.api.RdfLexicon.MEMENTO_TYPE;
import static org.fcrepo.kernel.api.RdfLexicon.NON_RDF_SOURCE;
import static org.fcrepo.kernel.api.RdfLexicon.RDF_SOURCE;
import static org.fcrepo.kernel.api.RdfLexicon.RESOURCE;
import static org.fcrepo.kernel.api.RdfLexicon.VERSIONED_RESOURCE;
import static org.fcrepo.kernel.api.RdfLexicon.VERSIONING_TIMEMAP_TYPE;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.TimeUnit;

import javax.ws.rs.core.Link;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.client.methods.HttpOptions;
import org.apache.http.client.methods.HttpPatch;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.apache.jena.graph.Node;
import org.apache.jena.rdf.model.Model;
import org.apache.jena.rdf.model.Property;
import org.apache.jena.rdf.model.Resource;
import org.apache.jena.riot.Lang;
import org.apache.jena.riot.RDFDataMgr;
import org.apache.jena.riot.RDFFormat;
import org.apache.jena.riot.RDFLanguages;
import org.apache.jena.sparql.core.DatasetGraph;
import org.apache.jena.sparql.core.Quad;
import org.apache.jena.vocabulary.DC;
import org.apache.jena.vocabulary.RDF;
import org.fcrepo.http.commons.test.util.CloseableDataset;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;

/**
 * @author lsitu
 * @author bbpennel
 */
public class FedoraVersioningIT extends AbstractResourceIT {

    public static final DateTimeFormatter MEMENTO_DATETIME_ID_FORMATTER = DateTimeFormatter
            .ofPattern("yyyyMMddHHmmss").withZone(ZoneId.of("GMT"));

    private static final String VERSIONED_RESOURCE_LINK_HEADER = "<" + VERSIONED_RESOURCE.getURI()
            + ">; rel=\"type\"";
    private static final String BINARY_CONTENT = "binary content";
    private static final String BINARY_UPDATED = "updated content";

    private static final String OCTET_STREAM_TYPE = "application/octet-stream";

    private static final Node MEMENTO_TYPE_NODE = createURI(MEMENTO_TYPE);
    private static final Node TEST_PROPERTY_NODE = createURI("info:test#label");

    private static final Property TEST_PROPERTY = createProperty("info:test#label");

    private final String MEMENTO_DATETIME = RFC_1123_DATE_TIME
            .format(LocalDateTime.of(2000, 1, 1, 00, 00).atZone(ZoneOffset.UTC));
    private final List<String> rdfTypes = new ArrayList<>(Arrays.asList(POSSIBLE_RDF_RESPONSE_VARIANTS_STRING));

    private String subjectUri;
    private String id;

    @Rule
    public TemporaryFolder tmpDir = new TemporaryFolder();

    @Before
    public void init() {
        id = getRandomUniqueId();
        subjectUri = serverAddress + id;
    }

    @Test
    public void testDeleteTimeMapForContainer() throws Exception {
        createVersionedContainer(id);

        final String mementoUri = createMemento(subjectUri, null, null, null);
        assertEquals(200, getStatus(new HttpGet(mementoUri)));

        final String timeMapUri = subjectUri + "/" + FCR_VERSIONS;
        assertEquals(200, getStatus(new HttpGet(timeMapUri)));

        // disabled versioning to delete TimeMap
        assertEquals(NO_CONTENT.getStatusCode(),
                getStatus(new HttpDelete(serverAddress + id + "/" + FCR_VERSIONS)));

        // validate that the memento version is gone
        assertEquals(404, getStatus(new HttpGet(mementoUri)));
        // validate that the LDPCv is gone
        assertEquals(404, getStatus(new HttpGet(timeMapUri)));
    }

    @Test
    public void testGetTimeMapResponse() throws Exception {
        createVersionedContainer(id);

        createContainerMementoWithBody(subjectUri, MEMENTO_DATETIME);
        verifyTimemapResponse(subjectUri, id, MEMENTO_DATETIME);
    }

    @Test
    public void testGetTimeMapResponseMultipleMementos() throws Exception {
        createVersionedContainer(id);
        final String memento1 = RFC_1123_DATE_TIME
                .format(LocalDateTime.of(2000, 1, 1, 00, 00, 00).atOffset(ZoneOffset.UTC));
        final String memento2 = RFC_1123_DATE_TIME
                .format(LocalDateTime.of(2015, 8, 13, 18, 30, 0).atOffset(ZoneOffset.UTC));
        final String memento3 = RFC_1123_DATE_TIME
                .format(LocalDateTime.of(1980, 5, 31, 9, 15, 30).atOffset(ZoneOffset.UTC));
        createContainerMementoWithBody(subjectUri, memento1);
        createContainerMementoWithBody(subjectUri, memento2);
        createContainerMementoWithBody(subjectUri, memento3);
        final String[] mementos = { memento1, memento2, memento3 };
        verifyTimemapResponse(subjectUri, id, mementos, memento3, memento2);
    }

    @Test
    public void testGetTimeMapRDFSubject() throws Exception {
        createVersionedContainer(id);

        final HttpGet httpGet = getObjMethod(id + "/" + FCR_VERSIONS);

        try (final CloseableDataset dataset = getDataset(httpGet)) {
            final DatasetGraph results = dataset.asDatasetGraph();
            final Node subject = createURI(subjectUri + "/" + FCR_VERSIONS);
            assertTrue("Did not find correct subject", results.contains(ANY, subject, ANY, ANY));
        }
    }

    @Test
    public void testCreateVersion() throws Exception {
        createVersionedContainer(id);

        final String mementoUri = createContainerMementoWithBody(subjectUri, null);
        assertMementoUri(mementoUri, subjectUri);

        try (final CloseableDataset dataset = getDataset(new HttpGet(mementoUri))) {
            final DatasetGraph results = dataset.asDatasetGraph();

            final Node mementoSubject = createURI(mementoUri);

            assertFalse("Memento type should not be visible",
                    results.contains(ANY, mementoSubject, RDF.type.asNode(), MEMENTO_TYPE_NODE));
        }
    }

    @Test
    public void testCreateVersionWithSlugHeader() throws Exception {
        createVersionedContainer(id);

        // Bad request with Slug header to create memento
        final String mementoDateTime = "Tue, 3 Jun 2008 11:05:30 GMT";
        final String body = createContainerMementoBodyContent(subjectUri, N3);
        final HttpPost post = postObjMethod(id + "/" + FCR_VERSIONS);

        post.addHeader("Slug", "version_label");
        post.addHeader(MEMENTO_DATETIME_HEADER, mementoDateTime);
        post.addHeader(CONTENT_TYPE, N3);
        post.setEntity(new StringEntity(body));

        assertEquals("Created memento with Slug!", BAD_REQUEST.getStatusCode(), getStatus(post));
    }

    @Test
    public void testCreateVersionWithMementoDatetimeFormat() throws Exception {
        createVersionedContainer(id);

        // Create memento with RFC-1123 date-time format
        final String mementoDateTime = "Tue, 3 Jun 2008 11:05:30 GMT";
        final String body = createContainerMementoBodyContent(subjectUri, N3);

        final HttpPost post = postObjMethod(id + "/" + FCR_VERSIONS);

        post.addHeader(MEMENTO_DATETIME_HEADER, mementoDateTime);
        post.addHeader(CONTENT_TYPE, N3);
        post.setEntity(new StringEntity(body));

        assertEquals("Unable to create memento with RFC-1123 date-time format!", CREATED.getStatusCode(),
                getStatus(post));

        // Create memento with RFC-1123 date-time format in wrong value
        final String dateTime1 = "Tue, 13 Jun 2008 11:05:35 ANYTIMEZONE";
        final HttpPost post1 = postObjMethod(id + "/" + FCR_VERSIONS);

        post1.addHeader(CONTENT_TYPE, N3);
        post1.setEntity(new StringEntity(body));
        post1.addHeader(MEMENTO_DATETIME_HEADER, dateTime1);

        assertEquals(BAD_REQUEST.getStatusCode(), getStatus(post1));

        // Create memento in date-time format other than RFC-1123
        final String dateTime2 = "2000-01-01T01:01:01.11Z";
        final HttpPost post2 = postObjMethod(id + "/" + FCR_VERSIONS);

        post2.addHeader(CONTENT_TYPE, N3);
        post2.setEntity(new StringEntity(body));
        post2.addHeader(MEMENTO_DATETIME_HEADER, dateTime2);

        assertEquals(BAD_REQUEST.getStatusCode(), getStatus(post2));
    }

    @Test
    public void testCreateVersionWithDatetime() throws Exception {
        createVersionedContainer(id);

        final HttpPost createVersionMethod = postObjMethod(id + "/" + FCR_VERSIONS);
        createVersionMethod.addHeader(CONTENT_TYPE, N3);
        createVersionMethod.addHeader(MEMENTO_DATETIME_HEADER, MEMENTO_DATETIME);

        // Attempt to create memento with no body
        try (final CloseableHttpResponse response = execute(createVersionMethod)) {
            assertEquals("Didn't get a BAD_REQUEST response!", BAD_REQUEST.getStatusCode(), getStatus(response));

            // Request must fail with constrained exception due to empty body
            assertConstrainedByPresent(response);
        }
    }

    @Test
    public void testCreateContainerWithoutServerManagedTriples() throws Exception {
        createVersionedContainer(id);

        final HttpPost createMethod = postObjMethod(id + "/" + FCR_VERSIONS);
        createMethod.addHeader(CONTENT_TYPE, N3);
        createMethod.setEntity(new StringEntity("<" + subjectUri + "> <info:test#label> \"part\""));
        createMethod.addHeader(MEMENTO_DATETIME_HEADER, MEMENTO_DATETIME);

        // Attempt to create memento with partial record
        try (final CloseableHttpResponse response = execute(createMethod)) {
            assertEquals("Didn't get a BAD_REQUEST response!", BAD_REQUEST.getStatusCode(), getStatus(response));

            // Request must fail with constrained exception due to empty body
            assertConstrainedByPresent(response);
        }
    }

    /**
     * POST to create LDPCv without memento-datetime must ignore body
     *
     * @throws Exception in case of error with test
     */
    @Test
    public void testCreateVersionWithBody() throws Exception {
        createVersionedContainer(id);

        final String mementoUri = createContainerMementoWithBody(subjectUri, null);
        assertMementoUri(mementoUri, subjectUri);

        final HttpGet httpGet = new HttpGet(mementoUri);
        try (final CloseableDataset dataset = getDataset(httpGet)) {
            final DatasetGraph results = dataset.asDatasetGraph();

            final Node mementoSubject = createURI(subjectUri);

            assertTrue("Memento created without datetime must retain original state",
                    results.contains(ANY, mementoSubject, TEST_PROPERTY_NODE, createLiteral("foo")));
            assertFalse("Memento created without datetime must ignore updates",
                    results.contains(ANY, mementoSubject, TEST_PROPERTY_NODE, createLiteral("bar")));
        }
    }

    @Test
    public void testCreateVersionWithDatetimeAndBody() throws Exception {
        createVersionedContainer(id);

        final String mementoUri = createContainerMementoWithBody(subjectUri, MEMENTO_DATETIME);
        assertMementoUri(mementoUri, subjectUri);
        final Node mementoSubject = createURI(subjectUri);
        final Node subject = createURI(subjectUri);

        // Verify that the memento has the new property added to it
        try (final CloseableHttpResponse response = execute(new HttpGet(mementoUri))) {
            // Verify datetime was set correctly
            assertMementoDatetimeHeaderMatches(response, MEMENTO_DATETIME);

            final CloseableDataset dataset = getDataset(response);
            final DatasetGraph results = dataset.asDatasetGraph();

            assertFalse("Memento must not have original property",
                    results.contains(ANY, mementoSubject, TEST_PROPERTY_NODE, createLiteral("foo")));
            assertTrue("Memento must have updated property",
                    results.contains(ANY, mementoSubject, TEST_PROPERTY_NODE, createLiteral("bar")));
        }

        // Verify that the original is unchanged
        try (final CloseableDataset dataset = getDataset(new HttpGet(subjectUri))) {
            final DatasetGraph results = dataset.asDatasetGraph();

            assertTrue("Original must have original property",
                    results.contains(ANY, subject, TEST_PROPERTY_NODE, createLiteral("foo")));
            assertFalse("Original must not have updated property",
                    results.contains(ANY, subject, TEST_PROPERTY_NODE, createLiteral("bar")));
        }
    }

    @Test
    public void testCreateVersionDuplicateMementoDatetime() throws Exception {
        createVersionedContainer(id);

        // Create first memento
        final String mementoUri = createContainerMementoWithBody(subjectUri, MEMENTO_DATETIME);

        // Attempt to create second memento with same datetime, which should fail
        final HttpPost createVersionMethod = postObjMethod(id + "/" + FCR_VERSIONS);
        createVersionMethod.addHeader(CONTENT_TYPE, N3);
        final String body = "<" + subjectUri + "> <info:test#label> \"far\"";
        createVersionMethod.setEntity(new StringEntity(body));
        createVersionMethod.addHeader(MEMENTO_DATETIME_HEADER, MEMENTO_DATETIME);

        try (final CloseableHttpResponse response = execute(createVersionMethod)) {
            assertEquals("Duplicate memento datetime should return 409 status", CONFLICT.getStatusCode(),
                    getStatus(response));
        }

        final Node mementoSubject = createURI(subjectUri);
        // Verify first memento content persists
        try (final CloseableDataset dataset = getDataset(new HttpGet(mementoUri))) {
            final DatasetGraph results = dataset.asDatasetGraph();

            assertTrue("Memento must have first updated property",
                    results.contains(ANY, mementoSubject, TEST_PROPERTY_NODE, createLiteral("bar")));
            assertFalse("Memento must not have second updated property",
                    results.contains(ANY, mementoSubject, TEST_PROPERTY_NODE, createLiteral("far")));
        }
    }

    @Test
    public void testDeleteAndPostContainerMemento() throws Exception {
        createVersionedContainer(id);

        final String mementoUri = createContainerMementoWithBody(subjectUri, MEMENTO_DATETIME);

        assertEquals("Expected delete to succeed", NO_CONTENT.getStatusCode(),
                getStatus(new HttpDelete(mementoUri)));

        assertEquals("Deleted memento must be removed", NOT_FOUND.getStatusCode(),
                getStatus(new HttpGet(mementoUri)));

        final String recreatedUri = createContainerMementoWithBody(subjectUri, MEMENTO_DATETIME);
        assertEquals("Recreated memento must exist", OK.getStatusCode(), getStatus(new HttpGet(recreatedUri)));
    }

    @Test
    public void testDeleteAndPostBinaryMemento() throws Exception {
        createVersionedBinary(id);

        final String mementoUri = createLDPNRMementoWithExistingBody(MEMENTO_DATETIME);

        assertEquals("Expected delete to succeed", NO_CONTENT.getStatusCode(),
                getStatus(new HttpDelete(mementoUri)));

        assertEquals("Deleted memento must be removed", NOT_FOUND.getStatusCode(),
                getStatus(new HttpGet(mementoUri)));

        final String recreatedUri = createLDPNRMementoWithExistingBody(MEMENTO_DATETIME);
        assertEquals("Recreated memento must exist", OK.getStatusCode(), getStatus(new HttpGet(recreatedUri)));
    }

    @Test
    public void testDeleteAndPostDescriptionMemento() throws Exception {
        createVersionedBinary(id);

        final String descId = id + "/" + FCR_METADATA;
        final String descUri = serverAddress + descId;
        final String mementoUri = createMementoWithExistingBody(descId, MEMENTO_DATETIME, false);

        assertEquals("Expected delete to succeed", NO_CONTENT.getStatusCode(),
                getStatus(new HttpDelete(mementoUri)));

        assertEquals("Deleted memento must be removed", NOT_FOUND.getStatusCode(),
                getStatus(new HttpGet(mementoUri)));

        final String recreatedUri = createContainerMementoWithBody(descUri, MEMENTO_DATETIME);
        assertEquals("Recreated memento must exist", OK.getStatusCode(), getStatus(new HttpGet(recreatedUri)));
    }

    @Test
    public void testMementoContainmentReferences() throws Exception {
        createVersionedContainer(id);

        final String childUri = subjectUri + "/x";
        createObjectAndClose(id + "/x");

        // create memento
        final String mementoUri = createContainerMementoWithBody(subjectUri, MEMENTO_DATETIME);

        // Remove the child resource
        assertEquals("Expected delete to succeed", NO_CONTENT.getStatusCode(), getStatus(new HttpDelete(childUri)));

        // Ensure that the resource reference is gone
        try (final CloseableHttpResponse getResponse1 = execute(new HttpGet(subjectUri));
                final CloseableDataset dataset = getDataset(getResponse1);) {
            final DatasetGraph graph = dataset.asDatasetGraph();
            assertFalse("Expected NOT to have child resource: " + graph,
                    graph.contains(ANY, ANY, createURI(CONTAINS.getURI()), createURI(childUri)));
        }

        // Ensure that the resource reference is still in memento
        try (final CloseableHttpResponse getResponse1 = execute(new HttpGet(mementoUri));
                final CloseableDataset dataset = getDataset(getResponse1);) {
            final DatasetGraph graph = dataset.asDatasetGraph();
            assertTrue("Expected child resource NOT found: " + graph,
                    graph.contains(ANY, ANY, createURI(CONTAINS.getURI()), createURI(childUri)));
        }
    }

    @Test
    public void testMementoExternalReference() throws Exception {
        createVersionedContainer(id);

        final String pid = getRandomUniqueId();
        final String resource = serverAddress + pid;
        createObjectAndClose(pid);

        final HttpPatch updateObjectGraphMethod = patchObjMethod(id);
        updateObjectGraphMethod.addHeader(CONTENT_TYPE, "application/sparql-update");
        updateObjectGraphMethod.setEntity(new StringEntity(
                "INSERT {" + " <> <http://pcdm.org/models#hasMember> <" + resource + "> } WHERE {}"));
        executeAndClose(updateObjectGraphMethod);

        // create memento
        final String mementoUri = createContainerMementoWithBody(subjectUri, MEMENTO_DATETIME);

        // Remove the referencing resource
        assertEquals("Expected delete to succeed", NO_CONTENT.getStatusCode(), getStatus(new HttpDelete(resource)));

        // Ensure that the resource reference is gone
        try (final CloseableHttpResponse getResponse1 = execute(new HttpGet(subjectUri));
                final CloseableDataset dataset = getDataset(getResponse1);) {
            final DatasetGraph graph = dataset.asDatasetGraph();
            assertFalse("Expected NOT to have resource: " + graph,
                    graph.contains(ANY, ANY, createURI("http://pcdm.org/models#hasMember"), createURI(resource)));
        }

        try (final CloseableHttpResponse getResponse1 = execute(new HttpGet(mementoUri));
                final CloseableDataset dataset = getDataset(getResponse1);) {

            final DatasetGraph graph = dataset.asDatasetGraph();

            // Ensure that the resource reference is still in memento
            assertTrue("Expected resource NOT found: " + graph,
                    graph.contains(ANY, ANY, createURI("http://pcdm.org/models#hasMember"), createURI(resource)));

            // Ensure that the subject of the memento is the original reosurce
            assertTrue("Subjects should be the original resource, not the memento: " + graph,
                    !graph.contains(ANY, createURI(mementoUri), ANY, ANY));
        }
    }

    @Test
    public void testDescriptionMementoReference() throws Exception {
        // Create binary with description referencing other resource
        createVersionedBinary(id);

        final String referencedPid = getRandomUniqueId();
        final String referencedResource = serverAddress + referencedPid;
        createObjectAndClose(referencedPid);

        final String metadataId = id + "/fcr:metadata";
        final String metadataUri = serverAddress + metadataId;

        final String relation = "http://purl.org/dc/elements/1.1/relation";
        final HttpPatch updateObjectGraphMethod = patchObjMethod(metadataId);
        updateObjectGraphMethod.addHeader(CONTENT_TYPE, "application/sparql-update");
        updateObjectGraphMethod.setEntity(
                new StringEntity("INSERT {" + " <> <" + relation + "> <" + referencedResource + "> } WHERE {}"));
        executeAndClose(updateObjectGraphMethod);

        // Create memento
        final String mementoUri = createMemento(subjectUri, null, null, null);
        assertMementoUri(mementoUri, subjectUri);

        // Delete referenced resource
        assertEquals("Expected delete to succeed", NO_CONTENT.getStatusCode(),
                getStatus(new HttpDelete(referencedResource)));

        final Node originalBinaryNode = createURI(serverAddress + id);
        // Ensure that the resource reference is gone
        try (final CloseableHttpResponse getResponse1 = execute(new HttpGet(metadataUri));
                final CloseableDataset dataset = getDataset(getResponse1);) {
            final DatasetGraph graph = dataset.asDatasetGraph();
            assertFalse("Expected NOT to have resource: " + graph,
                    graph.contains(ANY, originalBinaryNode, createURI(relation), createURI(referencedResource)));
        }

        final String descMementoUrl = mementoUri.replace(FCR_VERSIONS, "fcr:metadata/fcr:versions");
        // Ensure that the resource reference is still in memento
        try (final CloseableHttpResponse getResponse1 = execute(new HttpGet(descMementoUrl));
                final CloseableDataset dataset = getDataset(getResponse1);) {
            final DatasetGraph graph = dataset.asDatasetGraph();
            assertTrue("Expected resource NOT found: " + graph,
                    graph.contains(ANY, originalBinaryNode, createURI(relation), createURI(referencedResource)));

            // Verify that described by link persists and there is only one
            final Iterator<Quad> describedIt = graph.find(ANY, originalBinaryNode, DESCRIBED_BY.asNode(), ANY);
            assertEquals(metadataUri, describedIt.next().getObject().getURI());
            assertFalse(describedIt.hasNext());
        }
    }

    @Test
    public void testPutOnTimeMapContainer() throws Exception {
        createVersionedContainer(id);

        // status 405: PUT On LPDCv is disallowed.
        assertEquals(405, getStatus(new HttpPut(serverAddress + id + "/" + FCR_VERSIONS)));
    }

    @Test
    public void testPatchOnTimeMapContainer() throws Exception {
        createVersionedContainer(id);

        // status 405: PATCH On LPDCv is disallowed.
        assertEquals(405, getStatus(new HttpPatch(serverAddress + id + "/" + FCR_VERSIONS)));
    }

    @Test
    public void testGetTimeMapResponseForBinary() throws Exception {
        createVersionedBinary(id);

        verifyTimemapResponse(subjectUri, id);
    }

    @Test
    public void testGetTimeMapResponseWithBadAcceptHeader() throws Exception {
        createVersionedContainer(id);

        final HttpGet httpGet = getObjMethod(id + "/" + FCR_VERSIONS);
        httpGet.setHeader("Accept", "application/arbitrary");
        try (final CloseableHttpResponse response = execute(httpGet)) {
            assertEquals("Should get a 'Not Acceptable' response!", NOT_ACCEPTABLE.getStatusCode(),
                    getStatus(response));
        }

    }

    @Test
    public void testGetTimeMapResponseForBinaryDescription() throws Exception {
        createVersionedBinary(id);

        final String descriptionUri = subjectUri + "/fcr:metadata";
        final String descriptionId = id + "/fcr:metadata";

        verifyTimemapResponse(descriptionUri, descriptionId);
    }

    /**
     * Verify an application/link-format TimeMap response.
     *
     * @param uri The full URI of the Original Resource.
     * @param id The path of the Original Resource.
     * @throws Exception on HTTP request error
     */
    private void verifyTimemapResponse(final String uri, final String id) throws Exception {
        verifyTimemapResponse(uri, id, null, null, null);
    }

    /**
     * Verify an application/link-format TimeMap response.
     *
     * @param uri The full URI of the Original Resource.
     * @param id The path of the Original Resource.
     * @param mementoDateTime a RFC-1123 datetime
     * @throws Exception on HTTP request error
     */
    private void verifyTimemapResponse(final String uri, final String id, final String mementoDateTime)
            throws Exception {
        final String[] mementoDateTimes = { mementoDateTime };
        verifyTimemapResponse(uri, id, mementoDateTimes, null, null);
    }

    /**
     * Verify an application/link-format TimeMap response.
     *
     * @param uri The full URI of the Original Resource.
     * @param id The path of the Original Resource.
     * @param mementoDateTime Array of all the RFC-1123 datetimes for all the mementos.
     * @param rangeStart RFC-1123 datetime of the first memento.
     * @param rangeEnd RFC-1123 datetime of the last memento.
     * @throws Exception on HTTP request error
     */
    private void verifyTimemapResponse(final String uri, final String id, final String[] mementoDateTime,
            final String rangeStart, final String rangeEnd) throws Exception {
        final String ldpcvUri = uri + "/" + FCR_VERSIONS;
        final List<Link> listLinks = new ArrayList<>();
        listLinks.add(Link.fromUri(uri).rel("original").build());
        listLinks.add(Link.fromUri(uri).rel("timegate").build());

        final javax.ws.rs.core.Link.Builder selfLink = Link.fromUri(ldpcvUri).rel("self")
                .type(APPLICATION_LINK_FORMAT);
        if (rangeStart != null && rangeEnd != null) {
            selfLink.param("from", rangeStart).param("until", rangeEnd);
        }
        listLinks.add(selfLink.build());
        if (mementoDateTime != null) {
            for (final String memento : mementoDateTime) {
                final TemporalAccessor instant = RFC_1123_DATE_TIME.parse(memento);
                listLinks.add(Link.fromUri(ldpcvUri + "/" + MEMENTO_DATETIME_ID_FORMATTER.format(instant))
                        .rel("memento").param("datetime", memento).build());
            }
        }

        final Link[] expectedLinks = listLinks.stream().sorted((a, b) -> a.toString().compareTo(b.toString()))
                .toArray(Link[]::new);

        final HttpGet httpGet = getObjMethod(id + "/" + FCR_VERSIONS);
        httpGet.setHeader("Accept", APPLICATION_LINK_FORMAT);
        try (final CloseableHttpResponse response = execute(httpGet)) {
            assertEquals("Didn't get a OK response!", OK.getStatusCode(), getStatus(response));
            // verify headers in link format.
            verifyTimeMapHeaders(response, uri);
            final List<String> bodyList = Arrays.asList(EntityUtils.toString(response.getEntity()).split(",\n"));
            //the links from the body are not

            final Link[] bodyLinks = bodyList.stream().map(String::trim).filter(t -> !t.isEmpty())
                    .sorted((a, b) -> a.toString().compareTo(b.toString())).map(Link::valueOf).toArray(Link[]::new);
            assertArrayEquals(expectedLinks, bodyLinks);
        }
    }

    /**
     * Utility function to verify TimeMap headers
     *
     * @param response the response
     * @param uri the URI of the resource.
     */
    private static void verifyTimeMapHeaders(final CloseableHttpResponse response, final String uri) {
        final String ldpcvUri = uri + "/" + FCR_VERSIONS;
        checkForLinkHeader(response, RESOURCE.toString(), "type");
        checkForLinkHeader(response, CONTAINER.toString(), "type");
        checkForLinkHeader(response, uri, "original");
        checkForLinkHeader(response, uri, "timegate");
        checkForLinkHeader(response, uri + "/" + FCR_VERSIONS, "timemap");
        checkForLinkHeader(response, VERSIONING_TIMEMAP_TYPE, "type");
        checkForLinkHeader(response, ldpcvUri + "/" + FCR_ACL, "acl");
        assertEquals(1, response.getHeaders("Accept-Post").length);
    }

    @Test
    public void testCreateVersionOfBinary() throws Exception {
        createVersionedBinary(id);

        final String mementoUri = createMemento(subjectUri, null, null, null);
        assertMementoUri(mementoUri, subjectUri);

        final HttpGet httpGet = new HttpGet(mementoUri);
        try (final CloseableHttpResponse response = execute(httpGet)) {
            assertMementoDatetimeHeaderPresent(response);
            assertEquals("Binary content of memento must match original content", BINARY_CONTENT,
                    EntityUtils.toString(response.getEntity()));
        }

        // Verifying that the associated description memento was created
        final String descriptionMementoUri = mementoUri.replace("fcr:versions", "fcr:metadata/fcr:versions");

        final HttpGet descGet = new HttpGet(descriptionMementoUri);
        try (final CloseableHttpResponse response = execute(descGet)) {
            assertMementoDatetimeHeaderPresent(response);
            assertHasLink(response, type, RDF_SOURCE.getURI());
        }
    }

    @Test
    public void testCreateVersionOfBinaryWithDatetimeAndContentType() throws Exception {
        createVersionedBinary(id);

        final String mementoUri = createMemento(subjectUri, MEMENTO_DATETIME, OCTET_STREAM_TYPE, null);
        assertMementoUri(mementoUri, subjectUri);

        // Verify that the memento has the updated binary
        try (final CloseableHttpResponse response = execute(new HttpGet(mementoUri))) {
            assertMementoDatetimeHeaderMatches(response, MEMENTO_DATETIME);

            assertEquals("Binary content of memento must be empty", "", EntityUtils.toString(response.getEntity()));
            assertEquals(OCTET_STREAM_TYPE, response.getFirstHeader(CONTENT_TYPE).getValue());
        }
    }

    @Test
    public void testCreateVersionOfBinaryWithBody() throws Exception {
        createVersionedBinary(id);

        final String mementoUri = createMemento(subjectUri, null, OCTET_STREAM_TYPE, BINARY_UPDATED);
        assertMementoUri(mementoUri, subjectUri);

        final HttpGet httpGet = new HttpGet(mementoUri);
        try (final CloseableHttpResponse response = execute(httpGet)) {
            assertMementoDatetimeHeaderPresent(response);

            assertEquals("Binary content of memento must not have changed", BINARY_CONTENT,
                    EntityUtils.toString(response.getEntity()));
        }
    }

    @Test
    public void testCreateVersionOfBinaryWithDatetimeAndBody() throws Exception {
        createVersionedBinary(id);

        final String mementoUri = createMemento(subjectUri, MEMENTO_DATETIME, "text/plain", BINARY_UPDATED);
        assertMementoUri(mementoUri, subjectUri);

        // Verify that the memento has the updated binary
        try (final CloseableHttpResponse response = execute(new HttpGet(mementoUri))) {
            assertMementoDatetimeHeaderMatches(response, MEMENTO_DATETIME);

            // Content-type is not retained for a binary memento created without description
            assertEquals(OCTET_STREAM_TYPE, response.getFirstHeader(CONTENT_TYPE).getValue());

            assertEquals("Binary content of memento must match updated content", BINARY_UPDATED,
                    EntityUtils.toString(response.getEntity()));
        }
    }

    @Test
    public void testCreateVersionOfBinaryDescription() throws Exception {
        createVersionedBinary(id);

        final String descriptionUri = subjectUri + "/fcr:metadata";

        final String mementoUri = createContainerMementoWithBody(descriptionUri, null);
        assertMementoUri(mementoUri, descriptionUri);

        setDescriptionProperty(id, null, DC.title.getURI(), "Updated");

        try (final CloseableDataset dataset = getDataset(new HttpGet(mementoUri))) {
            final DatasetGraph results = dataset.asDatasetGraph();

            final Node mementoSubject = createURI(subjectUri);

            assertFalse("Property added to original must not appear in memento",
                    results.contains(ANY, mementoSubject, DC.title.asNode(), ANY));
            assertFalse("Memento type should not be visible",
                    results.contains(ANY, mementoSubject, RDF.type.asNode(), MEMENTO_TYPE_NODE));
            assertTrue("Must have binary type",
                    results.contains(ANY, mementoSubject, RDF.type.asNode(), FEDORA_BINARY.asNode()));
        }

        // No binary memento should be created when specifically creating a description memento.
        final String hypotheticalBinaryUri = mementoUri.replaceAll("fcr:metadata/fcr:versions", "fcr:versions");
        assertEquals(NOT_FOUND.getStatusCode(), getStatus(new HttpGet(hypotheticalBinaryUri)));
    }

    /*
     * Attempt to create binary description with container triples
     */
    @Test
    public void testCreateVersionOfBinaryDescriptionInvalidTriples() throws Exception {
        final String containerId = getRandomUniqueId();
        final String containerSubjectUri = serverAddress + containerId;
        createObjectAndClose(containerId);

        createVersionedBinary(id);

        final String descriptionUri = subjectUri + "/fcr:metadata";

        final String containerBody = createContainerMementoBodyContent(containerSubjectUri, "text/n3");
        final HttpPost createMethod = postObjMethod(descriptionUri);
        createMethod.addHeader(CONTENT_TYPE, "text/n3");
        createMethod.setEntity(new StringEntity(containerBody));

        // Attempt to create memento with partial record
        try (final CloseableHttpResponse response = execute(createMethod)) {
            assertEquals("Didn't get a BAD_REQUEST response!", BAD_REQUEST.getStatusCode(), getStatus(response));
        }
    }

    @Test
    public void testCreateVersionBinaryDescriptionWithBodyAndDatetime() throws Exception {
        createVersionedBinary(id);

        final String descriptionUri = subjectUri + "/fcr:metadata";

        final String mementoUri = createContainerMementoWithBody(descriptionUri, MEMENTO_DATETIME);
        assertMementoUri(mementoUri, descriptionUri);

        try (final CloseableDataset dataset = getDataset(new HttpGet(mementoUri))) {
            final DatasetGraph results = dataset.asDatasetGraph();

            final Node mementoSubject = createURI(subjectUri);

            assertFalse("Memento type should not be visible",
                    results.contains(ANY, mementoSubject, RDF.type.asNode(), MEMENTO_TYPE_NODE));
            assertTrue("Memento must have first updated property",
                    results.contains(ANY, mementoSubject, TEST_PROPERTY_NODE, createLiteral("bar")));
        }
    }

    @Test
    public void testCreateVersionHistoricBinaryAndDescription() throws Exception {
        createVersionedBinary(id, "text/plain", BINARY_CONTENT);

        final String descriptionUri = subjectUri + "/fcr:metadata";

        final String binaryMementoUri = createMemento(subjectUri, MEMENTO_DATETIME, null, "content");
        assertMementoUri(binaryMementoUri, subjectUri);

        final String mementoUri = createContainerMementoWithBody(descriptionUri, MEMENTO_DATETIME);
        assertMementoUri(mementoUri, descriptionUri);

        try (final CloseableDataset dataset = getDataset(new HttpGet(mementoUri))) {
            final DatasetGraph results = dataset.asDatasetGraph();

            final Node mementoSubject = createURI(subjectUri);

            assertTrue("Type must be a fedora:Binary",
                    results.contains(ANY, mementoSubject, RDF.type.asNode(), FEDORA_BINARY.asNode()));
            assertTrue("Memento must have first updated property",
                    results.contains(ANY, mementoSubject, TEST_PROPERTY_NODE, createLiteral("bar")));
        }

        // Verify that the memento has the updated binary
        try (final CloseableHttpResponse response = execute(new HttpGet(binaryMementoUri))) {
            assertMementoDatetimeHeaderMatches(response, MEMENTO_DATETIME);

            assertEquals("Binary content of memento must reflect historic version", "content",
                    EntityUtils.toString(response.getEntity()));
            assertEquals("text/plain", response.getFirstHeader(CONTENT_TYPE).getValue());
        }
    }

    @Test
    public void testGetUnversionedObjectVersionProfile() throws Exception {
        final String containerUri = serverAddress + id;

        createObject(id);

        final HttpGet getVersion = new HttpGet(containerUri + "/" + FCR_VERSIONS);
        assertEquals(NOT_FOUND.getStatusCode(), getStatus(getVersion));

        // Verify original has no memento headers
        assertNoMementoHeaders(containerUri);
    }

    @Test
    public void testGetUnversionedBinaryAndDescriptionVersionProfile() throws Exception {
        createDatastream(id, "ds", "content");

        final String binaryUri = serverAddress + id + "/ds";
        final HttpGet getBinary = new HttpGet(binaryUri + "/" + FCR_VERSIONS);
        assertEquals(NOT_FOUND.getStatusCode(), getStatus(getBinary));

        // Verify original binary has no memento headers
        assertNoMementoHeaders(binaryUri);

        final String metadataUri = binaryUri + "/" + FCR_METADATA;
        final HttpGet getDesc = new HttpGet(metadataUri + "/" + FCR_VERSIONS);
        assertEquals(NOT_FOUND.getStatusCode(), getStatus(getDesc));

        // Verify original has no memento headers
        assertNoMementoHeaders(metadataUri);
    }

    private void assertNoMementoHeaders(final String uri) throws Exception {
        final HttpGet getOriginal = new HttpGet(uri);

        try (final CloseableHttpResponse response = execute(getOriginal)) {
            assertEquals(OK.getStatusCode(), getStatus(response));

            assertNoLinkHeader(response, MEMENTO_TYPE, "type");
            assertNoLinkHeader(response, uri, "original");
            assertNoMementoDatetimeHeaderPresent(response);
        }
    }

    @Test
    public void testAddAndRetrieveVersion() throws Exception {
        createVersionedContainer(id);

        logger.debug("Setting a title");
        patchLiteralProperty(serverAddress + id, title.getURI(), "First Title");

        try (final CloseableDataset dataset = getContent(serverAddress + id)) {
            assertTrue("Should find original title",
                    dataset.asDatasetGraph().contains(ANY, ANY, title.asNode(), createLiteral("First Title")));
        }
        logger.debug("Posting version v0.0.1");
        final String mementoUri = createContainerMementoWithBody(subjectUri, null);
        assertMementoUri(mementoUri, subjectUri);

        logger.debug("Replacing the title");
        patchLiteralProperty(serverAddress + id, title.getURI(), "Second Title");

        try (final CloseableDataset dataset = getContent(mementoUri)) {
            logger.debug("Got version profile:");
            final DatasetGraph versionResults = dataset.asDatasetGraph();

            assertTrue("Should find a title in historic version",
                    versionResults.contains(ANY, ANY, title.asNode(), ANY));
            assertTrue("Should find original title in historic version",
                    versionResults.contains(ANY, ANY, title.asNode(), createLiteral("First Title")));
            assertFalse("Should not find the updated title in historic version",
                    versionResults.contains(ANY, ANY, title.asNode(), createLiteral("Second Title")));
        }
    }

    @Test
    public void testInvalidVersionDatetime() throws Exception {
        final String invalidDate = "blah";
        createVersionedContainer(id);

        // Create memento body
        final String body = createContainerMementoBodyContent(subjectUri, N3);

        final HttpPost postReq = postObjMethod(serverAddress + id + "/" + FCR_VERSIONS);
        postReq.addHeader(MEMENTO_DATETIME_HEADER, invalidDate);
        postReq.addHeader(CONTENT_TYPE, N3);
        postReq.setEntity(new StringEntity(body));

        assertEquals(BAD_REQUEST.getStatusCode(), getStatus(postReq));
    }

    @Test
    public void testEnableVersioning() throws Exception {
        final String containerUri = serverAddress + id;
        createObjectAndClose(id);

        final String versionsUri = containerUri + "/" + FCR_VERSIONS;
        assertEquals("fcr:versions must not exist before enabling versioning", NOT_FOUND.getStatusCode(),
                getStatus(new HttpGet(versionsUri)));

        // Enable versioning
        enableVersioning(containerUri);

        assertEquals("fcr:versions must be available after enabling versioning", OK.getStatusCode(),
                getStatus(new HttpGet(versionsUri)));

        final String mementoUri = createMemento(subjectUri, null, null, null);
        assertEquals("Memento must be created", OK.getStatusCode(), getStatus(new HttpGet(mementoUri)));
    }

    @Test
    public void testEnableVersioningBinary() throws Exception {
        final String binaryUri = serverAddress + id + "/ds";
        createDatastream(id, "ds", "content");

        final String versionsUri = binaryUri + "/" + FCR_VERSIONS;
        assertEquals("fcr:versions must not exist before enabling versioning", NOT_FOUND.getStatusCode(),
                getStatus(new HttpGet(versionsUri)));
        final String descUri = serverAddress + id + "/ds/" + FCR_METADATA;
        final String versionsDescUri = descUri + "/" + FCR_VERSIONS;

        assertEquals("fcr:metadata/fcr:versions must not exist before enabling versioning",
                NOT_FOUND.getStatusCode(), getStatus(new HttpGet(versionsDescUri)));

        // Enable versioning
        enableVersioning(binaryUri);

        assertEquals("fcr:versions must be available after enabling versioning", OK.getStatusCode(),
                getStatus(new HttpGet(versionsUri)));

        assertEquals("fcr:metadata/fcr:versions must be available after enabling versioning", OK.getStatusCode(),
                getStatus(new HttpGet(versionsDescUri)));

        final String mementoUri = createMemento(subjectUri, null, null, null);
        assertEquals("Memento must be created", OK.getStatusCode(), getStatus(new HttpGet(mementoUri)));
    }

    @Test
    public void testTimeMapResponseContentTypes() throws Exception {
        createVersionedContainer(id);

        final String[] timeMapResponseTypes = getTimeMapResponseTypes();
        for (final String type : timeMapResponseTypes) {
            final HttpGet method = new HttpGet(serverAddress + id + "/fcr:versions");
            method.addHeader(ACCEPT, type);
            assertEquals(type, getContentType(method));
        }
    }

    @Test
    public void testGetVersionResponseContentTypes() throws Exception {
        createVersionedContainer(id);
        final String versionUri = createContainerMementoWithBody(subjectUri, MEMENTO_DATETIME);

        final String[] rdfResponseTypes = rdfTypes.toArray(new String[rdfTypes.size()]);
        ;
        for (final String type : rdfResponseTypes) {
            final HttpGet method = new HttpGet(versionUri);
            method.addHeader(ACCEPT, type);
            assertEquals(type, getContentType(method));
        }
    }

    @Test
    public void testDatetimeNegotiationLDPRv() throws Exception {
        final CloseableHttpClient customClient = createClient(true);
        final DateTimeFormatter FMT = RFC_1123_DATE_TIME.withZone(ZoneId.of("UTC"));

        createVersionedContainer(id);
        final String memento1 = FMT.format(ISO_INSTANT.parse("2017-06-10T11:41:00Z", Instant::from));
        final String version1Uri = createLDPRSMementoWithExistingBody(memento1);
        final String memento2 = FMT.format(ISO_INSTANT.parse("2016-06-17T11:41:00Z", Instant::from));
        final String version2Uri = createLDPRSMementoWithExistingBody(memento2);

        final String request1Datetime = FMT.format(ISO_INSTANT.parse("2017-01-12T00:00:00Z", Instant::from));
        final HttpGet getMemento = getObjMethod(id);
        getMemento.addHeader(ACCEPT_DATETIME, request1Datetime);

        try (final CloseableHttpResponse response = customClient.execute(getMemento)) {
            assertEquals("Did not get FOUND response", FOUND.getStatusCode(), getStatus(response));
            assertNoMementoDatetimeHeaderPresent(response);
            assertEquals("Did not get Location header", version2Uri, response.getFirstHeader(LOCATION).getValue());
            assertEquals("Did not get Content-Length == 0", "0",
                    response.getFirstHeader(CONTENT_LENGTH).getValue());
        }

        final String request2Datetime = FMT.format(ISO_INSTANT.parse("2018-01-10T00:00:00Z", Instant::from));
        final HttpGet getMemento2 = getObjMethod(id);
        getMemento2.addHeader(ACCEPT_DATETIME, request2Datetime);

        try (final CloseableHttpResponse response = customClient.execute(getMemento2)) {
            assertEquals("Did not get FOUND response", FOUND.getStatusCode(), getStatus(response));
            assertNoMementoDatetimeHeaderPresent(response);
            assertEquals("Did not get Location header", version1Uri, response.getFirstHeader(LOCATION).getValue());
            assertEquals("Did not get Content-Length == 0", "0",
                    response.getFirstHeader(CONTENT_LENGTH).getValue());
        }
    }

    @Test
    public void testDatetimeNegotiationNoMementos() throws Exception {
        final CloseableHttpClient customClient = createClient(true);
        final DateTimeFormatter FMT = RFC_1123_DATE_TIME.withZone(ZoneId.of("UTC"));

        createVersionedContainer(id);
        final String requestDatetime = FMT.format(ISO_INSTANT.parse("2017-01-12T00:00:00Z", Instant::from));
        final HttpGet getMemento = getObjMethod(id);
        getMemento.addHeader(ACCEPT_DATETIME, requestDatetime);

        try (final CloseableHttpResponse response = customClient.execute(getMemento)) {
            assertEquals("Did not get NOT_FOUND response", NOT_FOUND.getStatusCode(), getStatus(response));
            assertNull("Did not expect a Location header", response.getFirstHeader(LOCATION));
            assertNotEquals("Did not get Content-Length > 0", 0,
                    response.getFirstHeader(CONTENT_LENGTH).getValue());
        }
    }

    @Test
    public void testFixityOnVersionedResource() throws Exception {
        createVersionedBinary(id);

        final String mementoUri = createMemento(subjectUri, null, null, null);

        final HttpGet checkFixity = new HttpGet(mementoUri + "/" + FCR_FIXITY);
        try (final CloseableHttpResponse response = execute(checkFixity)) {
            assertEquals("Did not get OK response", OK.getStatusCode(), getStatus(response));
        }
    }

    @Test
    public void testOptionsMemento() throws Exception {
        createVersionedContainer(id);
        final String mementoUri = createContainerMementoWithBody(subjectUri, null);

        final HttpOptions optionsRequest = new HttpOptions(mementoUri);
        try (final CloseableHttpResponse optionsResponse = execute(optionsRequest)) {
            assertEquals(OK.getStatusCode(), optionsResponse.getStatusLine().getStatusCode());
            assertMementoOptionsHeaders(optionsResponse);
        }
    }

    @Test
    public void testPatchOnMemento() throws Exception {
        createVersionedContainer(id);

        final String mementoUri = createContainerMementoWithBody(subjectUri, MEMENTO_DATETIME);
        final HttpPatch patch = new HttpPatch(mementoUri);
        patch.addHeader(CONTENT_TYPE, "application/sparql-update");
        patch.setEntity(new StringEntity("INSERT DATA { <> <" + title.getURI() + "> \"Memento title\" } "));

        // status 405: PATCH on memento is not allowed.
        assertEquals(405, getStatus(patch));
    }

    @Test
    public void testPostOnMemento() throws Exception {
        createVersionedContainer(id);

        final String mementoUri = createContainerMementoWithBody(subjectUri, MEMENTO_DATETIME);
        final String body = createContainerMementoBodyContent(subjectUri, N3);
        final HttpPost post = new HttpPost(mementoUri);
        post.addHeader(CONTENT_TYPE, N3);
        post.setEntity(new StringEntity(body));

        // status 405: POST on memento is not allowed.
        assertEquals(405, getStatus(post));
    }

    @Test
    public void testPutOnMemento() throws Exception {
        createVersionedContainer(id);

        final String mementoUri = createContainerMementoWithBody(subjectUri, MEMENTO_DATETIME);
        final String body = createContainerMementoBodyContent(subjectUri, N3);
        final HttpPut put = new HttpPut(mementoUri);
        put.addHeader(CONTENT_TYPE, N3);
        put.setEntity(new StringEntity(body));

        // status 405: PUT on memento is not allowed.
        assertEquals(405, getStatus(put));
    }

    @Test
    public void testGetLDPRSMementoHeaders() throws Exception {
        final DateTimeFormatter FMT = RFC_1123_DATE_TIME.withZone(ZoneId.of("UTC"));
        createVersionedContainer(id);

        final String memento1 = FMT.format(ISO_INSTANT.parse("2001-06-10T16:41:00Z", Instant::from));
        final String version1Uri = createLDPRSMementoWithExistingBody(memento1);
        final HttpGet getRequest = new HttpGet(version1Uri);

        try (final CloseableHttpResponse response = execute(getRequest)) {
            assertMementoDatetimeHeaderMatches(response, memento1);
            checkForLinkHeader(response, MEMENTO_TYPE, "type");
            checkForLinkHeader(response, subjectUri, "original");
            checkForLinkHeader(response, subjectUri, "timegate");
            checkForLinkHeader(response, subjectUri + "/" + FCR_VERSIONS, "timemap");
            checkForLinkHeader(response, RESOURCE.toString(), "type");
            assertNoLinkHeader(response, VERSIONED_RESOURCE.toString(), "type");
            assertNoLinkHeader(response, VERSIONING_TIMEMAP_TYPE.toString(), "type");
            assertNoLinkHeader(response, version1Uri + "/" + FCR_ACL, "acl");
        }
    }

    @Test
    public void testGetLDPNRMementoHeaders() throws Exception {
        final DateTimeFormatter FMT = RFC_1123_DATE_TIME.withZone(ZoneId.of("UTC"));
        createVersionedBinary(id, "text/plain", "This is some versioned content");

        final String memento1 = FMT.format(ISO_INSTANT.parse("2001-06-10T16:41:00Z", Instant::from));
        final String version1Uri = createLDPNRMementoWithExistingBody(memento1);
        final HttpGet getRequest = new HttpGet(version1Uri);

        try (final CloseableHttpResponse response = execute(getRequest)) {
            assertMementoDatetimeHeaderMatches(response, memento1);
            checkForLinkHeader(response, MEMENTO_TYPE, "type");
            checkForLinkHeader(response, subjectUri, "original");
            checkForLinkHeader(response, subjectUri, "timegate");
            checkForLinkHeader(response, subjectUri + "/" + FCR_VERSIONS, "timemap");
            checkForLinkHeader(response, NON_RDF_SOURCE.toString(), "type");
            assertNoLinkHeader(response, VERSIONED_RESOURCE.toString(), "type");
            assertNoLinkHeader(response, VERSIONING_TIMEMAP_TYPE.toString(), "type");
            assertNoLinkHeader(response, version1Uri + "/" + FCR_ACL, "acl");
        }
    }

    /*
     * Verify binary description timemap RDF representation can be retrieved with and without
     * accompanying binary memento
     */
    @Test
    public void testFcrepo2792() throws Exception {
        // 1. Create versioned resource
        createVersionedBinary(id);

        final String descriptionUri = subjectUri + "/fcr:metadata";
        final String descTimemapUri = descriptionUri + "/" + FCR_VERSIONS;

        // 2. verify that metadata versions endpoint returns 200
        assertEquals(OK.getStatusCode(), getStatus(new HttpGet(descTimemapUri)));

        // 3. create a binary version against binary timemap
        final String mementoUri = createMemento(subjectUri, null, null, null);
        final String descMementoUri = mementoUri.replace("fcr:versions", "fcr:metadata/fcr:versions");

        final Node timemapSubject = createURI(descTimemapUri);
        final Node descMementoResc = createURI(descMementoUri);
        // 4. verify that the binary description timemap RDF is there and contains the new description memento
        try (final CloseableDataset dataset = getDataset(new HttpGet(descTimemapUri))) {
            final DatasetGraph results = dataset.asDatasetGraph();
            assertTrue("Timemap RDF response must contain description memento",
                    results.contains(ANY, timemapSubject, CONTAINS.asNode(), descMementoResc));
        }

        // Wait a second to avoid timestamp collisions
        TimeUnit.SECONDS.sleep(1);

        // 5. Create a second binary description memento
        final String descMementoUri2 = createMemento(descriptionUri, null, null, null);

        // 6. verify that the binary description timemap availabe (returns 404 in fcrepo-2792)
        try (final CloseableDataset dataset = getDataset(new HttpGet(descTimemapUri))) {
            final DatasetGraph results = dataset.asDatasetGraph();
            final Node descMementoResc2 = createURI(descMementoUri2);

            assertTrue("Timemap RDF response must contain first description memento",
                    results.contains(ANY, timemapSubject, CONTAINS.asNode(), descMementoResc));
            assertTrue("Timemap RDF response must contain second description memento",
                    results.contains(ANY, timemapSubject, CONTAINS.asNode(), descMementoResc2));
        }
    }

    @Test
    public void testOptionsTimeMap() throws Exception {
        createVersionedContainer(id);
        final String timemapUri = subjectUri + "/" + FCR_VERSIONS;

        try (final CloseableHttpResponse response = execute(new HttpOptions(timemapUri))) {
            assertEquals(OK.getStatusCode(), getStatus(response));
            verifyTimeMapHeaders(response, subjectUri);
        }
    }

    @Test
    public void testCreateExternalBinaryProxyVersion() throws Exception {
        // Create binary to use as content for proxying
        final String proxyContent = "proxied content";
        final String proxiedId = getRandomUniqueId();
        final String proxiedUri = serverAddress + proxiedId + "/ds";
        createDatastream(proxiedId, "ds", proxyContent);

        // Create the proxied external binary object using the first binary
        createVersionedExternalBinaryMemento(id, "proxy", proxiedUri);

        // Create a version of the external binary using the second binary as content
        final String mementoUri = createMemento(subjectUri, null, null, null);

        // Verify that the historic version exists and proxies the old content
        final HttpGet httpGet1 = new HttpGet(mementoUri);
        try (final CloseableHttpResponse getResponse = execute(httpGet1)) {
            assertEquals(OK.getStatusCode(), getStatus(getResponse));
            assertMementoDatetimeHeaderPresent(getResponse);
            assertEquals(proxiedUri, getContentLocation(getResponse));
            final String content = EntityUtils.toString(getResponse.getEntity());
            assertEquals("Entity Data doesn't match proxied versioned content!", proxyContent, content);
        }
    }

    @Test
    public void testCreateHistoricExternalBinaryProxyVersion() throws Exception {
        // Create two binaries to use as content for proxying
        final String newContent = "new content";
        final String proxied1Id = getRandomUniqueId();
        final String proxied1Uri = serverAddress + proxied1Id + "/ds";
        createDatastream(proxied1Id, "ds", newContent);

        final String oldContent = "old content";
        final String proxied2Id = getRandomUniqueId();
        final String proxied2Uri = serverAddress + proxied2Id + "/ds";
        createDatastream(proxied2Id, "ds", "old content");

        // Create the proxied external binary object using the first binary
        createVersionedExternalBinaryMemento(id, "proxy", proxied1Uri);

        // Create a historic version of the external binary using the second binary as content
        final String mementoUri = createExternalBinaryMemento(subjectUri, "proxy", proxied2Uri);

        // Verify that the historic version exists and proxies the old content
        final HttpGet httpGet1 = new HttpGet(mementoUri);
        try (final CloseableHttpResponse getResponse = execute(httpGet1)) {
            assertEquals(OK.getStatusCode(), getStatus(getResponse));
            assertMementoDatetimeHeaderMatches(getResponse, MEMENTO_DATETIME);
            assertEquals(proxied2Uri, getContentLocation(getResponse));
            final String content = EntityUtils.toString(getResponse.getEntity());
            assertEquals("Entity Data doesn't match proxied historic content!", oldContent, content);
        }

        // Verify that the current version still proxies the correct content
        final HttpGet httpGet2 = new HttpGet(subjectUri);
        try (final CloseableHttpResponse getResponse = execute(httpGet2)) {
            assertEquals(OK.getStatusCode(), getStatus(getResponse));
            assertEquals(proxied1Uri, getContentLocation(getResponse));
            final String content = EntityUtils.toString(getResponse.getEntity());
            assertEquals("Entity Data doesn't match proxied historic content!", newContent, content);
        }
    }

    @Test
    public void testCreateHistoricExternalBinaryRedirectVersion() throws Exception {
        // Create two binaries to use as content for proxying
        final String newContent = "new content";
        final String ext1Id = getRandomUniqueId();
        final String ext1Uri = serverAddress + ext1Id + "/ds";
        createDatastream(ext1Id, "ds", newContent);

        final String oldContent = "old content";
        final String ext2Id = getRandomUniqueId();
        final String ext2Uri = serverAddress + ext2Id + "/ds";
        createDatastream(ext2Id, "ds", "old content");

        // Create the proxied external binary object using the first binary
        createVersionedExternalBinaryMemento(id, "redirect", ext1Uri);

        // Create a historic version of the external binary using the second binary as content
        final String mementoUri = createExternalBinaryMemento(subjectUri, "redirect", ext2Uri);

        // Verify that the historic version exists and redirects to the old content
        final HttpGet httpGet1 = new HttpGet(mementoUri);
        try (final CloseableHttpResponse getResponse = execute(httpGet1)) {
            assertEquals(OK.getStatusCode(), getStatus(getResponse));
            final String content = EntityUtils.toString(getResponse.getEntity());
            assertEquals("Content doesn't match redirected historic content", oldContent, content);
        }

        // Verify that the current version still redirects to the correct content
        final HttpGet httpGet2 = new HttpGet(subjectUri);
        try (final CloseableHttpResponse getResponse = execute(httpGet2)) {
            assertEquals(OK.getStatusCode(), getStatus(getResponse));
            final String content = EntityUtils.toString(getResponse.getEntity());
            assertEquals("Content doesn't match redirected historic content", newContent, content);
        }
    }

    @Test
    public void testCreateHistoricExternalBinaryCopyVersion() throws Exception {
        final String newContent = "new content";

        final String oldContent = "old content";
        final File localFile = tmpDir.newFile();
        FileUtils.writeStringToFile(localFile, oldContent, "UTF-8");

        createVersionedBinary(id, "text/plain", newContent);

        final String mementoUri = createExternalBinaryMemento(subjectUri, "copy", localFile.toURI().toString());

        // Verify that the historic version exists and is the copied old content
        final HttpGet httpGet1 = new HttpGet(mementoUri);
        try (final CloseableHttpResponse getResponse = execute(httpGet1)) {
            assertEquals(OK.getStatusCode(), getStatus(getResponse));
            assertMementoDatetimeHeaderMatches(getResponse, MEMENTO_DATETIME);
            final String content = EntityUtils.toString(getResponse.getEntity());
            assertEquals("Content doesn't match copied historic content", oldContent, content);
        }

        // Verify that the current version is still available
        final HttpGet httpGet2 = new HttpGet(subjectUri);
        try (final CloseableHttpResponse getResponse = execute(httpGet2)) {
            assertEquals(OK.getStatusCode(), getStatus(getResponse));
            final String content = EntityUtils.toString(getResponse.getEntity());
            assertEquals("Binary doesn't match expected content", newContent, content);
        }
    }

    private void createVersionedExternalBinaryMemento(final String rescId, final String handling,
            final String externalUri) throws Exception {
        final HttpPut httpPut = putObjMethod(rescId);
        httpPut.addHeader(LINK, "<" + NON_RDF_SOURCE.getURI() + ">;rel=\"type\"");
        httpPut.addHeader(LINK, getExternalContentLinkHeader(externalUri, handling, null));
        httpPut.addHeader(LINK, VERSIONED_RESOURCE_LINK_HEADER);
        try (final CloseableHttpResponse response = execute(httpPut)) {
            assertEquals("Didn't get a CREATED response!", CREATED.getStatusCode(), getStatus(response));
        }
    }

    private String createExternalBinaryMemento(final String rescUri, final String handling,
            final String externalUri) throws Exception {
        final String ldpcvUri = rescUri + "/fcr:versions";
        final HttpPost httpPost = new HttpPost(ldpcvUri);
        httpPost.addHeader(LINK, getExternalContentLinkHeader(externalUri, handling, null));
        httpPost.addHeader(MEMENTO_DATETIME_HEADER, MEMENTO_DATETIME);

        try (final CloseableHttpResponse response = execute(httpPost)) {
            assertEquals("Didn't get a CREATED response!", CREATED.getStatusCode(), getStatus(response));
            assertMementoDatetimeHeaderPresent(response);
            return response.getFirstHeader(LOCATION).getValue();
        }
    }

    private static void assertMementoOptionsHeaders(final HttpResponse httpResponse) {
        final List<String> methods = headerValues(httpResponse, "Allow");
        assertTrue("Should allow GET", methods.contains(HttpGet.METHOD_NAME));
        assertTrue("Should allow HEAD", methods.contains(HttpHead.METHOD_NAME));
        assertTrue("Should allow OPTIONS", methods.contains(HttpOptions.METHOD_NAME));
        assertTrue("Should allow DELETE", methods.contains(HttpDelete.METHOD_NAME));
    }

    private String createLDPRSMementoWithExistingBody(final String mementoDateTime) throws Exception {
        return createMementoWithExistingBody(id, mementoDateTime, false);
    }

    private String createLDPNRMementoWithExistingBody(final String mementoDateTime) throws Exception {
        return createMementoWithExistingBody(id, mementoDateTime, true);
    }

    private String createMementoWithExistingBody(final String id, final String mementoDateTime,
            final boolean isLDPNR) throws Exception {
        final HttpGet getRequest = getObjMethod(id);
        if (!isLDPNR) {
            getRequest.setHeader(ACCEPT, NTRIPLES);
        }
        try (final CloseableHttpResponse response = execute(getRequest)) {
            if (getStatus(response) == OK.getStatusCode()) {
                // Resource exists so get the body to put back with header
                final String body = EntityUtils.toString(response.getEntity());
                final String mimeType = response.getFirstHeader(CONTENT_TYPE).getValue();
                return createMemento(serverAddress + id, mementoDateTime, mimeType, body);
            }
        }
        return null;
    }

    private String createMemento(final String subjectUri, final String mementoDateTime, final String contentType,
            final String body) throws Exception {
        final HttpPost createVersionMethod = new HttpPost(subjectUri + "/" + FCR_VERSIONS);
        if (contentType != null) {
            createVersionMethod.addHeader(CONTENT_TYPE, contentType);
        }
        if (body != null) {
            createVersionMethod.setEntity(new StringEntity(body));
        }
        if (mementoDateTime != null && !mementoDateTime.isEmpty()) {
            createVersionMethod.addHeader(MEMENTO_DATETIME_HEADER, mementoDateTime);
        }

        // Create new memento of resource with updated body
        try (final CloseableHttpResponse response = execute(createVersionMethod)) {
            assertEquals("Didn't get a CREATED response!", CREATED.getStatusCode(), getStatus(response));
            assertMementoDatetimeHeaderPresent(response);

            return response.getFirstHeader(LOCATION).getValue();
        }
    }

    private String createContainerMementoWithBody(final String subjectUri, final String mementoDateTime)
            throws Exception {

        final String body = createContainerMementoBodyContent(subjectUri, N3);
        return createMemento(subjectUri, mementoDateTime, N3, body);
    }

    private String createContainerMementoBodyContent(final String subjectUri, final String contentType)
            throws Exception {
        // Produce new body from current body with changed triple
        final String body;
        final HttpGet httpGet = new HttpGet(subjectUri);
        final Model model = createDefaultModel();
        final Lang rdfLang = RDFLanguages.contentTypeToLang(contentType);
        try (final CloseableHttpResponse response = execute(httpGet)) {
            model.read(response.getEntity().getContent(), "", rdfLang.getName());
        }
        final String resourceUri = subjectUri.replace("/fcr:metadata", "");
        final Resource subjectResc = model.getResource(resourceUri);
        subjectResc.removeAll(TEST_PROPERTY);
        subjectResc.addLiteral(TEST_PROPERTY, "bar");

        try (StringWriter stringOut = new StringWriter()) {
            RDFDataMgr.write(stringOut, model, RDFFormat.NTRIPLES);
            body = stringOut.toString();
        }

        return body;
    }

    /**
     * Create a versioned LDP-RS
     *
     * @param id the desired slug
     * @return Location of the new resource
     * @throws Exception
     */
    private String createVersionedContainer(final String id) throws Exception {
        final HttpPost createMethod = postObjMethod();
        createMethod.addHeader("Slug", id);
        createMethod.addHeader(CONTENT_TYPE, N3);
        createMethod.addHeader(LINK, VERSIONED_RESOURCE_LINK_HEADER);
        createMethod.setEntity(new StringEntity("<> <info:test#label> \"foo\""));

        try (final CloseableHttpResponse response = execute(createMethod)) {
            assertEquals("Didn't get a CREATED response!", CREATED.getStatusCode(), getStatus(response));
            return response.getFirstHeader(LOCATION).getValue();
        }
    }

    /**
     * Create a versioned LDP-NR.
     *
     * @param id the desired slug
     * @param mimeType the mimeType of the content
     * @param content the actual content
     * @return Location of the new resource
     * @throws Exception
     */
    private String createVersionedBinary(final String id, final String mimeType, final String content)
            throws Exception {
        final HttpPost createMethod = postObjMethod();
        createMethod.addHeader("Slug", id);
        if (mimeType == null && content == null) {
            createMethod.addHeader(CONTENT_TYPE, OCTET_STREAM_TYPE);
            createMethod.setEntity(new StringEntity(BINARY_CONTENT));
        } else {
            createMethod.addHeader(CONTENT_TYPE, mimeType);
            createMethod.setEntity(new StringEntity(content));
        }
        createMethod.addHeader(LINK, VERSIONED_RESOURCE_LINK_HEADER);

        try (final CloseableHttpResponse response = execute(createMethod)) {
            assertEquals("Didn't get a CREATED response!", CREATED.getStatusCode(), getStatus(response));
            logger.info("created object: {}", response.getFirstHeader(LOCATION).getValue());
            return response.getFirstHeader(LOCATION).getValue();
        }
    }

    /**
     * Create a versioned LDP-NR.
     *
     * @param id the desired slug
     * @return Location of the new resource
     * @throws Exception
     */
    private String createVersionedBinary(final String id) throws Exception {
        return createVersionedBinary(id, null, null);
    }

    private static void assertNoMementoDatetimeHeaderPresent(final CloseableHttpResponse response) {
        assertNull("No memento datetime header set in response", response.getFirstHeader(MEMENTO_DATETIME_HEADER));
    }

    private static void assertMementoDatetimeHeaderPresent(final CloseableHttpResponse response) {
        assertNotNull("No memento datetime header set in response",
                response.getFirstHeader(MEMENTO_DATETIME_HEADER));
    }

    private static void assertMementoDatetimeHeaderMatches(final CloseableHttpResponse response,
            final String expected) {
        assertMementoDatetimeHeaderPresent(response);
        assertEquals("Response memento datetime did not match expected value", expected,
                response.getFirstHeader(MEMENTO_DATETIME_HEADER).getValue());
    }

    private static void enableVersioning(final String uri) throws Exception {
        final HttpPut enableMethod = new HttpPut(uri);
        enableMethod.addHeader(CONTENT_TYPE, N3);
        enableMethod.addHeader(LINK, VERSIONED_RESOURCE_LINK_HEADER);
        // Resubmit the existing state of the resource
        try (final CloseableHttpResponse response = execute(new HttpGet(uri))) {
            final String body = IOUtils.toString(response.getEntity().getContent(), "UTF-8");
            enableMethod.setEntity(new StringEntity(body));
        }

        try (final CloseableHttpResponse response = execute(enableMethod)) {
            assertEquals("Didn't get a CREATED response!", NO_CONTENT.getStatusCode(), getStatus(response));
        }
    }

    private static void patchLiteralProperty(final String url, final String predicate, final String literal)
            throws IOException {
        final HttpPatch updateObjectGraphMethod = new HttpPatch(url);
        updateObjectGraphMethod.addHeader(CONTENT_TYPE, "application/sparql-update");
        updateObjectGraphMethod
                .setEntity(new StringEntity("INSERT DATA { <> <" + predicate + "> \"" + literal + "\" } "));
        assertEquals(NO_CONTENT.getStatusCode(), getStatus(updateObjectGraphMethod));
    }

    private CloseableDataset getContent(final String url) throws IOException {
        final HttpGet getVersion = new HttpGet(url);
        getVersion.addHeader("Prefer", "return=representation; include=\"" + EMBED_CONTAINED.toString() + "\"");
        return getDataset(getVersion);
    }

    private String[] getTimeMapResponseTypes() {
        rdfTypes.add(APPLICATION_LINK_FORMAT);
        return rdfTypes.toArray(new String[rdfTypes.size()]);
    }

    protected static void assertMementoUri(final String mementoUri, final String subjectUri) {
        assertTrue(mementoUri.matches(subjectUri + "/fcr:versions/\\d+"));
    }

    private static void assertHasLink(final CloseableHttpResponse response, final Property relation,
            final String uri) {
        final String relName = relation.getLocalName();
        assertTrue("Missing link " + relName + " with value " + uri, getLinkHeaders(response).stream()
                .map(Link::valueOf).anyMatch(l -> relName.equals(l.getRel()) && uri.equals(l.getUri().toString())));
    }
}