/* 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.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.Date; import java.util.Map; import javax.xml.stream.XMLEventWriter; import javax.xml.stream.XMLStreamException; import org.apache.log4j.Logger; import fedora.common.Constants; import fedora.server.errors.ServerException; import fedora.server.journal.entry.CreatorJournalEntry; import fedora.server.journal.helpers.EncodingBase64InputStream; import fedora.server.journal.helpers.JournalHelper; import fedora.server.journal.xmlhelpers.AbstractXmlWriter; import fedora.server.journal.xmlhelpers.ContextXmlWriter; /** * <p> * The abstract base for all JournalWriter classes. * </p> * <p> * Each child class is responsible for providing an XMLEventWriter that will * receive the JournalEntry tag. This class will format a JournalEntry object * into XML and add it to the XMLEventWriter. * </p> * <p> * Note that the writing of an entry is necessarily a three step process, * consisting of * <ol> * <li>calling {@link #prepareToWriteJournalEntry()},</li> * <li>invoking the management method,</li> * <li>calling {@link #writeJournalEntry(CreatorJournalEntry)}</li> * </ol> * Several factors combine to require this sequence. * <ul> * <li>Each new journal file starts with a repository hash.</li> * <li>Repository hashes are expensive to calculate (on the order of 10 * seconds). We don't want to calculate a hash unless it is needed for a new * journal file.</li> * <li>We cannot predict in advance which journal entry will require that a new * journal file be opened. The preceding file may be closed asynchronously as a * result of a period of inactivity.</li> * <li>The repository hash must be calculated before the management method is * invoked, so the receiver can confirm the file before making any changes to * its own repository.</li> * <li>The journal entry must be written after the management method is * invoked. The management method may add values to the context object in the * {@link CreatorJournalEntry}, and these values must be written to the * journal.</li> * </ul> * </p> * * @author Jim Blake */ public abstract class JournalWriter extends AbstractXmlWriter { /** Logger for this class. */ private static final Logger LOG = Logger.getLogger(JournalWriter.class.getName()); /** * A single object on which to synchronize all writing operations. The most * obvious use is in CreatorJournalEntry to be sure that Management methods * are single-threaded. A less obvious, but necessary use is to synchronize * the timeout on JournalFiles, so a file is not closed in the middle of an * operation. */ public static final Object SYNCHRONIZER = new Object(); /** * Create an instance of the proper JournalWriter child class, as determined * by the server parameters. */ public static JournalWriter getInstance(Map<String, String> parameters, String role, ServerInterface server) throws JournalException { Object journalWriter = JournalHelper .createInstanceAccordingToParameter(PARAMETER_JOURNAL_WRITER_CLASSNAME, new Class[] { Map.class, String.class, ServerInterface.class}, new Object[] { parameters, role, server}, parameters); LOG.info("JournalWriter is " + journalWriter.toString()); return (JournalWriter) journalWriter; } protected final String role; protected final Map<String, String> parameters; protected final ServerInterface server; /** * Concrete sub-classes must implement this constructor. */ protected JournalWriter(Map<String, String> parameters, String role, ServerInterface server) { this.parameters = parameters; this.role = role; this.server = server; } public abstract void shutdown() throws JournalException; /** * Concrete sub-classes should insure that a message transport is ready, and * call {@link #writeDocumentHeader(XMLEventWriter) if needed. This method * is called separately from {@link #writeJournalEntry(CreatorJournalEntry)}, * so we can obtain the repository hash before the Management method is * invoked. */ public abstract void prepareToWriteJournalEntry() throws JournalException; /** * Concrete sub-classes should provide an XMLEventWriter, and call * {@link #writeJournalEntry(XMLEventWriter)}, after which, they should * probably flush the XMLEventWriter. This method is called after the * Management method is invoked, since the Management method may modify the * context object in the journal entry. */ public abstract void writeJournalEntry(CreatorJournalEntry journalEntry) throws JournalException; /** * Subclasses should call this method to initialize a new Journal file. */ protected void writeDocumentHeader(XMLEventWriter writer) throws JournalException { writeDocumentHeader(writer, getRepositoryHash(), new Date()); } /** * Subclasses should call this method to initialize a new Journal file, if * they already know the repository hash and the current date. */ protected void writeDocumentHeader(XMLEventWriter writer, String repositoryHash, Date currentDate) throws JournalException { try { putStartDocument(writer); putStartTag(writer, QNAME_TAG_JOURNAL); putAttribute(writer, QNAME_ATTR_REPOSITORY_HASH, repositoryHash); putAttribute(writer, QNAME_ATTR_TIMESTAMP, JournalHelper .formatDate(currentDate)); } catch (XMLStreamException e) { throw new JournalException(e); } } /** * Subclasses should call this method to close a Journal file. */ protected void writeDocumentTrailer(XMLEventWriter writer) throws JournalException { try { putEndDocument(writer); } catch (XMLStreamException e) { throw new JournalException(e); } } /** * Format a JournalEntry object and write a JournalEntry tag to the journal. */ protected void writeJournalEntry(CreatorJournalEntry journalEntry, XMLEventWriter writer) throws JournalException { try { writeJournaEntryStartTag(journalEntry, writer); new ContextXmlWriter().writeContext(journalEntry.getContext(), writer); writeArguments(journalEntry.getArgumentsMap(), writer); putEndTag(writer, QNAME_TAG_JOURNAL_ENTRY); writer.flush(); } catch (XMLStreamException e) { throw new JournalException(e); } } private void writeJournaEntryStartTag(CreatorJournalEntry journalEntry, XMLEventWriter writer) throws XMLStreamException { putStartTag(writer, QNAME_TAG_JOURNAL_ENTRY); putAttribute(writer, QNAME_ATTR_METHOD, journalEntry.getMethodName()); putAttribute(writer, QNAME_ATTR_TIMESTAMP, JournalHelper .formatDate(journalEntry.getContext().now())); String[] clientIpArray = journalEntry .getContext() .getEnvironmentValues(Constants.HTTP_REQUEST.CLIENT_IP_ADDRESS.uri); if (clientIpArray != null && clientIpArray.length > 0) { putAttribute(writer, QNAME_ATTR_CLIENT_IP, clientIpArray[0]); } String[] loginIdArray = journalEntry.getContext() .getSubjectValues(Constants.SUBJECT.LOGIN_ID.uri); if (loginIdArray != null && loginIdArray.length > 0) { putAttribute(writer, QNAME_ATTR_LOGIN_ID, loginIdArray[0]); } } private void writeArguments(Map<String, Object> arguments, XMLEventWriter writer) throws XMLStreamException, JournalException { for (String key : arguments.keySet()) { Object value = arguments.get(key); if (value == null) { writeNullArgument(key, writer); } else if (value instanceof String) { writeStringArgument(key, (String) value, writer); } else if (value instanceof String[]) { writeStringArrayArgument(key, (String[]) value, writer); } else if (value instanceof Date) { writeDateArgument(key, (Date) value, writer); } else if (value instanceof Integer) { writeIntegerArgument(key, (Integer) value, writer); } else if (value instanceof Boolean) { writeBooleanArgument(key, (Boolean) value, writer); } else if (value instanceof File) { writeFileArgument(key, (File) value, writer); } else { throw new JournalException("Unknown argument type: name='" + key + "', type='" + value.getClass().getName() + "'"); } } } private void writeNullArgument(String key, XMLEventWriter writer) throws XMLStreamException { putStartTag(writer, QNAME_TAG_ARGUMENT); putAttribute(writer, QNAME_ATTR_NAME, key); putAttribute(writer, QNAME_ATTR_TYPE, ARGUMENT_TYPE_NULL); putEndTag(writer, QNAME_TAG_ARGUMENT); } private void writeStringArgument(String key, String value, XMLEventWriter writer) throws XMLStreamException { putStartTag(writer, QNAME_TAG_ARGUMENT); putAttribute(writer, QNAME_ATTR_NAME, key); putAttribute(writer, QNAME_ATTR_TYPE, ARGUMENT_TYPE_STRING); putCharacters(writer, value); putEndTag(writer, QNAME_TAG_ARGUMENT); } private void writeDateArgument(String key, Date date, XMLEventWriter writer) throws XMLStreamException { putStartTag(writer, QNAME_TAG_ARGUMENT); putAttribute(writer, QNAME_ATTR_NAME, key); putAttribute(writer, QNAME_ATTR_TYPE, ARGUMENT_TYPE_DATE); putCharacters(writer, JournalHelper.formatDate(date)); putEndTag(writer, QNAME_TAG_ARGUMENT); } private void writeIntegerArgument(String key, Integer value, XMLEventWriter writer) throws XMLStreamException { putStartTag(writer, QNAME_TAG_ARGUMENT); putAttribute(writer, QNAME_ATTR_NAME, key); putAttribute(writer, QNAME_ATTR_TYPE, ARGUMENT_TYPE_INTEGER); putCharacters(writer, value.toString()); putEndTag(writer, QNAME_TAG_ARGUMENT); } private void writeBooleanArgument(String key, Boolean value, XMLEventWriter writer) throws XMLStreamException { putStartTag(writer, QNAME_TAG_ARGUMENT); putAttribute(writer, QNAME_ATTR_NAME, key); putAttribute(writer, QNAME_ATTR_TYPE, ARGUMENT_TYPE_BOOLEAN); putCharacters(writer, value.toString()); putEndTag(writer, QNAME_TAG_ARGUMENT); } private void writeStringArrayArgument(String key, String[] value, XMLEventWriter writer) throws XMLStreamException { putStartTag(writer, QNAME_TAG_ARGUMENT); putAttribute(writer, QNAME_ATTR_NAME, key); putAttribute(writer, QNAME_ATTR_TYPE, ARGUMENT_TYPE_STRINGARRAY); for (String element : value) { putStartTag(writer, QNAME_TAG_ARRAYELEMENT); putCharacters(writer, element); putEndTag(writer, QNAME_TAG_ARRAYELEMENT); } putEndTag(writer, QNAME_TAG_ARGUMENT); } /** * An InputStream argument must be written as a Base64-encoded String. It is * read from the temp file in segments. Each segment is encoded and written * to the XML writer as a series of character events. */ private void writeFileArgument(String key, File file, XMLEventWriter writer) throws XMLStreamException, JournalException { try { putStartTag(writer, QNAME_TAG_ARGUMENT); putAttribute(writer, QNAME_ATTR_NAME, key); putAttribute(writer, QNAME_ATTR_TYPE, ARGUMENT_TYPE_STREAM); EncodingBase64InputStream encoder = new EncodingBase64InputStream(new BufferedInputStream(new FileInputStream(file))); String encodedChunk; while (null != (encodedChunk = encoder.read(1000))) { putCharacters(writer, encodedChunk); } encoder.close(); putEndTag(writer, QNAME_TAG_ARGUMENT); } catch (IOException e) { throw new JournalException("IO Exception on temp file", e); } } /** * 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. */ private String getRepositoryHash() throws JournalException { if (!server.hasInitialized()) { throw new IllegalStateException("The repository hash is not available until " + "the server is fully initialized."); } try { return server.getRepositoryHash(); } catch (ServerException e) { throw new JournalException(e); } } }