/* 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 fedora.server.journal; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Map; import javax.xml.namespace.QName; import javax.xml.stream.XMLEventReader; import javax.xml.stream.XMLStreamException; import javax.xml.stream.events.StartElement; import javax.xml.stream.events.XMLEvent; import org.apache.log4j.Logger; import fedora.server.errors.ModuleInitializationException; import fedora.server.errors.ServerException; import fedora.server.journal.entry.ConsumerJournalEntry; import fedora.server.journal.entry.JournalEntryContext; import fedora.server.journal.helpers.DecodingBase64OutputStream; import fedora.server.journal.helpers.JournalHelper; import fedora.server.journal.recoverylog.JournalRecoveryLog; import fedora.server.journal.xmlhelpers.AbstractXmlReader; import fedora.server.journal.xmlhelpers.ContextXmlReader; /** * The abstract base for all JournalReader classes. * <p> * Each child class is responsible for providing an XMLEventReader that is * positioned at the beginning of a JournalEntry tag. This class will read the * entry and leave the XMLEventReader positioned after the corresponding closing * tag. * * @author Jim Blake */ public abstract class JournalReader extends AbstractXmlReader implements JournalConstants { /** Logger for this class. */ private static final Logger LOG = Logger.getLogger(JournalReader.class.getName()); protected final Map<String, String> parameters; protected final String role; protected final JournalRecoveryLog recoveryLog; protected final ServerInterface server; private boolean ignoreHashErrors; /** * Create an instance of the proper JournalReader child class, as determined * by the server parameters. */ public static JournalReader getInstance(Map<String, String> parameters, String role, JournalRecoveryLog recoveryLog, ServerInterface server) throws ModuleInitializationException { try { Object journalReader = JournalHelper .createInstanceAccordingToParameter(PARAMETER_JOURNAL_READER_CLASSNAME, new Class[] { Map.class, String.class, JournalRecoveryLog.class, ServerInterface.class}, new Object[] { parameters, role, recoveryLog, server}, parameters); LOG.info("JournalReader is " + journalReader.toString()); return (JournalReader) journalReader; } catch (JournalException e) { String msg = "Can't create JournalReader"; LOG.error(msg, e); throw new ModuleInitializationException(msg, role, e); } } /** * Concrete sub-classes must implement this constructor. */ protected JournalReader(Map<String, String> parameters, String role, JournalRecoveryLog recoveryLog, ServerInterface server) throws JournalException { this.parameters = parameters; this.role = role; this.recoveryLog = recoveryLog; this.server = server; parseParameters(); } private void parseParameters() throws JournalException { String ignore = parameters.get(PARAMETER_IGNORE_HASH); if (ignore == null) { ignoreHashErrors = false; } else if (ignore.equals(VALUE_FALSE)) { ignoreHashErrors = false; } else if (ignore.equals(VALUE_TRUE)) { ignoreHashErrors = true; } else { throw new JournalException("'" + PARAMETER_IGNORE_HASH + "' parameter must be '" + VALUE_FALSE + "'(default) or '" + VALUE_TRUE + "'"); } } /** * Concrete sub-classes should probably synchronize this method, since it * can be called either from the JournalConsumerThread or from the Server. */ public abstract void shutdown() throws JournalException; /** * Concrete sub-classes should insure that their XMLEventReader is * positioned at the beginning of a JournalEntry, and call * {@link #readJournalEntry(XMLEventReader)}. It is likely that this method * should be synchronized also, since it could be called from * JournalConsumerThread when the server calls {@link #shutdown()} */ public abstract ConsumerJournalEntry readJournalEntry() throws JournalException, XMLStreamException; /** * Compare the repository hash from the journal file with the current hash * obtained from the server. If they do not match, either throw an exception * or simply log it, depending on the parameters. This method must not be * called before the server has completed initialization. That's the only * way we can be confident that the DOManager is present, and ready to * create the repository has that we will compare to. */ protected void checkRepositoryHash(String hash) throws JournalException { if (!server.hasInitialized()) { throw new IllegalStateException("The repository has is not available until " + "the server is fully initialized."); } JournalException hashException = null; if (hash == null) { hashException = new JournalException("'" + QNAME_TAG_JOURNAL + "' tag must have a '" + QNAME_ATTR_REPOSITORY_HASH + "' attribute."); } else { try { String currentHash = server.getRepositoryHash(); if (hash.equals(currentHash)) { recoveryLog.log("Validated repository hash: '" + hash + "'."); } else { hashException = new JournalException("'" + QNAME_ATTR_REPOSITORY_HASH + "' attribute in '" + QNAME_TAG_JOURNAL + "' tag does not match current repository hash: '" + hash + "' vs. '" + currentHash + "'."); } } catch (ServerException e) { hashException = new JournalException(e); } } if (hashException != null) { if (ignoreHashErrors) { recoveryLog.log("WARNING: " + hashException.getMessage()); } else { throw hashException; } } } /** * Read a JournalEntry from the journal, to produce a * <code>ConsumerJournalEntry</code> instance. Concrete sub-classes should * insure that the XMLEventReader is positioned at the beginning of a * JournalEntry before calling this method. */ protected ConsumerJournalEntry readJournalEntry(XMLEventReader reader) throws JournalException, XMLStreamException { StartElement startTag = getJournalEntryStartTag(reader); String methodName = getRequiredAttributeValue(startTag, QNAME_ATTR_METHOD); JournalEntryContext context = new ContextXmlReader().readContext(reader); ConsumerJournalEntry cje = new ConsumerJournalEntry(methodName, context); readArguments(reader, cje); return cje; } /** * Get the next event and complain if it isn't a JournalEntry start tag. */ private StartElement getJournalEntryStartTag(XMLEventReader reader) throws XMLStreamException, JournalException { XMLEvent event = reader.nextTag(); if (!isStartTagEvent(event, QNAME_TAG_JOURNAL_ENTRY)) { throw getNotStartTagException(QNAME_TAG_JOURNAL_ENTRY, event); } return event.asStartElement(); } /** * Read arguments and add them to the event, until we hit the end tag for * the event. */ private void readArguments(XMLEventReader reader, ConsumerJournalEntry cje) throws XMLStreamException, JournalException { while (true) { XMLEvent nextTag = reader.nextTag(); if (isStartTagEvent(nextTag, QNAME_TAG_ARGUMENT)) { readArgument(nextTag, reader, cje); } else if (isEndTagEvent(nextTag, QNAME_TAG_JOURNAL_ENTRY)) { return; } else { throw getNotNextMemberOrEndOfGroupException(QNAME_TAG_JOURNAL_ENTRY, QNAME_TAG_ARGUMENT, nextTag); } } } private void readArgument(XMLEvent nextTag, XMLEventReader reader, ConsumerJournalEntry journalEntry) throws JournalException, XMLStreamException { StartElement element = nextTag.asStartElement(); String argName = getRequiredAttributeValue(element, QNAME_ATTR_NAME); String argType = getRequiredAttributeValue(element, QNAME_ATTR_TYPE); if (ARGUMENT_TYPE_NULL.equals(argType)) { readNullArgument(reader, journalEntry, argName); } else if (ARGUMENT_TYPE_STRING.equals(argType)) { readStringArgument(reader, journalEntry, argName); } else if (ARGUMENT_TYPE_STRINGARRAY.equals(argType)) { readStringArrayArgument(reader, journalEntry, argName); } else if (ARGUMENT_TYPE_INTEGER.equals(argType)) { readIntegerArgument(reader, journalEntry, argName); } else if (ARGUMENT_TYPE_BOOLEAN.equals(argType)) { readBooleanArgument(reader, journalEntry, argName); } else if (ARGUMENT_TYPE_DATE.equals(argType)) { readDateArgument(reader, journalEntry, argName); } else if (ARGUMENT_TYPE_STREAM.equals(argType)) { readStreamArgument(reader, journalEntry, argName); } else { throw new JournalException("Unknown argument type: name='" + argName + "', type='" + argType + "'"); } } private void readStringArgument(XMLEventReader reader, ConsumerJournalEntry journalEntry, String name) throws XMLStreamException, JournalException { String value = readCharactersUntilEndOfArgument(reader, QNAME_TAG_ARGUMENT, journalEntry.getMethodName(), name, ARGUMENT_TYPE_STRING); journalEntry.addArgument(name, value); } private void readStringArrayArgument(XMLEventReader reader, ConsumerJournalEntry journalEntry, String name) throws XMLStreamException, JournalException { List<String> values = new ArrayList<String>(); while (true) { XMLEvent event = reader.nextTag(); if (isStartTagEvent(event, QNAME_TAG_ARRAYELEMENT)) { values .add(readCharactersUntilEndOfArgument(reader, QNAME_TAG_ARRAYELEMENT, journalEntry .getMethodName(), name, ARGUMENT_TYPE_STRINGARRAY)); } else if (isEndTagEvent(event, QNAME_TAG_ARGUMENT)) { break; } else { throw getUnexpectedEventInArgumentException(name, ARGUMENT_TYPE_STRINGARRAY, journalEntry .getMethodName(), event); } } Object[] valuesArray = values.toArray(new String[values.size()]); journalEntry.addArgument(name, valuesArray); } private void readIntegerArgument(XMLEventReader reader, ConsumerJournalEntry journalEntry, String name) throws XMLStreamException, JournalException { XMLEvent chars = reader.nextEvent(); if (!chars.isCharacters()) { throw getUnexpectedEventInArgumentException(name, ARGUMENT_TYPE_INTEGER, journalEntry .getMethodName(), chars); } Integer integerValue = Integer.valueOf(chars.asCharacters().getData()); XMLEvent endTag = reader.nextEvent(); if (isEndTagEvent(endTag, QNAME_TAG_ARGUMENT)) { journalEntry.addArgument(name, integerValue); } else { throw getUnexpectedEventInArgumentException(name, ARGUMENT_TYPE_INTEGER, journalEntry .getMethodName(), endTag); } } private void readBooleanArgument(XMLEventReader reader, ConsumerJournalEntry journalEntry, String name) throws XMLStreamException, JournalException { XMLEvent chars = reader.nextEvent(); if (!chars.isCharacters()) { throw getUnexpectedEventInArgumentException(name, ARGUMENT_TYPE_BOOLEAN, journalEntry .getMethodName(), chars); } Boolean booleanValue = Boolean.valueOf(chars.asCharacters().getData()); XMLEvent endTag = reader.nextEvent(); if (isEndTagEvent(endTag, QNAME_TAG_ARGUMENT)) { journalEntry.addArgument(name, booleanValue); } else { throw getUnexpectedEventInArgumentException(name, ARGUMENT_TYPE_BOOLEAN, journalEntry .getMethodName(), endTag); } } private void readDateArgument(XMLEventReader reader, ConsumerJournalEntry journalEntry, String name) throws XMLStreamException, JournalException { XMLEvent chars = reader.nextEvent(); if (!chars.isCharacters()) { throw getUnexpectedEventInArgumentException(name, ARGUMENT_TYPE_BOOLEAN, journalEntry .getMethodName(), chars); } Date dateValue = JournalHelper.parseDate(chars.asCharacters().getData()); XMLEvent endTag = reader.nextEvent(); if (isEndTagEvent(endTag, QNAME_TAG_ARGUMENT)) { journalEntry.addArgument(name, dateValue); } else { throw getUnexpectedEventInArgumentException(name, ARGUMENT_TYPE_DATE, journalEntry .getMethodName(), endTag); } } /** * An InputStream argument appears as a Base64-encoded String. It must be * decoded and written to a temp file, so it can be presented to the * management method as an InputStream again. */ private void readStreamArgument(XMLEventReader reader, ConsumerJournalEntry journalEntry, String name) throws XMLStreamException, JournalException { try { File tempFile = JournalHelper.createTempFile(); DecodingBase64OutputStream decoder = new DecodingBase64OutputStream(new FileOutputStream(tempFile)); while (true) { XMLEvent event = reader.nextEvent(); if (event.isCharacters()) { decoder.write(event.asCharacters().getData()); } else if (isEndTagEvent(event, QNAME_TAG_ARGUMENT)) { break; } else { throw getUnexpectedEventInArgumentException(name, ARGUMENT_TYPE_STREAM, journalEntry .getMethodName(), event); } } decoder.close(); journalEntry.addArgument(name, tempFile); } catch (IOException e) { throw new JournalException("failed to write stream argument to temp file", e); } } private void readNullArgument(XMLEventReader reader, ConsumerJournalEntry journalEntry, String name) throws XMLStreamException, JournalException { XMLEvent endTag = reader.nextTag(); if (!isEndTagEvent(endTag, QNAME_TAG_ARGUMENT)) { throw getUnexpectedEventInArgumentException(name, ARGUMENT_TYPE_NULL, journalEntry .getMethodName(), endTag); } } /** * Loop through a series of character events, accumulating the data into a * String. The character events should be terminated by an EndTagEvent with * the expected tag name. */ private String readCharactersUntilEndOfArgument(XMLEventReader reader, QName tagName, String methodName, String argumentName, String argumentType) throws XMLStreamException, JournalException { StringBuffer stringValue = new StringBuffer(); while (true) { XMLEvent event = reader.nextEvent(); if (event.isCharacters()) { stringValue.append(event.asCharacters().getData()); } else if (isEndTagEvent(event, tagName)) { break; } else { throw getUnexpectedEventInArgumentException(argumentName, argumentType, methodName, event); } } return stringValue.toString(); } /** * If we encounter an unexpected event when reading the a method argument, * create an exception with all of the pertinent information. */ private JournalException getUnexpectedEventInArgumentException(String name, String argumentType, String methodName, XMLEvent event) { return new JournalException("Unexpected event while processing '" + name + "' argument (type = '" + argumentType + "') for '" + methodName + "' method call: event is '" + event + "'"); } }