/*
* eXist Open Source Native XML Database
* Copyright (C) 2001-04 The eXist Team
*
* http://exist-db.org
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*
* $Id$
*/
package org.exist.storage.journal;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import org.apache.log4j.Logger;
import org.exist.storage.DBBroker;
/**
* Read log entries from the journal file. This class is used during recovery to scan the
* last journal file. It uses a memory-mapped byte buffer on the file.
* Journal entries can be read forward (during redo) or backward (during undo).
*
* @author wolf
*
*/
public class JournalReader {
private static final Logger LOG = Logger.getLogger(JournalReader.class);
private FileChannel fc;
private ByteBuffer header = ByteBuffer.allocateDirect(Journal.LOG_ENTRY_HEADER_LEN);
private ByteBuffer payload = ByteBuffer.allocateDirect(8192);
private int fileNumber;
private DBBroker broker;
/**
* Opens the specified file for reading.
*
* @param broker
* @param file
* @param fileNumber
* @throws LogException
*/
public JournalReader(DBBroker broker, File file, int fileNumber) throws LogException {
this.broker = broker;
this.fileNumber = fileNumber;
try {
FileInputStream is = new FileInputStream(file);
fc = is.getChannel();
} catch (IOException e) {
throw new LogException("Failed to read log file " + file.getAbsolutePath(), e);
}
}
/**
* Returns the next entry found from the current position.
*
* @return the next entry
* @throws LogException if an entry could not be read due to an inconsistency on disk.
*/
public Loggable nextEntry() throws LogException {
try {
if (fc.position() + Journal.LOG_ENTRY_BASE_LEN > fc.size())
return null;
return readEntry();
} catch (IOException e) {
return null;
}
}
/**
* Returns the previous entry found by scanning backwards from the current position.
*
* @return the previous entry
* @throws LogException if an entry could not be read due to an inconsistency on disk.
* @throws LogException
*/
public Loggable previousEntry() throws LogException {
try {
if (fc.position() == 0)
return null;
// go back two bytes and read the back-link of the last entry
fc.position(fc.position() - 2);
header.clear().limit(2);
int bytes = fc.read(header);
if (bytes < 2)
throw new LogException("Incomplete log entry found!");
header.flip();
final short prevLink = header.getShort();
// position the channel to the start of the previous entry and mark it
final long prevStart = fc.position() - 2 -prevLink;
fc.position(prevStart);
final Loggable loggable = readEntry();
// reset to the mark
fc.position(prevStart);
return loggable;
} catch (IOException e) {
throw new LogException("Fatal error while reading journal entry: " + e.getMessage(), e);
}
}
public Loggable lastEntry() throws LogException {
try {
fc.position(fc.size());
return previousEntry();
} catch (IOException e) {
throw new LogException("Fatal error while reading journal entry: " + e.getMessage(), e);
}
}
/**
* Read a single entry.
*
* @return The entry
* @throws LogException
*/
private Loggable readEntry() throws LogException {
try {
final long lsn = Lsn.create(fileNumber, (int) fc.position() + 1);
header.clear();
int bytes = fc.read(header);
if (bytes <= 0)
return null;
if (bytes < Journal.LOG_ENTRY_HEADER_LEN)
throw new LogException("Incomplete log entry header found: " + bytes);
header.flip();
final byte entryType = header.get();
final long transactId = header.getLong();
final short size = header.getShort();
if (fc.position() + size > fc.size())
throw new LogException("Invalid length");
final Loggable loggable = LogEntryTypes.create(entryType, broker, transactId);
if (loggable == null)
throw new LogException("Invalid log entry: " + entryType + "; size: " + size + "; id: " +
transactId + "; at: " + Lsn.dump(lsn));
loggable.setLsn(lsn);
if (size + 2 > payload.capacity()) {
// resize the payload buffer
payload = ByteBuffer.allocate(size + 2);
}
payload.clear().limit(size + 2);
bytes = fc.read(payload);
if (bytes < size + 2)
throw new LogException("Incomplete log entry found!");
payload.flip();
loggable.read(payload);
final short prevLink = payload.getShort();
if (prevLink != size + Journal.LOG_ENTRY_HEADER_LEN) {
LOG.warn("Bad pointer to previous: prevLink = " + prevLink + "; size = " + size +
"; transactId = " + transactId);
throw new LogException("Bad pointer to previous in entry: " + loggable.dump());
}
return loggable;
} catch (Exception e) {
throw new LogException(e.getMessage(), e);
}
}
/**
* Re-position the file position so it points to the start of the entry
* with the given LSN.
*
* @param lsn
* @throws LogException
*/
public void position(long lsn) throws LogException {
try {
fc.position((int) Lsn.getOffset(lsn) - 1);
} catch (IOException e) {
throw new LogException("Fatal error while reading journal: " + e.getMessage(), e);
}
}
public void close() {
try {
fc.close();
} catch (IOException e) {
}
fc = null;
}
}