org.fcrepo.integration.connector.file.FileConnectorIT.java Source code

Java tutorial

Introduction

Here is the source code for org.fcrepo.integration.connector.file.FileConnectorIT.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.connector.file;

import static java.lang.Integer.MAX_VALUE;
import static java.lang.Integer.parseInt;
import static java.lang.System.currentTimeMillis;
import static java.lang.Thread.sleep;
import static java.util.UUID.randomUUID;
import static javax.ws.rs.core.MediaType.TEXT_PLAIN;
import static javax.ws.rs.core.Response.Status.CREATED;
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.createURI;
import static org.apache.jena.vocabulary.RDF.type;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.fcrepo.kernel.modeshape.RdfJcrLexicon.MODE_NAMESPACE;
import static org.fcrepo.http.commons.test.util.TestHelpers.parseTriples;

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Locale;

import org.apache.http.HttpResponse;
import org.apache.http.annotation.NotThreadSafe;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpHead;
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.client.methods.HttpRequestBase;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
import org.apache.jena.sparql.core.DatasetGraph;
import org.apache.jena.graph.Node;

import org.fcrepo.http.commons.test.util.CloseableDataset;

import org.junit.Ignore;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Tests around using the fcrepo-connector-file
 *
 * @author awoods
 * @author ajs6f
 */
public class FileConnectorIT {

    private static final Logger logger = LoggerFactory.getLogger(FileConnectorIT.class);

    private static final int SERVER_PORT = parseInt(System.getProperty("fcrepo.dynamic.test.port", "8080"));

    private static final String HOSTNAME = "localhost";

    private static final String PROTOCOL = "http";

    private static final String serverAddress = PROTOCOL + "://" + HOSTNAME + ":" + SERVER_PORT + "/fcrepo/rest/";

    private static CloseableHttpClient client = createClient();

    private static CloseableHttpClient createClient() {
        return HttpClientBuilder.create().setMaxConnPerRoute(MAX_VALUE).setMaxConnTotal(MAX_VALUE).build();
    }

    private static SimpleDateFormat headerFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US);

    /**
     * I should be able to link to content on a federated filesystem.
     *
     * @throws IOException thrown during this function
     **/
    @Test
    public void testFederatedDatastream() throws IOException {
        final String federationAddress = serverAddress + "files/FileSystem1/ds1";
        final String linkingAddress;
        try (final CloseableHttpResponse response = client.execute(new HttpPost(serverAddress))) {
            EntityUtils.consume(response.getEntity());
            linkingAddress = response.getFirstHeader("Location").getValue();
        }

        // link from the object to the content of the file on the federated filesystem
        final HttpPatch patch = new HttpPatch(linkingAddress);
        patch.addHeader("Content-Type", "application/sparql-update");
        patch.setEntity(new ByteArrayEntity(("INSERT DATA { <> <http://some-vocabulary#hasExternalContent> " + "<"
                + federationAddress + "> . }").getBytes()));
        assertEquals("Couldn't link to external datastream!", NO_CONTENT.getStatusCode(), getStatus(patch));
    }

    /**
     * Given a directory at: test-FileSystem1/ /ds1 /ds2 /TestSubdir/ and a projection of test-objects as
     * fedora:/files, then I should be able to retrieve an object from fedora:/files/FileSystem1 that lists a child
     * object at fedora:/files/FileSystem1/TestSubdir and lists datastreams ds and ds2
     *
     * @throws IOException thrown during this function
     */
    @Test
    public void testGetProjectedNode() throws IOException {
        final HttpGet method = new HttpGet(serverAddress + "files/FileSystem1");
        try (final CloseableDataset dataset = getDataset(client, method)) {
            final DatasetGraph graph = dataset.asDatasetGraph();
            final Node subjectURI = createURI(serverAddress + "files/FileSystem1");
            assertTrue("Didn't find the first datastream! ",
                    graph.contains(ANY, subjectURI, ANY, createURI(subjectURI + "/ds1")));
            assertTrue("Didn't find the second datastream! ",
                    graph.contains(ANY, subjectURI, ANY, createURI(subjectURI + "/ds2")));
            assertTrue("Didn't find the first object! ",
                    graph.contains(ANY, subjectURI, ANY, createURI(subjectURI + "/TestSubdir")));
        }
    }

    /**
     * When I make changes to a resource in a federated filesystem, the parent folder's Last-Modified header should be
     * updated.
     *
     * @throws IOException thrown during this function
     **/
    @Test
    public void testLastModifiedUpdatedAfterUpdates() throws IOException {
        // create directory containing a file in filesystem
        final File fed = new File("target/test-classes/test-objects");
        final String id = randomUUID().toString();
        final File dir = new File(fed, id);
        final File child = new File(dir, "child");
        final long timestamp1 = currentTimeMillis();
        dir.mkdir();
        child.mkdir();
        // TODO this seems really brittle
        try {
            sleep(2000);
        } catch (final InterruptedException e) {
        }

        // check Last-Modified header is current
        final long lastmod1;
        try (final CloseableHttpResponse resp1 = client.execute(new HttpHead(serverAddress + "files/" + id))) {
            assertEquals(OK.getStatusCode(), getStatus(resp1));
            lastmod1 = headerFormat.parse(resp1.getFirstHeader("Last-Modified").getValue()).getTime();
            assertTrue((timestamp1 - lastmod1) < 1000); // because rounding

            // remove the file and wait for the TTL to expire
            final long timestamp2 = currentTimeMillis();
            child.delete();
            try {
                sleep(2000);
            } catch (final InterruptedException e) {
            }

            // check Last-Modified header is updated
            try (final CloseableHttpResponse resp2 = client.execute(new HttpHead(serverAddress + "files/" + id))) {
                assertEquals(OK.getStatusCode(), getStatus(resp2));
                final long lastmod2 = headerFormat.parse(resp2.getFirstHeader("Last-Modified").getValue())
                        .getTime();
                assertTrue((timestamp2 - lastmod2) < 1000); // because rounding
                assertFalse("Last-Modified headers should have changed", lastmod1 == lastmod2);
            } catch (final ParseException e) {
                fail();
            }
        } catch (final ParseException e) {
            fail();
        }
    }

    /**
     * I should be able to copy objects from a federated filesystem to the repository.
     **/
    @Test
    public void testCopyFromProjection() {
        final String destination = serverAddress + "copy-" + randomUUID().toString() + "-ds1";
        final String source = serverAddress + "files/FileSystem1/ds1";

        // ensure the source is present
        assertEquals(OK.getStatusCode(), getStatus(new HttpGet(source)));

        // copy to repository
        final HttpCopy request = new HttpCopy(source);
        request.addHeader("Destination", destination);
        assertEquals(CREATED.getStatusCode(), getStatus(request));

        // repository copy should now exist
        assertEquals(OK.getStatusCode(), getStatus(new HttpGet(destination)));
        assertEquals(OK.getStatusCode(), getStatus(new HttpGet(source)));
    }

    /**
     * I should be able to copy objects from the repository to a federated filesystem.
     *
     * @throws IOException exception thrown during this function
     **/
    @Ignore("Enabled once the FedoraFileSystemConnector becomes readable/writable")
    public void testCopyToProjection() throws IOException {
        // create object in the repository
        final String pid = randomUUID().toString();
        final HttpPut put = new HttpPut(serverAddress + pid + "/ds1");
        put.setEntity(new StringEntity("abc123"));
        put.setHeader("Content-Type", TEXT_PLAIN);
        client.execute(put);

        // copy to federated filesystem
        final HttpCopy request = new HttpCopy(serverAddress + pid);
        request.addHeader("Destination", serverAddress + "files/copy-" + pid);
        assertEquals(CREATED.getStatusCode(), getStatus(request));

        // federated copy should now exist
        final HttpGet copyGet = new HttpGet(serverAddress + "files/copy-" + pid);
        assertEquals(OK.getStatusCode(), getStatus(copyGet));

        // repository copy should still exist
        final HttpGet originalGet = new HttpGet(serverAddress + pid);
        assertEquals(OK.getStatusCode(), getStatus(originalGet));
    }

    @Test
    public void testGetRepositoryGraph() throws IOException {
        try (final CloseableDataset dataset = getDataset(client, new HttpGet(serverAddress))) {
            final DatasetGraph graph = dataset.asDatasetGraph();
            logger.trace("Retrieved repository graph:\n" + graph);
            assertFalse("Should not find the root type",
                    graph.contains(ANY, ANY, type.asNode(), createURI(MODE_NAMESPACE + "root")));
        }
    }

    /**
     * I should be able to create two subdirectories of a non-existent parent directory.
     *
     * @throws IOException thrown during this function
     **/
    @Ignore("Enabled once the FedoraFileSystemConnector becomes readable/writable")
    // TODO
    public void testBreakFederation() throws IOException {
        final String id = randomUUID().toString();
        testGetRepositoryGraph();
        createObjectAndClose("files/a0/" + id + "b0");
        createObjectAndClose("files/a0/" + id + "b1");
        testGetRepositoryGraph();
    }

    /**
     * I should be able to upload a file to a read/write federated filesystem.
     *
     * @throws IOException thrown during this function
     **/
    @Ignore("Enabled once the FedoraFileSystemConnector becomes readable/writable")
    // TODO
    public void testUploadToProjection() throws IOException {
        // upload file to federated filesystem using rest api
        final String id = randomUUID().toString();
        final String uploadLocation = serverAddress + "files/" + id + "/ds1";
        final String uploadContent = "abc123";
        logger.debug("Uploading to federated filesystem via rest api: " + uploadLocation);
        // final HttpResponse response = createDatastream("files/" + pid, "ds1", uploadContent);
        // final String actualLocation = response.getFirstHeader("Location").getValue();
        // assertEquals("Wrong URI in Location header", uploadLocation, actualLocation);

        // validate content
        try (final CloseableHttpResponse getResponse = client.execute(new HttpGet(uploadLocation))) {
            final String actualContent = EntityUtils.toString(getResponse.getEntity());
            assertEquals(OK.getStatusCode(), getResponse.getStatusLine().getStatusCode());
            assertEquals("Content doesn't match", actualContent, uploadContent);
        }
        // validate object profile
        try (final CloseableHttpResponse objResponse = client.execute(new HttpGet(serverAddress + "files/" + id))) {
            assertEquals(OK.getStatusCode(), objResponse.getStatusLine().getStatusCode());
        }
    }

    /* Many of the following private methods are copied directly from
     * org.fcrepo.integration.http.api.AbstractResourceIT.java. The reason we're not
     * simply inheriting from that class and re-using these methods has to do with the
     * complexities of building a working spring-based container along with the assumptions of
     * certain dependency chains. Basically, it didn't work, and this is a compromise
     * to simply make this integration test work. */

    /**
     * Executes an HTTP request and returns the status code of the response, closing the response.
     *
     * @param req the request to execute
     * @return the HTTP status code of the response
     */
    private static int getStatus(final HttpUriRequest req) {
        try (final CloseableHttpResponse response = client.execute(req)) {
            final int result = getStatus(response);
            if (!(result > 199) || !(result < 400)) {
                logger.warn("Got status {}", result);
                if (response.getEntity() != null) {
                    logger.trace(EntityUtils.toString(response.getEntity()));
                }
            }
            EntityUtils.consume(response.getEntity());
            return result;
        } catch (final IOException e) {
            throw new RuntimeException(e);
        }
    }

    private static int getStatus(final HttpResponse response) {
        return response.getStatusLine().getStatusCode();
    }

    private CloseableHttpResponse createObject(final String pid) {
        final HttpPost httpPost = new HttpPost(serverAddress + "/");
        if (pid.length() > 0) {
            httpPost.addHeader("Slug", pid);
        }
        try {
            final CloseableHttpResponse response = client.execute(httpPost);
            assertEquals(CREATED.getStatusCode(), getStatus(response));
            return response;
        } catch (final IOException e) {
            throw new RuntimeException(e);
        }
    }

    private void createObjectAndClose(final String pid) {
        try {
            createObject(pid).close();
        } catch (final IOException e) {
            throw new RuntimeException(e);
        }
    }

    private CloseableDataset getDataset(final CloseableHttpClient client, final HttpUriRequest req)
            throws IOException {
        if (!req.containsHeader("Accept")) {
            req.addHeader("Accept", "application/n-triples");
        }
        logger.debug("Retrieving RDF using mimeType: {}", req.getFirstHeader("Accept"));

        try (final CloseableHttpResponse response = client.execute(req)) {
            assertEquals(OK.getStatusCode(), response.getStatusLine().getStatusCode());
            final CloseableDataset result = parseTriples(response.getEntity());
            logger.trace("Retrieved RDF: {}", result);
            return result;
        }
    }

    @NotThreadSafe // HttpRequestBase is @NotThreadSafe
    private class HttpCopy extends HttpRequestBase {

        /**
         * @throws IllegalArgumentException if the uri is invalid.
         */
        public HttpCopy(final String uri) {
            super();
            setURI(URI.create(uri));
        }

        @Override
        public String getMethod() {
            return "COPY";
        }
    }
}