test.junit.fedora.server.journal.TestJournalRoundTrip Maven / Gradle / Ivy
package fedora.server.journal;
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.junit.Before;
import org.junit.Test;
import fedora.common.Constants;
import fedora.common.rdf.RDFName;
import fedora.server.Context;
import fedora.server.errors.InvalidStateException;
import fedora.server.errors.ModuleInitializationException;
import fedora.server.errors.ModuleShutdownException;
import fedora.server.errors.ServerException;
import fedora.server.journal.entry.JournalEntryContext;
import fedora.server.management.Management;
import fedora.server.management.MockManagementDelegate;
import fedora.server.management.MockManagementDelegate.Call;
import fedora.server.storage.types.DSBinding;
import fedora.server.storage.types.DSBindingMap;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.fail;
/**
* Check every management method to be sure:
*
* - that we know whether it is journalled or not
* - that we know what items are stored in the context for recovery
* - that a journalled method is played back the same as it was recorded
* - that a journalled method WILL NOT be accepted from an outside source by
* a JournalConsumer
* - that a non-journalled method WILL be accepted from an outside source by
* a JournalConsumer
*
*
* @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 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 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;
private DSBindingMap bindingMap;
/*
* ------------------------------------------------------------------------
* 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();
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();
}
@Before
public void initializeExpectedCall() {
// Every test should call buildExpectedCall().
expectedCall = null;
}
@Before
public void initializeBindingMap() {
bindingMap = new DSBindingMap();
bindingMap.dsBindMapID = "bindMapID";
bindingMap.dsBindMapLabel = "bindMapLabel";
bindingMap.dsBindMechanismPID = "bindMechPID";
bindingMap.dsBindings = new DSBinding[0];
bindingMap.state = "bindState";
}
/*
* ------------------------------------------------------------------------
* The tests
* ------------------------------------------------------------------------
*/
@Test
public void addDatastream() throws ServerException {
expectInContext(Constants.RECOVERY.DATASTREAM_ID, "theDsId");
testJournalledMethod(JournalConstants.METHOD_ADD_DATASTREAM,
leadingContext,
"thePid",
"theDsId",
new String[0],
"theDsLabel",
false,
"theMIMEType",
"theFormatURI",
"theLocation",
"theControlGroup",
"theDsState",
"theChecksumType",
"theChecksum",
"theLogMessage");
}
@Test
public void addDisseminator() throws ServerException {
expectInContext(Constants.RECOVERY.DISSEMINATOR_ID, "dissID");
testJournalledMethod(JournalConstants.METHOD_ADD_DISSEMINATOR,
leadingContext,
"thePid",
"theBdefPID",
"aBmechPID",
"dissLabel",
bindingMap,
"dissState",
"theLogMessage");
}
@Test
public void adminPing() throws ServerException {
testNonJournalledMethod("adminPing", leadingContext);
}
@Test
public void compareDatastreamChecksum() throws ServerException {
testNonJournalledMethod("compareDatastreamChecksum",
leadingContext,
"thePid",
"theDsId",
new Date(12345L));
}
@Test
public void exportObject() throws ServerException {
testNonJournalledMethod("exportObject",
leadingContext,
"PID",
"format",
"SomeExportContext",
"encoding");
}
@Test
public void getDatastream() throws ServerException {
testNonJournalledMethod("getDatastream",
leadingContext,
"PID",
"aDatastreamID",
new Date());
}
@Test
public void getDatastreamHistory() throws ServerException {
testNonJournalledMethod("getDatastreamHistory",
leadingContext,
"PID",
"anotherDatastreamID");
}
@Test
public void getDatastreams() throws ServerException {
testNonJournalledMethod("getDatastreams",
leadingContext,
"sonOfPID",
new Date(111111L),
"someStateString");
}
@Test
public void getDisseminator() throws ServerException {
testNonJournalledMethod("getDisseminator",
leadingContext,
"anotherPID",
"dissID",
new Date(111111L));
}
@Test
public void getDisseminators() throws ServerException {
testNonJournalledMethod("getDisseminators",
leadingContext,
"sonOfPID",
new Date(222111L),
"someDissState");
}
@Test
public void getDisseminatorHistory() throws ServerException {
testNonJournalledMethod("getDisseminatorHistory",
leadingContext,
"sonOfPID",
"dissID");
}
@Test
public void getNextPID() throws ServerException {
expectInContext(Constants.RECOVERY.PID_LIST, new String[] {
"sillyPID_0", "sillyPID_1", "sillyPID_2", "sillyPID_3",
"sillyPID_4"});
testJournalledMethod(JournalConstants.METHOD_GET_NEXT_PID,
leadingContext,
5,
"myFavoriteNamespace");
}
@Test
public void getObjectProperties() throws ServerException {
testNonJournalledMethod("getObjectProperties", leadingContext, "yaPID");
}
@Test
public void getObjectXML() throws ServerException {
testNonJournalledMethod("getObjectXML",
leadingContext,
"myPID",
"encodingScheme");
}
/**
* This one will always be special, in that it doesn't use a context as its
* first argument. If it were a Journalled method, that would be a problem.
*/
@Test
public void getTempStream() throws ServerException {
testNonJournalledMethod(METHOD_GET_TEMP_STREAM, "streamID");
}
@Test
public void ingestObject() throws ServerException {
expectInContext(Constants.RECOVERY.PID, "IngestObject:1");
testJournalledMethod(JournalConstants.METHOD_INGEST_OBJECT,
leadingContext,
new ByteArrayInputStream(new byte[0]),
"theLogMessage",
"aFormat",
"someEncoding",
true);
}
@Test
public void modifyDatastreamByReference() throws ServerException {
testJournalledMethod(JournalConstants.METHOD_MODIFY_DATASTREAM_BY_REFERENCE,
leadingContext,
"myPid",
"datastreamIdentifier",
new String[] {"altID"},
"datastreamLabel",
"mime/type",
"formatUri",
"dsLocation",
"checksumType",
"checksum",
"logMessage",
false);
}
@Test
public void modifyDatastreamByValue() throws ServerException {
testJournalledMethod(JournalConstants.METHOD_MODIFY_DATASTREAM_BY_VALUE,
leadingContext,
"myPid",
"datastreamIdentifier",
new String[] {"altID"},
"datastreamLabel",
"mime/type",
"formatUri",
new ByteArrayInputStream(new byte[0]),
"checksumType",
"checksum",
"logMessage",
false);
}
@Test
public void modifyObject() throws ServerException {
testJournalledMethod(JournalConstants.METHOD_MODIFY_OBJECT,
leadingContext,
"myPid",
"state",
"objectLabel",
"owner",
"logMessage");
}
@Test
public void modifyDisseminator() throws ServerException {
testJournalledMethod(JournalConstants.METHOD_MODIFY_DISSEMINATOR,
leadingContext,
"myPid",
"disseminatorID",
"bMechPID",
"disseminatorLabel",
bindingMap,
"state",
"logMessage",
false);
}
@Test
public void purgeDatastream() throws ServerException {
testJournalledMethod(JournalConstants.METHOD_PURGE_DATASTREAM,
leadingContext,
"myPid",
"dsID",
new Date(123),
new Date(456),
"logMessage",
true);
}
@Test
public void purgeDisseminator() throws ServerException {
testJournalledMethod(JournalConstants.METHOD_PURGE_DISSEMINATOR,
leadingContext,
"myPid",
"dissID",
new Date(123),
"logMessage");
}
@Test
public void purgeObject() throws ServerException {
testJournalledMethod(JournalConstants.METHOD_PURGE_OBJECT,
leadingContext,
"aPID",
"PurgeLogMessage",
true);
}
@Test
public void putTempStream() throws ServerException {
expectInContext(Constants.RECOVERY.UPLOAD_ID, "tempStreamId");
testJournalledMethod(JournalConstants.METHOD_PUT_TEMP_STREAM,
leadingContext,
new ByteArrayInputStream(new byte[0]));
}
@Test
public void setDatastreamState() throws ServerException {
testJournalledMethod(JournalConstants.METHOD_SET_DATASTREAM_STATE,
leadingContext,
"pid",
"dsID",
"dsState",
"dsLogMessage");
}
@Test
public void setDatastreamVersionable() throws ServerException {
testJournalledMethod(JournalConstants.METHOD_SET_DATASTREAM_VERSIONABLE,
leadingContext,
"lastPID",
"lastDsID",
true,
"the Log!");
}
@Test
public void setDisseminatorState() throws ServerException {
testJournalledMethod(JournalConstants.METHOD_SET_DISSEMINATOR_STATE,
leadingContext,
"yaPID",
"yaDsID",
"newState",
"the Message");
}
/*
* ------------------------------------------------------------------------
* Helper methods.
* ------------------------------------------------------------------------
*/
/**
* What additional "recovery attributes" should we expect the leading
* delegate to set into the context?
*/
private void expectInContext(RDFName key, Object value) {
contextAdditions.put(key, value);
}
/**
*
* Test a Journalled method.
*
*
* 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.
*
*
* Calling the method directly on the JournalConsumer should produce an
* exception.
*
*
* @throws ModuleInitializationException
* @throws ModuleShutdownException
*/
private void testJournalledMethod(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.
}
}
/**
*
* Test a non-Journalled method.
*
*
* 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.
*
*
* Call the selected method on the JournalConsumer. Again, compare the call
* to the one that we expected.
*
*/
private void testNonJournalledMethod(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);
}
/**
*
* The expected call includes the expected context, as well as the supplied
* arguments.
*
*
* Note that "getTempStream" is the only Management call that doesn't use
* "context" as its first argument.
*
*/
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 entry : contextAdditions.entrySet()) {
try {
expectedContext.getRecoveryAttributes().set(entry.getKey().uri,
entry.getValue());
} catch (Exception e) {
fail("Stupid design of MultiValueMap");
}
}
}
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);
}
/**
*
* Note that "getTempStream" is the only Management call that doesn't use
* "context" as its first argument.
*
*/
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 boolean
or int
rather than
* {@link Bookean} 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.
}
}
}