/* 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.readerwriter.multifile; import java.io.File; import java.util.Map; import javax.xml.stream.XMLEventReader; import javax.xml.stream.XMLStreamException; import javax.xml.stream.events.Attribute; import javax.xml.stream.events.StartElement; import javax.xml.stream.events.XMLEvent; import fedora.server.journal.JournalException; import fedora.server.journal.JournalReader; import fedora.server.journal.ServerInterface; import fedora.server.journal.entry.ConsumerJournalEntry; import fedora.server.journal.helpers.ParameterHelper; import fedora.server.journal.recoverylog.JournalRecoveryLog; /** * A JournalReader implementation for "recovering", when using a * {@link MultiFileJournalWriter}, or the equivalent. * <p> * The recovery is complete when the all of the files in the journal directory * have been processed and moved to the archive directory. * * @author Jim Blake */ public class MultiFileJournalReader extends JournalReader implements MultiFileJournalConstants { // the directory that holds the journal files before they are processed. private final File journalDirectory; // the directory that will hold the journal files after they are processed. private final File archiveDirectory; // journal file names will start with this. private final String filenamePrefix; protected JournalInputFile currentFile; protected boolean open = true; public MultiFileJournalReader(Map<String, String> parameters, String role, JournalRecoveryLog recoveryLog, ServerInterface server) throws JournalException { super(parameters, role, recoveryLog, server); journalDirectory = ParameterHelper .parseParametersForWritableDirectory(parameters, PARAMETER_JOURNAL_DIRECTORY); archiveDirectory = ParameterHelper .parseParametersForWritableDirectory(parameters, PARAMETER_ARCHIVE_DIRECTORY); filenamePrefix = ParameterHelper.parseParametersForFilenamePrefix(parameters); checkDirectoriesAreDifferent(); } private void checkDirectoriesAreDifferent() throws JournalException { if (archiveDirectory.equals(journalDirectory)) { throw new JournalException("Archive directory and Journal directory are identical: '" + archiveDirectory.getPath() + "'"); } } /* * Close the current file and set the closed flag. */ @Override public synchronized void shutdown() throws JournalException { if (open) { recoveryLog.log("Shutdown requested by server."); closeCurrentFile(); open = false; } } /* * Advance to the next tag. If its end of file, close and get next file. If * null, return a null entry. */ @Override public synchronized ConsumerJournalEntry readJournalEntry() throws JournalException, XMLStreamException { if (!open) { return null; } scanThroughFilesForNextJournalEntry(); if (currentFile == null) { return null; } else { String identifier = peekAtJournalEntryIdentifier(currentFile); ConsumerJournalEntry journalEntry = super.readJournalEntry(currentFile.getReader()); journalEntry.setIdentifier(identifier); return journalEntry; } } /** * Create an identifier string for the Journal Entry, so we can easily * connect the entries in the Recovery Log with those in the Journal. Call * this before calling * {@link JournalReader#readJournalEntry(XMLEventReader)}, because the * reader is positioned at the beginning of the JournalEntry, so a peek() * will give us the start tag, with the info we need. */ private String peekAtJournalEntryIdentifier(JournalInputFile file) throws XMLStreamException { String fileName = file.getFilename(); XMLEvent event = file.getReader().peek(); String timeString = "unknown"; if (event.isStartElement()) { StartElement start = event.asStartElement(); Attribute timeStamp = start.getAttributeByName(QNAME_ATTR_TIMESTAMP); if (timeStamp != null) { timeString = timeStamp.getValue(); } } return "file='" + fileName + "', entry='" + timeString + "'"; } /** * Advance to the next journal entry if there is one. If we find one, the * current file will be pointing to it. If we don't find one, there will be * no current file. */ private void scanThroughFilesForNextJournalEntry() throws JournalException { try { while (true) { if (currentFile != null) { // Check to see whether the current file contains any more // entries. advancePastWhitespace(currentFile.getReader()); XMLEvent next = currentFile.getReader().peek(); if (isStartTagEvent(next, QNAME_TAG_JOURNAL_ENTRY)) { // found it return; } else if (isEndTagEvent(next, QNAME_TAG_JOURNAL)) { // need to get the next file closeCurrentFile(); } else { // problems throw getNotNextMemberOrEndOfGroupException(QNAME_TAG_JOURNAL, QNAME_TAG_JOURNAL_ENTRY, next); } } // If we don't have a file open, try to open one. if (currentFile == null) { currentFile = openNextFile(); } // If we still don't have a file open, we're finished. if (currentFile == null) { // if there was no next file, we're done. return; } // A new file needs to be advanced before using. advanceIntoFile(currentFile.getReader()); } } catch (XMLStreamException e) { throw new JournalException(e); } } /** * Look in the directory for files that match the prefix. If there are none, * leave with currentFile still null. If we find one, advance into it. */ protected JournalInputFile openNextFile() throws JournalException { File[] journalFiles = MultiFileJournalHelper .getSortedArrayOfJournalFiles(journalDirectory, filenamePrefix); if (journalFiles.length == 0) { return null; } JournalInputFile nextFile = new JournalInputFile(journalFiles[0]); recoveryLog.log("Opening journal file: '" + nextFile.getFilename() + "'"); return nextFile; } /** * Advance past the document header to the first JournalEntry. */ private void advanceIntoFile(XMLEventReader reader) throws XMLStreamException, JournalException { XMLEvent event = reader.nextEvent(); if (!event.isStartDocument()) { throw new JournalException("Expecting XML document header, but event was '" + event + "'"); } event = reader.nextTag(); if (!isStartTagEvent(event, QNAME_TAG_JOURNAL)) { throw new JournalException("Expecting FedoraJournal start tag, but event was '" + event + "'"); } String hash = getOptionalAttributeValue(event.asStartElement(), QNAME_ATTR_REPOSITORY_HASH); checkRepositoryHash(hash); } private void closeCurrentFile() throws JournalException { if (currentFile != null) { recoveryLog.log("Closing journal file: '" + currentFile.getFilename() + "'"); currentFile.closeAndRename(archiveDirectory); currentFile = null; } } @Override public String toString() { return super.toString() + ", journalDirectory='" + journalDirectory + "', archiveDirectory='" + archiveDirectory + "', filenamePrefix='" + filenamePrefix + "'"; } }