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

Java tutorial

Introduction

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

Source

/**
 * Copyright 2014 DuraSpace, Inc.
 *
 * Licensed 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 com.google.common.base.Predicate;
import com.google.common.collect.Iterators;
import com.hp.hpl.jena.graph.Node;
import com.hp.hpl.jena.rdf.model.ResourceFactory;
import com.hp.hpl.jena.update.GraphStore;
import org.apache.commons.io.IOUtils;
import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
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.BasicHttpEntity;
import org.junit.Ignore;
import org.junit.Test;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Arrays;

import static com.hp.hpl.jena.graph.Node.ANY;
import static com.hp.hpl.jena.graph.NodeFactory.createURI;
import static com.hp.hpl.jena.rdf.model.ResourceFactory.createPlainLiteral;
import static com.hp.hpl.jena.rdf.model.ResourceFactory.createResource;
import static java.lang.Math.min;
import static java.lang.Thread.sleep;
import static java.util.UUID.randomUUID;
import static java.util.regex.Pattern.compile;
import static javax.ws.rs.core.Response.Status.NO_CONTENT;
import static org.fcrepo.kernel.RdfLexicon.DC_TITLE;
import static org.jgroups.util.Util.assertFalse;
import static org.fcrepo.kernel.TransactionImpl.DEFAULT_TIMEOUT;
import static org.fcrepo.kernel.TransactionImpl.TIMEOUT_SYSTEM_PROPERTY;
import static org.fcrepo.kernel.services.TransactionServiceImpl.REAP_INTERVAL;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;

public class FedoraTransactionsIT extends AbstractResourceIT {

    @Test
    public void testCreateTransaction() throws Exception {
        final String location = createTransaction();

        logger.info("Got location {}", location);
        assertTrue("Expected Location header to send us to root node path within the transaction",
                compile("tx:[0-9a-f-]+$").matcher(location).find());

    }

    @Test
    public void testRequestsInTransactionThatDoestExist() throws Exception {
        /* create a tx */
        final HttpPost createTx = new HttpPost(serverAddress + "tx:123/objects");
        final HttpResponse response = execute(createTx);
        assertEquals(410, response.getStatusLine().getStatusCode());

    }

    @Test
    public void testCreateAndTimeoutTransaction() throws Exception {

        /* create a short-lived tx */
        final long testTimeout = min(500, REAP_INTERVAL / 2);
        System.setProperty(TIMEOUT_SYSTEM_PROPERTY, Long.toString(testTimeout));

        /* create a tx */
        final String location = createTransaction();

        final HttpGet getWithinTx = new HttpGet(location);
        HttpResponse resp = execute(getWithinTx);
        IOUtils.toString(resp.getEntity().getContent());
        assertEquals(200, resp.getStatusLine().getStatusCode());
        assertTrue(Iterators.any(Iterators.forArray(resp.getHeaders("Link")), new Predicate<Header>() {
            @Override
            public boolean apply(Header input) {
                return input.getValue().contains("<" + serverAddress + ">;rel=\"canonical\"");
            }
        }));

        int statusCode = 0;

        sleep(REAP_INTERVAL * 2);
        final HttpGet getWithExpiredTx = new HttpGet(location);
        resp = execute(getWithExpiredTx);
        IOUtils.toString(resp.getEntity().getContent());
        statusCode = resp.getStatusLine().getStatusCode();

        try {
            assertEquals("Transaction did not expire", 410, statusCode);
        } finally {
            System.setProperty(TIMEOUT_SYSTEM_PROPERTY, Long.toString(DEFAULT_TIMEOUT));
            System.clearProperty("fcrepo4.tx.timeout");
        }
    }

    @Test
    public void testCreateDoStuffAndRollbackTransaction() throws Exception {
        /* create a tx */
        final HttpPost createTx = new HttpPost(serverAddress + "fcr:tx");

        final HttpResponse response = execute(createTx);
        assertEquals(201, response.getStatusLine().getStatusCode());

        final String txLocation = response.getFirstHeader("Location").getValue();

        /* create a new object inside the tx */
        final HttpPost postNew = new HttpPost(txLocation);
        final String pid = getRandomUniquePid();
        postNew.addHeader("Slug", pid);
        HttpResponse resp = execute(postNew);
        assertEquals(201, resp.getStatusLine().getStatusCode());

        /* fetch the created tx from the endpoint */
        final HttpGet getTx = new HttpGet(txLocation + "/" + pid);
        final GraphStore graphStore = getGraphStore(getTx);
        logger.debug(graphStore.toString());

        assertTrue(graphStore.toDataset().asDatasetGraph().contains(Node.ANY, createURI(txLocation + "/" + pid),
                ANY, ANY));

        /* fetch the created tx from the endpoint */
        final HttpGet getObj = new HttpGet(serverAddress + "/" + pid);
        resp = execute(getObj);
        assertEquals("Expected to not find our object within the scope of the transaction", 404,
                resp.getStatusLine().getStatusCode());

        /* and rollback */
        final HttpPost rollbackTx = new HttpPost(txLocation + "/fcr:tx/fcr:rollback");
        resp = execute(rollbackTx);

        assertEquals(204, resp.getStatusLine().getStatusCode());

    }

    @Test
    public void testCreateDoStuffAndCommitTransaction() throws Exception {
        /* create a tx */
        final String txLocation = createTransaction();

        /* create a new object inside the tx */
        final String objectInTxCommit = randomUUID().toString();
        final HttpPost postNew = new HttpPost(txLocation);
        postNew.addHeader("Slug", objectInTxCommit);
        HttpResponse resp = execute(postNew);
        assertEquals(201, resp.getStatusLine().getStatusCode());

        /* fetch the created tx from the endpoint */
        final HttpGet getTx = new HttpGet(txLocation + "/" + objectInTxCommit);
        GraphStore graphStore = getGraphStore(getTx);

        logger.debug(graphStore.toString());

        assertTrue(graphStore.toDataset().asDatasetGraph().contains(ANY,
                createURI(txLocation + "/" + objectInTxCommit), ANY, ANY));

        /* fetch the object-in-tx outside of the tx */
        final HttpGet getObj = new HttpGet(serverAddress + objectInTxCommit);
        resp = execute(getObj);
        assertEquals("Expected to not find our object within the scope of the transaction", 404,
                resp.getStatusLine().getStatusCode());

        /* and commit */
        final HttpPost commitTx = new HttpPost(txLocation + "/fcr:tx/fcr:commit");
        resp = execute(commitTx);

        assertEquals(204, resp.getStatusLine().getStatusCode());

        /* fetch the object-in-tx outside of the tx after it has been committed */
        final HttpGet getObjCommitted = new HttpGet(serverAddress + objectInTxCommit);
        graphStore = getGraphStore(getObjCommitted);

        assertTrue("Expected to  find our object after the transaction was committed", graphStore.toDataset()
                .asDatasetGraph().contains(ANY, createURI(serverAddress + objectInTxCommit), ANY, ANY));

    }

    @Test
    public void testCreateDoStuffAndCommitTransactionSeparateConnections() throws Exception {
        /* create a tx */
        final String txLocation = createTransaction();

        /* create a new object inside the tx */
        final String objectInTxCommit = randomUUID().toString();
        client = createClient();
        final HttpPost postNew = new HttpPost(txLocation);
        postNew.addHeader("Slug", objectInTxCommit);
        HttpResponse resp = execute(postNew);
        assertEquals(201, resp.getStatusLine().getStatusCode());

        /* fetch the created tx from the endpoint */
        client = createClient();
        final HttpGet getTx = new HttpGet(txLocation + "/" + objectInTxCommit);
        GraphStore graphStore = getGraphStore(getTx);

        logger.debug(graphStore.toString());

        assertTrue(graphStore.toDataset().asDatasetGraph().contains(ANY,
                createURI(txLocation + "/" + objectInTxCommit), ANY, ANY));

        /* fetch the object-in-tx outside of the tx */
        client = createClient();
        final HttpGet getObj = new HttpGet(serverAddress + objectInTxCommit);
        resp = execute(getObj);
        assertEquals("Expected to not find our object within the scope of the transaction", 404,
                resp.getStatusLine().getStatusCode());

        /* and commit */
        client = createClient();
        final HttpPost commitTx = new HttpPost(txLocation + "/fcr:tx/fcr:commit");
        resp = execute(commitTx);

        assertEquals(204, resp.getStatusLine().getStatusCode());

        /* fetch the object-in-tx outside of the tx after it has been committed */
        client = createClient();
        final HttpGet getObjCommitted = new HttpGet(serverAddress + objectInTxCommit);
        graphStore = getGraphStore(getObjCommitted);

        assertTrue("Expected to  find our object after the transaction was committed", graphStore.toDataset()
                .asDatasetGraph().contains(ANY, createURI(serverAddress + objectInTxCommit), ANY, ANY));

    }

    /**
     * Tests whether a Sparql update is visible within a transaction
     * and if the update is made persistent along with the commit.
     * @throws Exception
     */
    @Test
    public void testIngestNewWithSparqlPatchWithinTransaction() throws Exception {
        final String objectInTxCommit = randomUUID().toString();

        /* create new tx */
        final String txLocation = createTransaction();

        client = createClient();
        final HttpPost postNew = new HttpPost(txLocation);
        postNew.addHeader("Slug", objectInTxCommit);
        HttpResponse resp = execute(postNew);
        assertEquals(201, resp.getStatusLine().getStatusCode());
        final String newObjectLocation = resp.getFirstHeader("Location").getValue();

        /* update sparql */
        final HttpPatch method = new HttpPatch(newObjectLocation);
        method.addHeader("Content-Type", "application/sparql-update");
        final BasicHttpEntity entity = new BasicHttpEntity();
        final String title = "this is a new title";
        entity.setContent(new ByteArrayInputStream(
                ("INSERT { <> <http://purl.org/dc/elements/1.1/title> \"" + title + "\" } WHERE {}").getBytes()));
        method.setEntity(entity);
        final HttpResponse responseFromPatch = client.execute(method);
        final int status = responseFromPatch.getStatusLine().getStatusCode();
        assertEquals("Didn't get a 204 status! Got status:\n" + status, NO_CONTENT.getStatusCode(), status);

        /* make sure the change was made within the tx */
        final HttpGet httpGet = new HttpGet(newObjectLocation);
        final GraphStore graphStore = getGraphStore(httpGet);
        assertTrue("The sparql update did not succeed within a transaction", graphStore.contains(ANY,
                createResource(newObjectLocation).asNode(), DC_TITLE.asNode(), createPlainLiteral(title).asNode()));

        /* commit */
        client = createClient();
        final HttpPost commitTx = new HttpPost(txLocation + "/fcr:tx/fcr:commit");
        resp = execute(commitTx);

        assertEquals(204, resp.getStatusLine().getStatusCode());

        /* it must exist after commit */
        client = createClient();
        final HttpGet getObjCommitted = new HttpGet(serverAddress + objectInTxCommit);
        final GraphStore graphStoreAfterCommit = getGraphStore(getObjCommitted);
        assertTrue("The inserted triple does not exist after the transaction has committed",
                graphStoreAfterCommit.contains(ANY, ANY, DC_TITLE.asNode(), createPlainLiteral(title).asNode()));

    }

    @Test
    public void testGetNonExistingObject() throws Exception {
        final String txLocation = createTransaction();
        final String newObjectLocation = txLocation + "/idontexist";
        final HttpGet httpGet = new HttpGet(newObjectLocation);

        client = createClient();
        final HttpResponse responseFromGet = client.execute(httpGet);
        final int status = responseFromGet.getStatusLine().getStatusCode();
        assertEquals("Status should be 404", 404, status);
    }

    /**
     * Tests that transactions cannot be hijacked
     */
    @Test
    public void testTransactionHijackingNotPossible() throws Exception {

        /* "fedoraAdmin" creates a transaction */
        final HttpPost createTx = new HttpPost(serverAddress + "fcr:tx");
        final HttpResponse response = executeWithBasicAuth(createTx, "fedoraAdmin", "fedoraAdmin");
        assertEquals("Status should be 201 after creating a transaction with user fedoraAdmin", 201,
                response.getStatusLine().getStatusCode());
        final String txLocation = response.getFirstHeader("Location").getValue();

        /* "fedoraUser" puts to "fedoraAdmin"'s transaction and fails */
        final HttpPut putFedoraUser = new HttpPut(txLocation);
        final HttpResponse responseFedoraUser = executeWithBasicAuth(putFedoraUser, "fedoraUser", "fedoraUser");
        assertEquals("Status should be 410 because putting on a transaction of a different user is not allowed",
                410, responseFedoraUser.getStatusLine().getStatusCode());

        /* anonymous user puts to "fedoraAdmin"'s transaction and fails */
        final HttpPut putTxAnon = new HttpPut(txLocation);
        final HttpResponse responseTxAnon = execute(putTxAnon);
        assertEquals("Status should be 410 because putting on a transaction of a different user is not allowed",
                410, responseTxAnon.getStatusLine().getStatusCode());

        /* transaction is still intact and "fedoraAdmin" - the owner - can successfully put to it */
        final String objectInTxCommit = randomUUID().toString();
        final HttpPut putToExistingTx = new HttpPut(txLocation + "/" + objectInTxCommit);
        final HttpResponse responseFromPutToTx = executeWithBasicAuth(putToExistingTx, "fedoraAdmin",
                "fedoraAdmin");
        assertEquals("Status should be 201 after putting", 201,
                responseFromPutToTx.getStatusLine().getStatusCode());

    }

    /**
     * Tests that transactions cannot be hijacked,
     * even if created by an anonymous user
     */
    @Test
    public void testTransactionHijackingNotPossibleAnoymous() throws Exception {

        /* anonymous user creates a transaction */
        final String txLocation = createTransaction();

        /* fedoraAdmin attempts to puts to anonymous transaction and fails */
        final HttpPut putFedoraAdmin = new HttpPut(txLocation);
        final HttpResponse responseFedoraAdmin = executeWithBasicAuth(putFedoraAdmin, "fedoraAdmin", "fedoraAdmin");
        assertEquals("Status should be 410 because putting on a transaction of a different user is not permitted",
                410, responseFedoraAdmin.getStatusLine().getStatusCode());

        /* fedoraUser attempts to put to anonymous transaction and fails */
        final HttpPut putFedoraUser = new HttpPut(txLocation);
        final HttpResponse responseFedoraUser = executeWithBasicAuth(putFedoraUser, "fedoraUser", "fedoraUser");
        assertEquals("Status should be 410 because putting on a transaction of a different user is not permitted",
                410, responseFedoraUser.getStatusLine().getStatusCode());

        /* transaction is still intact and any anonymous user can successfully put to it */
        final String objectInTxCommit = randomUUID().toString();
        final HttpPut putToExistingTx = new HttpPut(txLocation + "/" + objectInTxCommit);
        final HttpResponse responseFromPutToTx = execute(putToExistingTx);
        assertEquals("Status should be 201 after putting", 201,
                responseFromPutToTx.getStatusLine().getStatusCode());

    }

    /**
     * Tests that transactions are treated as atomic with regards to nodes.
     *
     * A common use case for applications written against fedora is that an
     * operation checks some property of a fedora object and acts on it
     * accordingly.  In order for this to work in a multi-client or
     * multi-threaded environment that comparison+action combination needs to
     * be atomic.
     *
     * Imagine a scenario where we have one process that deletes all objects
     * in the repository that don't have a "preserve" property set to the
     * literal "true", and we have any number of other clients that add such
     * a property.
     *
     * We want to ensure that there is no way for a client to successfully
     * add this property between when the "deleter" process has determined that
     * no such property exists and when it deletes the object.
     *
     * In other words, if there are only clients adding properties and the
     * "deleter" deleting objects it should not be possible for an object
     * to be deleted if a client has added a title and received a successful
     * http response code.
     */
    @Test
    @Ignore("Until we implement some kind of record level locking.")
    public void testTransactionAndConcurrentConflictingUpdate() throws Exception {
        final String preserveProperty = "preserve";
        final String preserveValue = "true";

        /* create the object in question */
        final String objPid = randomUUID().toString();
        createObject(objPid);

        /* create the deleter transaction */
        final String deleterTxLocation = createTransaction();
        final String deleterTxId = deleterTxLocation.substring(serverAddress.length());

        /* assert that the object is eligible for delete in the transaction */
        verifyProperty("No preserve property should be set!", objPid, deleterTxId, preserveProperty, preserveValue,
                false);

        /* delete that object in the transaction */
        final HttpDelete delete = new HttpDelete(deleterTxLocation + "/" + objPid);
        assertEquals(204, execute(delete).getStatusLine().getStatusCode());

        /* fetch the object-deleted-in-tx outside of the tx */
        final HttpGet getObj = new HttpGet(serverAddress + objPid);
        HttpResponse resp = execute(getObj);
        assertEquals(
                "Expected to find our object outside the scope of the tx,"
                        + " despite it being deleted in the uncommitted transaction.",
                200, resp.getStatusLine().getStatusCode());

        /* mark the object as not deletable outside the context of the transaction */
        setProperty(objPid, preserveProperty, preserveValue);

        /* commit that transaction */
        final HttpPost commitDeleteTx = new HttpPost(deleterTxLocation + "/fcr:tx/fcr:commit");
        resp = execute(commitDeleteTx);
        assertNotEquals("Transaction is not atomic with regards to the object!", 204,
                resp.getStatusLine().getStatusCode());
    }

    private String createTransaction() throws IOException {
        final HttpPost createTx = new HttpPost(serverAddress + "fcr:tx");
        final HttpResponse response = execute(createTx);
        assertEquals(201, response.getStatusLine().getStatusCode());
        return response.getFirstHeader("Location").getValue();
    }

    private void verifyProperty(final String assertionMessage, final String pid, final String txId,
            final String propertyUri, final String propertyValue, final boolean shouldExist) throws IOException {
        client = createClient();
        final HttpGet getObjCommitted = new HttpGet(serverAddress + (txId != null ? txId + "/" : "") + pid);
        final GraphStore graphStore = getGraphStore(getObjCommitted);
        final boolean exists = graphStore.contains(ANY, createResource(serverAddress + pid).asNode(),
                ResourceFactory.createProperty(propertyUri).asNode(), createPlainLiteral(propertyValue).asNode());
        if (shouldExist) {
            assertTrue(assertionMessage, exists);
        } else {
            assertFalse(assertionMessage, exists);
        }
    }

}