Package org.fcrepo.server.journal

Source Code of org.fcrepo.server.journal.TestJournalRoundTrip

/* The contents of this file are subject to the license and copyright terms
* detailed in the license directory at the root of the source tree (also
* available online at http://fedora-commons.org/license/).
*/

package org.fcrepo.server.journal;

import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.fail;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import junit.framework.JUnit4TestAdapter;

import org.fcrepo.common.Constants;
import org.fcrepo.common.rdf.RDFName;
import org.fcrepo.server.Context;
import org.fcrepo.server.errors.InvalidStateException;
import org.fcrepo.server.errors.ModuleInitializationException;
import org.fcrepo.server.errors.ModuleShutdownException;
import org.fcrepo.server.errors.ServerException;
import org.fcrepo.server.journal.entry.JournalEntryContext;
import org.fcrepo.server.management.Management;
import org.fcrepo.server.management.MockManagementDelegate;
import org.fcrepo.server.management.MockManagementDelegate.Call;
import org.junit.Before;
import org.junit.Test;

/**
* Check every management method to be sure:
* <ul>
* <li>that we know whether it is journaled or not</li>
* <li>that we know what items are stored in the context for recovery</li>
* <li>that a journaled method is played back the same as it was recorded</li>
* <li>that a journaled method WILL NOT be accepted from an outside source by a
* JournalConsumer</li>
* <li>that a non-journaled method WILL be accepted from an outside source by a
* JournalConsumer</li>
* </ul>
*
* @author Jim Blake
*/
public class TestJournalRoundTrip {

    private static final String METHOD_GET_TEMP_STREAM = "getTempStream";

    private static final String THE_ROLE = "theRole";

    private static final String THE_SERVER_HASH = "theServerHash";

    public static junit.framework.Test suite() {
        return new JUnit4TestAdapter(TestJournalRoundTrip.class);
    }

    /**
     * Defines the names of the classes used for reading, writing, and logging.
     */
    private Map<String, String> journalParameters;

    /**
     * Context items that will be provided to the JournalCreator.
     */
    private JournalEntryContext leadingContext;

    /**
     * Attributes that we expect to see added to the {@link #leadingContext}
     * when the management method is called.
     */
    private Map<RDFName, String[]> contextAdditions;

    /**
     * Context items as expected after the call.
     */
    private JournalEntryContext expectedContext;

    /**
     * The journal module of the "leader" server.
     */
    private JournalCreator creator;

    /**
     * Records the calls that are submitted to the Management interface by the
     * JournalCreator.
     */
    private MockManagementDelegate leadingDelegate;

    /**
     * The journal module of the "follower" server.
     */
    private JournalConsumer consumer;

    /**
     * Records the calls that are submitted to the Management interface by the
     * JournalConsumer.
     */
    MockManagementDelegate followingDelegate;

    private Call expectedCall;

    /*
     * ------------------------------------------------------------------------
     * Setup
     * ------------------------------------------------------------------------
     */

    /**
     * For each test, set the parameters that will be required by the
     * {@link #creator} and the {@link #consumer}.
     */
    @Before
    public void initializeJournalParameters() {
        journalParameters = new HashMap<String, String>();
        journalParameters
                .put(JournalConstants.PARAMETER_JOURNAL_WRITER_CLASSNAME,
                     MockJournalWriter.class.getName());
        journalParameters
                .put(JournalConstants.PARAMETER_JOURNAL_READER_CLASSNAME,
                     MockJournalReader.class.getName());
        journalParameters
                .put(JournalConstants.PARAMETER_JOURNAL_RECOVERY_LOG_CLASSNAME,
                     MockJournalRecoveryLog.class.getName());
    }

    @Before
    public void initializeContextObjects() {
        leadingContext = new JournalEntryContext();
        expectedContext = null;
        contextAdditions = new HashMap<RDFName, String[]>();
    }

    @Before
    public void initializeExpectedCall() {
        // Every test should call buildExpectedCall().
        expectedCall = null;
    }

    /*
     * ------------------------------------------------------------------------
     * The tests
     * ------------------------------------------------------------------------
     */

    @Test
    public void addDatastream() throws ServerException {
        expectInContext(Constants.RECOVERY.DATASTREAM_ID, "theDsId");
        testJournaledMethod(JournalConstants.METHOD_ADD_DATASTREAM,
                            leadingContext,
                            "thePid",
                            "theDsId",
                            new String[0],
                            "theDsLabel",
                            false,
                            "theMIMEType",
                            "theFormatURI",
                            "theLocation",
                            "theControlGroup",
                            "theDsState",
                            "theChecksumType",
                            "theChecksum",
                            "theLogMessage");
    }

    @Test
    public void addRelationship() throws ServerException {
        testJournaledMethod(JournalConstants.METHOD_ADD_RELATIONSHIP,
                            leadingContext,
                            "theSubject",
                            "relationship",
                            "anObject",
                            false,
                            "");
    }

    @Test
    public void compareDatastreamChecksum() throws ServerException {
        testNonJournaledMethod("compareDatastreamChecksum",
                               leadingContext,
                               "thePid",
                               "theDsId",
                               new Date(12345L));
    }

    @Test
    public void export() throws ServerException {
        testNonJournaledMethod("export",
                               leadingContext,
                               "PID",
                               "format",
                               "SomeExportContext",
                               "encoding");
    }

    @Test
    public void getDatastream() throws ServerException {
        testNonJournaledMethod("getDatastream",
                               leadingContext,
                               "PID",
                               "aDatastreamID",
                               new Date());
    }

    @Test
    public void getDatastreamHistory() throws ServerException {
        testNonJournaledMethod("getDatastreamHistory",
                               leadingContext,
                               "PID",
                               "anotherDatastreamID");
    }

    @Test
    public void getDatastreams() throws ServerException {
        testNonJournaledMethod("getDatastreams",
                               leadingContext,
                               "sonOfPID",
                               new Date(111111L),
                               "someStateString");
    }

    @Test
    public void getNextPID() throws ServerException {
        expectInContext(Constants.RECOVERY.PID_LIST, new String[]{
                "sillyPID_0", "sillyPID_1", "sillyPID_2", "sillyPID_3",
                "sillyPID_4"});
        testJournaledMethod(JournalConstants.METHOD_GET_NEXT_PID,
                            leadingContext,
                            5,
                            "myFavoriteNamespace");
    }

    @Test
    public void getObjectXML() throws ServerException {
        testNonJournaledMethod("getObjectXML",
                               leadingContext,
                               "myPID",
                               "encodingScheme");
    }

    @Test
    public void getRelationships() throws ServerException {
        testNonJournaledMethod("getRelationships",
                               leadingContext,
                               "mySubject",
                               "someRelationship");
    }

    /**
     * This one will always be special, in that it doesn't use a context as its
     * first argument. If it were a Journaled method, that would be a problem.
     */
    @Test
    public void getTempStream() throws ServerException {
        testNonJournaledMethod(METHOD_GET_TEMP_STREAM, "streamID");
    }

    @Test
    public void ingest() throws ServerException {
        expectInContext(Constants.RECOVERY.PID, "Ingest:1");
        testJournaledMethod(JournalConstants.METHOD_INGEST,
                            leadingContext,
                            new ByteArrayInputStream(new byte[0]),
                            "theLogMessage",
                            "aFormat",
                            "someEncoding",
                            "new");
    }

    @Test
    public void modifyDatastreamByReference() throws ServerException {
        testJournaledMethod(JournalConstants.METHOD_MODIFY_DATASTREAM_BY_REFERENCE,
                            leadingContext,
                            "myPid",
                            "datastreamIdentifier",
                            new String[]{"altID"},
                            "datastreamLabel",
                            "mime/type",
                            "formatUri",
                            "dsLocation",
                            "checksumType",
                            "checksum",
                            "logMessage",
                            new Date(253370761200L)); // Jan 1 9999);
    }

    @Test
    public void modifyDatastreamByValue() throws ServerException {
        testJournaledMethod(JournalConstants.METHOD_MODIFY_DATASTREAM_BY_VALUE,
                            leadingContext,
                            "myPid",
                            "datastreamIdentifier",
                            new String[]{"altID"},
                            "datastreamLabel",
                            "mime/type",
                            "formatUri",
                            new ByteArrayInputStream(new byte[0]),
                            "checksumType",
                            "checksum",
                            "logMessage",
                            new Date(253370761200L)); // Jan 1 9999);
    }

    @Test
    public void modifyObject() throws ServerException {
        testJournaledMethod(JournalConstants.METHOD_MODIFY_OBJECT,
                            leadingContext,
                            "myPid",
                            "state",
                            "objectLabel",
                            "owner",
                            "logMessage",
                            new Date(253370761200L)); // Jan 1 9999);
    }

    @Test
    public void purgeDatastream() throws ServerException {
        testJournaledMethod(JournalConstants.METHOD_PURGE_DATASTREAM,
                            leadingContext,
                            "myPid",
                            "dsID",
                            new Date(123),
                            new Date(456),
                            "logMessage");
    }

    @Test
    public void purgeObject() throws ServerException {
        testJournaledMethod(JournalConstants.METHOD_PURGE_OBJECT,
                            leadingContext,
                            "aPID",
                            "PurgeLogMessage");
    }

    @Test
    public void purgeRelationship() throws ServerException {
        testJournaledMethod(JournalConstants.METHOD_PURGE_RELATIONSHIP,
                            leadingContext,
                            "aSubject",
                            "theRelationship",
                            "someObject",
                            false,
                            "datatype");
    }

    @Test
    public void putTempStream() throws ServerException {
        expectInContext(Constants.RECOVERY.UPLOAD_ID, "tempStreamId");
        testJournaledMethod(JournalConstants.METHOD_PUT_TEMP_STREAM,
                            leadingContext,
                            new ByteArrayInputStream(new byte[0]));
    }

    @Test
    public void setDatastreamState() throws ServerException {
        testJournaledMethod(JournalConstants.METHOD_SET_DATASTREAM_STATE,
                            leadingContext,
                            "pid",
                            "dsID",
                            "dsState",
                            "dsLogMessage");
    }

    @Test
    public void setDatastreamVersionable() throws ServerException {
        testJournaledMethod(JournalConstants.METHOD_SET_DATASTREAM_VERSIONABLE,
                            leadingContext,
                            "lastPID",
                            "lastDsID",
                            true,
                            "the Logger!");
    }

    /*
     * ------------------------------------------------------------------------
     * Helper methods.
     * ------------------------------------------------------------------------
     */

    /**
     * What additional "recovery attributes" should we expect the leading
     * delegate to set into the context?
     */
    private void expectInContext(RDFName key, String value) {
        contextAdditions.put(key, new String[]{value});
    }
    private void expectInContext(RDFName key, String[] value) {
        contextAdditions.put(key, value);
    }

    /**
     * <p>
     * Test a Journaled method.
     * </p>
     * <p>
     * Call the selected method on the JournalCreator. Then tell the
     * JournalConsumer to process the journal. Compare the calls received by
     * both Management delegates to the call we expected them to see.
     * </p>
     * <p>
     * Calling the method directly on the JournalConsumer should produce an
     * exception.
     * </p>
     *
     * @throws ModuleInitializationException
     * @throws ModuleShutdownException
     */
    private void testJournaledMethod(String methodName, Object... arguments)
            throws ServerException {
        buildExpectedCall(methodName, arguments);

        setupLeader();
        executeManagmentMethod(creator, methodName, arguments);
        closeLeader();

        setupFollower();
        letFollowerCatchUp();

        assertExpectedCall("leading", leadingDelegate);
        assertExpectedCall("following", followingDelegate);

        try {
            executeManagmentMethod(consumer, methodName, arguments);
            fail("expected an InvalidStateException");
        } catch (InvalidStateException e) {
            // That's the one we expected.
        }
    }

    /**
     * <p>
     * Test a non-Journaled method.
     * </p>
     * <p>
     * Call the selected method on the JournalCreator. Check that the Journal is
     * empty. Compare the call received by the Management delegates to the call
     * we expected it to see.
     * </p>
     * <p>
     * Call the selected method on the JournalConsumer. Again, compare the call
     * to the one that we expected.
     * </p>
     */
    private void testNonJournaledMethod(String methodName, Object... arguments)
            throws ServerException {
        buildExpectedCall(methodName, arguments);

        setupLeader();
        executeManagmentMethod(creator, methodName, arguments);
        closeLeader();

        assertEmptyJournal();
        assertExpectedCall("leading", leadingDelegate);

        setupFollower();
        letFollowerCatchUp();
        executeManagmentMethod(consumer, methodName, arguments);
        assertExpectedCall("following", followingDelegate);
    }

    /**
     * <p>
     * The expected call includes the expected context, as well as the supplied
     * arguments.
     * </p>
     * <p>
     * Note that "getTempStream" is the only Management call that doesn't use
     * "context" as its first argument.
     * </p>
     */
    private void buildExpectedCall(String methodName, Object[] arguments) {
        loadExpectedContext();
        if (arguments[0] == leadingContext) {
            arguments[0] = expectedContext;
        }
        expectedCall = new Call(methodName, arguments);
    }

    /**
     * The {@link #expectedContext} should be like the {@link #leadingContext},
     * with the addition of anything we expect the management method to add.
     */
    private void loadExpectedContext() {
        expectedContext = new JournalEntryContext(leadingContext);
        for (Map.Entry<RDFName, String[]> entry : contextAdditions.entrySet()) {
            expectedContext.getRecoveryAttributes().set(entry.getKey().uri,
                                                            entry.getValue());
        }
    }

    private void setupLeader() throws ModuleInitializationException {
        leadingDelegate = new MockManagementDelegate();
        MockServerForJournalTesting leadingServer =
                new MockServerForJournalTesting(leadingDelegate,
                                                THE_SERVER_HASH);
        creator =
                new JournalCreator(journalParameters, THE_ROLE, leadingServer);
        creator.setManagementDelegate(leadingDelegate);
    }

    /**
     * <p>
     * Note that "getTempStream" is the only Management call that doesn't use
     * "context" as its first argument.
     * </p>
     */
    private void executeManagmentMethod(Management management,
                                        String methodName,
                                        Object[] arguments)
            throws ServerException {
        Class<?>[] argTypes = new Class[arguments.length];
        for (int i = 0; i < arguments.length; i++) {
            argTypes[i] = getArgType(arguments[i]);
        }

        try {
            Method method =
                    Management.class.getDeclaredMethod(methodName, argTypes);
            method.invoke(management, arguments);
        } catch (SecurityException e) {
            e.printStackTrace();
            fail("Failed to invoke the method: " + e);
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
            fail("Failed to invoke the method: " + e);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
            fail("Failed to invoke the method: " + e);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
            fail("Failed to invoke the method: " + e);
        } catch (InvocationTargetException e) {
            Throwable cause = e.getCause();
            if (cause instanceof ServerException) {
                throw (ServerException) cause;
            } else {
                e.printStackTrace();
                fail("Failed to invoke the method: " + e);
            }
        }
    }

    /**
     * Figure the class of each argument to the call, assuming that we always
     * use <code>boolean</code> or <code>int</code> rather than
     * {@link Boolean} or {@link Integer}. Also, use {@link Context} and
     * {@link InputStream} rather than one of their subclasses.
     */
    private Class<?> getArgType(Object argument) {
        if (argument == null) {
            fail("Can't run unit test with null arguments.");
        }
        Class<?> argType = argument.getClass();
        if (argType.equals(Integer.class)) {
            argType = Integer.TYPE;
        }
        if (argType.equals(Boolean.class)) {
            argType = Boolean.TYPE;
        }
        if (Context.class.isAssignableFrom(argType)) {
            argType = Context.class;
        }
        if (InputStream.class.isAssignableFrom(argType)) {
            argType = InputStream.class;
        }
        return argType;
    }

    private void closeLeader() throws ModuleShutdownException {
        // Tell the leader to close the "file".
        creator.shutdown();
        // Transfer the journal contents to the reader.
        MockJournalReader.setBuffer(MockJournalWriter.getBuffer());
    }

    public void setupFollower() throws ModuleInitializationException {
        followingDelegate = new MockManagementDelegate();
        MockServerForJournalTesting followingServer =
                new MockServerForJournalTesting(followingDelegate,
                                                THE_SERVER_HASH);
        consumer =
                new JournalConsumer(journalParameters,
                                    THE_ROLE,
                                    followingServer);
    }

    private void letFollowerCatchUp() throws ModuleShutdownException {
        // Start the follower.
        consumer.setManagementDelegate(followingDelegate);
        // Wait for it to catch up.
        waitForConsumerThread();
        // Shut it down.
        consumer.shutdown();
    }

    /**
     * Compare the expected call with the call that the delegate actually
     * recorded.
     */
    private void assertExpectedCall(String label,
                                    MockManagementDelegate delegate) {
        if (delegate.getCallCount() != 1) {
            fail("Wrong number of " + label + " calls: expected 1, actual "
                 + delegate.getCallCount() + ". Calls are as follows:\n"
                 + delegate.getCalls());
        }
        assertEquals(label + " calls", expectedCall, delegate.getCalls().get(0));
    }

    private void assertEmptyJournal() {
        assertEquals("non-empty journal", 0, MockJournalWriter.getBuffer()
                .length());
    }

    /**
     * Let me know when the follower has caught up.
     */
    private void waitForConsumerThread() {
        try {
            // Shouldn't there be a better way?
            Thread.sleep(500);
        } catch (InterruptedException e) {
            // Won't happen; wouldn't care.
        }
    }

}
TOP

Related Classes of org.fcrepo.server.journal.TestJournalRoundTrip

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.