/*
DBJournal.java
Class to handle the journal file for the DBStore.
Created: 3 December 1996
Module By: Jonathan Abbey, jonabbey@arlut.utexas.edu
-----------------------------------------------------------------------
Ganymede Directory Management System
Copyright (C) 1996-2014
The University of Texas at Austin
Ganymede is a registered trademark of The University of Texas at Austin
Contact information
Web site: http://www.arlut.utexas.edu/gash2
Author Email: ganymede_author@arlut.utexas.edu
Email mailing list: ganymede@arlut.utexas.edu
US Mail:
Computer Science Division
Applied Research Laboratories
The University of Texas at Austin
PO Box 8029, Austin TX 78713-8029
Telephone: (512) 835-3200
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package arlut.csd.ganymede.server;
import java.io.DataOutput;
import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import arlut.csd.Util.StringUtils;
import arlut.csd.Util.TranslationService;
import arlut.csd.ganymede.common.ObjectStatus;
import arlut.csd.ganymede.rmi.db_field;
/*------------------------------------------------------------------------------
class
DBJournal
------------------------------------------------------------------------------*/
/**
* <p>The DBJournal class is used to provide journalling of changes to
* the {@link arlut.csd.ganymede.server.DBStore DBStore} during
* operations. The Journal file will contain a complete list of all
* changes made since the last dump of the complete DBStore. The
* Journal file is composed of a header block followed by a number of
* transactions.</p>
*
* <p>Each transaction consists of a number of object modification
* records, each record specifying the creation, deletion, or
* modification of a particular object. At the end of the
* transaction, a marker indicates the completion of the transaction.
* At DBStore startup time, the journal is read in and all complete
* transactions recorded are performed on the main DBStore.</p>
*
* <p>Generally, if the DBStore was shut down correctly, the entire
* memory structure of the DBStore will be cleanly dumped out and the
* Journal will be removed. The Journal is intended to insure that
* the DBStore remains transaction consistent if the server running
* Ganymede crashes during runtime.</p>
*
* <p>See the {@link arlut.csd.ganymede.server.DBEditSet DBEditSet}
* class for more information on Ganymede transactions.</p>
*
* <p>Nota bene: this class includes synchronized methods which
* serialize operations on the Ganymede transaction journal. The
* DBJournal monitor is intended to be the innermost monitor for
* operations involving the DBStore and DBJournal objects.
* Synchronized methods in DBJournal must not call synchronized
* methods on DBStore, as synchronized DBStore methods can and will
* call methods on DBJournal.</p>
*/
public final class DBJournal implements ObjectStatus {
static boolean debug = false;
public static void setDebug(boolean val)
{
debug = val;
}
/**
* <p>TranslationService object for handling string localization in
* the Ganymede server.</p>
*/
static final TranslationService ts = TranslationService.getTranslationService("arlut.csd.ganymede.server.DBJournal");
static final String id_string = "GJournal";
static final short major_version = 2;
static final short minor_version = 3;
static final String OPENTRANS = "open";
static final String CLOSETRANS = "close";
static final String FINALIZE = "finalize";
static final byte CREATE = 1;
static final byte EDIT = 2;
static final byte DELETE = 3;
// instance variables
private final DBStore store;
private String filename;
private RandomAccessFile jFile = null;
private boolean dirty = false; // dirty is true if the journal has any
// transactions written out
private short file_major_version = -1;
private short file_minor_version = -1;
private short file_dbstore_major_version = -1;
private short file_dbstore_minor_version = -1;
private int transactionsInJournal = 0;
private DBJournalTransaction incompleteTransaction = null;
/* -- */
static void initialize(DataOutput out, DBStore store) throws IOException
{
out.writeUTF(DBJournal.id_string);
out.writeShort(DBJournal.major_version);
out.writeShort(DBJournal.minor_version);
out.writeShort(store.major_version);
out.writeShort(store.minor_version);
out.writeLong(System.currentTimeMillis());
}
/* -- */
/**
* <p>The DBJournal constructor takes a filename and creates a
* DBJournal object. If the file named does not exist, the
* DBJournal constructor will create the file and write the
* DBJournal header, leaving the file pointer pointing to the end of
* the file. If the Journal file does exist, the constructor will
* advance the file pointer to the end of the file.</p>
*
* <p>In either case, the file will be made ready for new
* transactions to be written.</p>
*/
public DBJournal(DBStore store, String filename) throws IOException
{
this.store = store;
this.filename = filename;
if (this.store == null)
{
// "bad parameter, store == null"
throw new IllegalArgumentException(ts.l("init.badstore"));
}
if (this.filename == null)
{
// "bad parameter, filename == null"
throw new IllegalArgumentException(ts.l("init.badfile"));
}
// "Initializing DBStore Journal: {0}"
debug(ts.l("init.initing", filename));
File file = new File(filename);
if (!file.exists())
{
// create an empty Journal file
// "Creating Journal File"
debug(ts.l("init.creating"));
jFile = new RandomAccessFile(filename, "rw");
// "Writing DBStore Journal header"
debug(ts.l("init.writing"));
initialize(jFile, this.store);
dirty = false;
}
else
{
// open an existing Journal file and prepare to
// write to the end of it
// "Opening Journal File for Append"
debug(ts.l("init.opening"));
jFile = new RandomAccessFile(filename, "rw");
// look to see if there are any transactions in the journal
readHeaders();
try
{
if (jFile.readUTF().compareTo(OPENTRANS) != 0)
{
// "DBJournal constructor: open string mismatch"
throw new IOException(ts.l("init.badheader"));
}
else
{
dirty = true; // we have some transactions in the existing journal
}
}
catch (EOFException ex)
{
dirty = false; // we have no transactions in the existing journal
}
// from now on, write to append to the end of the file
jFile.seek(jFile.length());
}
}
/**
* <p>This method returns true if the disk file being loaded by this DBStore
* has a version number greater than major.minor.</p>
*/
public boolean isAtLeast(int major, int minor)
{
if (this.file_major_version < 0)
{
// "DBJournal.isAtLeast() called before journal loaded."
throw new RuntimeException(ts.l("isAtLeast.notloaded"));
}
return (this.file_major_version > major ||
(this.file_major_version == major && this.file_minor_version >= minor));
}
/**
* <p>This method returns true if the disk file being loaded by this DBStore
* has a version number earlier than major.minor.</p>
*/
public boolean isLessThan(int major, int minor)
{
if (this.file_major_version < 0)
{
// "DBJournal.isLessThan() called before journal loaded."
throw new RuntimeException(ts.l("isLessThan.notloaded"));
}
return (this.file_major_version < major ||
(this.file_major_version == major && this.file_minor_version < minor));
}
/**
* <p>This method returns true if the disk file being loaded by this DBStore
* has a version number equal to major.minor.</p>
*/
public boolean isAtRev(int major, int minor)
{
if (this.file_major_version < 0)
{
// "DBJournal.isAtRev() called before journal loaded."
throw new RuntimeException(ts.l("isAtRev.notloaded"));
}
return (this.file_major_version == major && this.file_minor_version == minor);
}
/**
* <p>This method returns true if the disk file being loaded by this
* DBStore has a version number between greater than or equal to
* major1.minor1 and less than major2.minor2.</p>
*/
public boolean isBetweenRevs(int major1, int minor1, int major2, int minor2)
{
if (this.file_major_version < 0)
{
// "DBJournal.isBetweenRevs() called before journal loaded."
throw new RuntimeException(ts.l("isBetweenRevs.notloaded"));
}
if (this.file_major_version == major1 && this.file_minor_version >= minor1)
{
return true;
}
if (this.file_major_version == major2 && this.file_minor_version < minor2)
{
return true;
}
if (this.file_major_version > major1 && this.file_major_version < major2)
{
return true;
}
return false;
}
/**
* <p>The reset() method is used to copy the journal file to a safe
* location and truncate it. reset() should be called immediately
* after the DBStore is dumped to disk and before the DumpLock is
* relinquished.</p>
*/
public synchronized void reset() throws IOException
{
String newname;
/* - */
// "DBJournal: Resetting Journal File"
debug(ts.l("reset.resetting"));
if (jFile != null)
{
jFile.close();
}
File file = new File(filename);
newname = filename + ".old";
// "DBJournal: saving old Journal as {0}"
debug(ts.l("reset.savingold", newname));
if (!file.renameTo(new File(newname)))
{
throw new IOException("Couldn't rename " + file.getPath() + " to " + newname);
}
// "DBJournal: creating fresh Journal {0}"
debug(ts.l("reset.freshness", filename));
jFile = new RandomAccessFile(filename, "rw");
initialize(jFile, this.store);
jFile.getFD().sync();
dirty = false;
transactionsInJournal = 0;
GanymedeAdmin.updateTransCount();
if (Ganymede.log != null)
{
Ganymede.log.logSystemEvent(new DBLogEvent("journalreset",
ts.l("reset.logstring"), // "Ganymede Journal Reset"
null,
null,
null,
null));
}
}
/**
* <p>The load() method reads in all transactions in the current
* DBStore Journal and makes the appropriate changes to the DBStore
* Object Bases. This method should be called after the main body
* of the DBStore is loaded and before the DBStore is put into
* production mode.</p>
*
* <p>load() will return true if the on-disk journal was in a
* consistent state, with no incomplete transactions. If load()
* encounters EOF in the middle of attempting to read in a
* transaction record, load() will return false. In either case,
* any valid and complete transaction records will be processed and
* integrated into the DBStore.</p>
*/
public synchronized boolean load() throws IOException
{
long transaction_time = 0;
Date transactionDate = null;
int object_count = 0;
boolean EOFok = true;
List<JournalEntry> entries = null;
byte operation;
short obj_type, obj_id;
DBObjectBase base;
DBObject obj;
String status = null;
int nextTransactionNumber = 0;
/* - */
entries = new ArrayList<JournalEntry>();
// skip past the journal header block
readHeaders();
// start reading and applying the changes
try
{
while (true)
{
if (jFile.readUTF().compareTo(OPENTRANS) != 0)
{
// "DBJournal.load(): Transaction open string mismatch"
debug(ts.l("load.openmismatch"));
throw new IOException(ts.l("load.openmismatch"));
}
else
{
// "DBJournal.load(): Transaction open string match OK"
debug(ts.l("load.okmatch"));
}
EOFok = false;
// "Reading transaction time"
status = ts.l("load.readingtime");
transaction_time = jFile.readLong();
transactionDate = new Date(transaction_time);
debug(ts.l("load.showtime", transactionDate));
if (isAtLeast(2,1))
{
nextTransactionNumber = jFile.readInt();
Ganymede.db.updateTransactionNumber(nextTransactionNumber);
debug("nextTransactionNumber:"+nextTransactionNumber);
}
// "Reading object count"
status = ts.l("load.readingobjcount");
// read object information
object_count = jFile.readInt();
debug("Objects In Transaction: " + object_count);
if (object_count > 0)
{
dirty = true;
}
for (int i = 0; i < object_count; i++)
{
Integer iObj = Integer.valueOf(i);
// "Reading operation code for object {0}"
status = ts.l("load.readingopcode", iObj);
operation = jFile.readByte();
// "Reading object type for object {0}"
status = ts.l("load.readingtype", iObj);
obj_type = jFile.readShort();
base = store.getObjectBase(obj_type);
switch (operation)
{
case CREATE:
// "Reading created object {0}"
status = ts.l("load.readingcreated", iObj);
obj = new DBObject(base, jFile, true);
if (debug)
{
// "Create: {0}"
debug(ts.l("load.create", obj.getInvid()));
printObject(obj);
}
entries.add(new JournalEntry(base, obj.getID(), obj));
break;
case EDIT:
// "Reading edited object {0}"
status = ts.l("load.readingedited", iObj);
DBObjectDeltaRec delta = new DBObjectDeltaRec(jFile);
DBObject original = DBStore.viewDBObject(delta.getInvid());
obj = new DBObject(original, delta);
// we have to do the delta.toString() after we apply the delta so that
// the scalarValue fields get parented
if (debug)
{
// "Delta read:\n\t{0}\n"
debug("\n"+ts.l("load.deltaread", StringUtils.replaceStr(delta.toString(),"\n","\n\t")));
// "DBJournal.load(): original object, before delta edit:"
debug(ts.l("load.original"));
printObject(original);
// "DBJournal.load(): object after delta edit:"
debug(ts.l("load.postdelta"));
printObject(obj);
}
entries.add(new JournalEntry(base, obj.getID(), obj));
break;
case DELETE:
// "Reading deleted object {0}"
status = ts.l("load.readingdeleted", iObj);
obj_id = jFile.readShort();
// "Delete: {0}:{1}"
debug(ts.l("load.delete", base.getName(), Short.valueOf(obj_id)));
entries.add(new JournalEntry(base, obj_id, null));
break;
}
}
// "Reading close transaction information"
status = ts.l("load.readingclosed");
String closeDebug = jFile.readUTF();
Long transTimeDebug = jFile.readLong();
debug("closeDebug and transTimeDebug:"+ closeDebug +"*"+ transTimeDebug.toString()+"* transaction_time:"+transaction_time);
//if ((jFile.readUTF().compareTo(CLOSETRANS) != 0) ||
// (jFile.readLong() != transaction_time))
if ((closeDebug.compareTo(CLOSETRANS) != 0) ||
(transTimeDebug != transaction_time))
{
// "Transaction close timestamp mismatch"
throw new IOException(ts.l("load.badclosed"));
}
// we've read in all of the transaction's changes.. now we
// need to see whether all active sync channels were
// successfully written to. if we don't see the complete
// finalize token stream, we'll need to undo the
// transaction after all, by clearing the offending
// transaction from whatever of the sync channels it was
// written to
if (isAtLeast(2,2))
{
boolean success = false;
try
{
success = ((jFile.readUTF().compareTo(FINALIZE) == 0) &&
(jFile.readLong() == transaction_time) &&
(jFile.readInt() == nextTransactionNumber));
if (!success)
{
// "DBJournal: transaction {0} not finalized in journal, rejecting"
throw new IOException(ts.l("load.notfinalized", Integer.valueOf(nextTransactionNumber)));
}
}
catch (IOException ex)
{
// we didn't get the transaction finalized, but it
// had been persisted, which means that some of
// the sync channels may possibly have had some
// transactions written out. we should have
// cleaned up after any incomplete writes to the
// sync channels, but if the server was abnormally
// terminated at the wrong time, we may have some
// transactions lingering. we'll record enough
// information in the incompleteTransaction member
// variable so that Ganymede.java can clean them
// up
this.incompleteTransaction = new DBJournalTransaction(transaction_time, -1, nextTransactionNumber, null);
// then we'll throw this exception back up
throw ex;
}
}
// "Finished transaction"
status = ts.l("load.finished");
// "Transaction {0} successfully read from Journal.\nIntegrating transaction into DBStore memory image."
debug(ts.l("load.success", transactionDate));
// "Processing {0} objects"
debug(ts.l("load.processing", Integer.valueOf(entries.size())));
// okay, process this transaction
for (JournalEntry entry: entries)
{
entry.process(store);
}
// clear the entries we've now processed
entries.clear();
EOFok = true;
}
}
catch (EOFException ex)
{
if (EOFok)
{
// "All transactions processed successfully"
debug(ts.l("load.allclear"));
return true;
}
else
{
// "DBJournal file unexpectedly ended: state = {0}"
debug(ts.l("load.failure", status));
// ok, the journal ended badly, but aside from losing a partial
// transaction, we should be ok.
return false;
}
}
}
/**
* <p>The writeTransaction() method performs the work of persisting a
* transaction to the DBStore Journal. writeTransaction() should be
* called before the changes are actually finalized in the DBStore
* Object Bases. If writeTransaction() is not able to successfully
* write the complete transaction log to the Journal file, an
* IOException will be thrown.</p>
*
* <p>Note that writeTransaction() does not mark the transaction in
* the on-disk journal file as finalized. If the server is
* abnormally terminated (or if the disk fills) between the time that
* writeTransaction() is called and the time that
* finalizeTransaction() is called, the server on startup will not consider
* the transaction to have been finally put to rest, and the system will attempt
* to push the unfinalized change in the journal to the sync channels.</p>
*
* @return On success, a {@link
* arlut.csd.ganymede.server.DBJournalTransaction} recording the
* transaction's time stamp and integral transaction number is
* returned. This DBJournalTransaction can be passed back to
* finalizeTransaction() when writes to the sync channels are
* completed.
*/
public synchronized DBJournalTransaction writeTransaction(DBEditSet transaction) throws IOException
{
DBJournalTransaction transRecord;
Date now;
DBEditObject[] objects = transaction.getObjectList();
/* - */
now = new Date();
transRecord = new DBJournalTransaction(now.getTime(),
jFile.getFilePointer(),
Ganymede.db.getNextTransactionNumber(),
transaction.getUsername());
// Writing transaction to the Journal : {0}
debug(ts.l("writeTransaction.writing", now));
try
{
jFile.writeUTF(OPENTRANS);
jFile.writeLong(transRecord.getTime());
jFile.writeInt(transRecord.getTransactionNumber());
// "Objects in Transaction: {0}"
debug(ts.l("writeTransaction.objcount", Integer.valueOf(objects.length)));
jFile.writeInt(objects.length);
for (DBEditObject eObj: objects)
{
switch (eObj.getStatus())
{
case CREATING:
jFile.writeByte(CREATE);
jFile.writeShort(eObj.objectBase.getTypeID());
eObj.emit(jFile);
if (debug)
{
// "Creating object:"
System.err.println(ts.l("writeTransaction.creating"));
printObject(eObj);
}
break;
case EDITING:
jFile.writeByte(EDIT);
jFile.writeShort(eObj.objectBase.getTypeID());
DBObjectDeltaRec delta = new DBObjectDeltaRec(eObj.original, eObj);
delta.emit(jFile);
if (debug)
{
// "Wrote object edit record:\n\t{0}"
System.err.print(ts.l("writeTransaction.wroteobjedit",
StringUtils.replaceStr(delta.toString(),"\n","\n\t")));
}
break;
case DELETING:
jFile.writeByte(DELETE);
jFile.writeShort(eObj.objectBase.getTypeID());
jFile.writeShort(eObj.getID());
// "Wrote object deletion record:\n\t{0} : {1}"
debug(ts.l("writeTransaction.wroteobjdel", eObj.objectBase.getName(),Integer.valueOf(eObj.getID())));
break;
case DROPPING:
if (debug)
{
// "Dropping object:"
System.err.println(ts.l("writeTransaction.dropping"));
printObject(eObj);
}
break;
}
}
dirty = true;
// write out the end of transaction stamp.. the transaction_time
// is used to verify that we completed this write okay.
jFile.writeUTF(CLOSETRANS);
jFile.writeLong(transRecord.getTime());
// "Transaction {0} persisted to Journal."
debug(ts.l("writeTransaction.written", now));
}
catch (IOException ex)
{
// oops, did we run out of disk space? roll back what we've
// written
try
{
undoTransaction(transRecord);
}
catch (IOException inex)
{
// ***
// *** Error in commit_finalizeTransaction()! Couldn''t undo a transaction in the
// *** journal file after catching an exception!
// ***
// *** The journal may not be completely recoverable!
// ***
//
// {0}
Ganymede.debug(ts.l("writeTransaction.badundo", inex.toString()));
}
}
return transRecord;
}
/**
* The finalizeTransaction() method actually performs the full work of
* writing out a transaction to the DBStore Journal.
* writeTransaction() should be called before the changes are
* actually finalized in the DBStore Object Bases. If
* writeTransaction() is not able to successfully write the complete
* transaction log to the Journal file, an IOException will be
* thrown. The Journal entries are structured so that if a Journal
* entry can't be completed, the transaction's whole entry will
* be ignored at load time.
*/
public synchronized void finalizeTransaction(DBJournalTransaction transRecord) throws IOException
{
jFile.writeUTF(FINALIZE);
jFile.writeLong(transRecord.getTime());
jFile.writeInt(transRecord.getTransactionNumber());
// and push the blocks to disk
jFile.getFD().sync();
transactionsInJournal++;
GanymedeAdmin.updateTransCount();
}
/**
* <p>The undoTransaction() method is responsible for rolling back the
* most recent transaction written to the journal, in the event that
* writing to the sync channels or finalizing the transaction fails
* (due to a lack of disk space, presumably).</p>
*/
public synchronized void undoTransaction(DBJournalTransaction transRecord) throws IOException
{
jFile.setLength(transRecord.getOffset()-1);
jFile.seek(transRecord.getOffset());
// try to make sure the disk operations we just did are committed
// to disk
jFile.getFD().sync();
Ganymede.db.undoNextTransactionNumber(transRecord.getTransactionNumber());
}
/**
* <p>If the journal contained a persisted but not finalized
* transaction when we tried to load it, this method will return a
* DBJournalTransaction containing enough information that the
* Ganymede main() method can iterate over the defined sync channels
* and clear out any lingering xml sync for that transaction.</p>
*
* <p>This can only happen if the server is abnormally terminated in
* a very narrow window, but in the one case in 10,000 where an
* abnormal shutdown hit right in that window, we'll go ahead and
* worry about it.</p>
*/
public DBJournalTransaction getIncompleteTransaction()
{
return this.incompleteTransaction;
}
/**
* <p>After the Ganymede main() method has tried to clean out any
* remaining bits of the non-finalized transaction, it will need
* to clear the incompleteTransaction record. This method does
* that. </p>
*/
public void clearIncompleteTransaction()
{
this.incompleteTransaction = null;
}
/**
* Returns true if the journal does not contain any transactions.
*/
public boolean isClean()
{
return !dirty;
}
/**
* <p>Returns the number of transactions currently recorded in the
* on-disk journal file.</p>
*
* <p>The precise value is not critical, so this is
* unsynchronized</p>
*/
public int getTransactionsInJournal()
{
return this.transactionsInJournal;
}
/**
* <p>This method is used to read and check the first few fields of the journal
* as a sanity check on journal open/load.</p>
*
* <p>This method <b>MUST NOT</b> be called after the journal is open and active,
* or else the journal will become corrupted.</p>
*/
void readHeaders() throws IOException
{
// "DBJournal: Loading transactions from {0}"
debug(ts.l("readHeaders.loading", filename));
jFile.seek(0);
if (DBJournal.id_string.compareTo(jFile.readUTF()) != 0)
{
// "Error, id_string mismatch.. wrong file type?"
throw new RuntimeException(ts.l("readHeaders.badid"));
}
file_major_version = jFile.readShort();
file_minor_version = jFile.readShort();
debug("Journal file major minor version:"+file_major_version+","+file_minor_version);
// At version 2,3: Fail if DBStore version greater than equal 2/22
// and DBJournal <= 2/2, because we changed the structure of the
// DBJournal file after this.
if (store.isAtLeast(2,23) && isLessThan(2,3))
{
// "Error, journal version mismatch.. wrong file type?"
throw new RuntimeException("Mismatch Version between DBStore and DBJournal. ");
}
if ((file_major_version > DBJournal.major_version) ||
(file_major_version == DBJournal.major_version && file_minor_version > minor_version))
{
// "Error, journal version mismatch.. wrong file type?"
throw new RuntimeException(ts.l("readHeaders.badversion"));
}
// At journal version 2,3 we save the dbversion in the headers also.
if (isAtLeast(2,3))
{
file_dbstore_major_version = jFile.readShort();
file_dbstore_minor_version = jFile.readShort();
if (file_dbstore_major_version != store.file_major || file_dbstore_minor_version != store.file_minor)
{
throw new RuntimeException("DBStore and Journal file versions do not match. ");
}
}
if (debug)
{
// "DBJournal file created {0}"
debug(ts.l("readHeaders.created", new Date(jFile.readLong())));
}
else
{
// JAMES QUESTION - why is this only not in debug mode.... odd??!?!!
jFile.readLong(); // date is there for others to look at
}
}
private void printObject(DBObject obj)
{
String objectStr = obj.getPrintString();
objectStr = StringUtils.replaceStr(objectStr, "\n", "\n\t");
System.err.println("\t" + objectStr);
}
/**
* This is a convenience method used by server-side code to send
* debug output to stderr and to any attached admin consoles.
*/
static public void debug(String string)
{
if (debug)
{
System.err.println(string);
}
}
}
/*------------------------------------------------------------------------------
class
JournalEntry
------------------------------------------------------------------------------*/
/**
* <p>This class holds data corresponding to a modification record for
* a single object in the server's {@link
* arlut.csd.ganymede.server.DBJournal DBJournal} class.</p>
*/
class JournalEntry {
static boolean debug = false;
// ---
DBObjectBase base;
int id;
DBObject obj; // if null, we'll delete
/* -- */
public JournalEntry(DBObjectBase base, int id, DBObject obj)
{
this.base = base;
this.id = id;
this.obj = obj;
}
void process(DBStore store)
{
DBObjectBaseField definition;
DBObject oldObject;
/* -- */
debug("JournalEntry.process():"+
"\t" + StringUtils.replaceStr(this.toString(), "\n", "\n\t"));
if (obj == null)
{
// delete the object
oldObject = base.getObject(id);
if (oldObject == null)
{
Ganymede.debug("Warning.. journal is instructing us to delete an object not in the database");
}
else
{
// Remove the object. If any of the fields are namespace
// restricted, see if any of the values currently held in
// such a field are registered in a namespace. If so, and
// if the field we're deleting is the one currently taking
// that value's slot in the namespace, remove the value
// from the namespace hash.
// NOTE: we don't want to do a full-up object delete here
// because that would just get put into a new transaction.
// All invid linking issues are taken care of when this
// transaction was written to the journal.. the invid
// unbinding process would automatically include the other
// objects in their post-commit state, so we don't have
// to worry about it here.
// We do need to unregister the former asymmetric links
// from the DBStore aSymLinkTracker, however.
oldObject.unregisterAsymmetricLinks();
// and we need to clear out any namespace pointers
DBField[] tempFields = oldObject.listDBFields();
for (DBField _field: tempFields)
{
definition = _field.getFieldDef();
if (definition.getNameSpace() != null)
{
if (_field.isVector())
{
for (int j = 0; j < _field.size(); j++)
{
if (definition.getNameSpace().lookupPersistent(_field.key(j)) != _field)
{
throw new RuntimeException("Error, namespace mismatch in DBJournal code [" + j + "]");
}
definition.getNameSpace().removeHandle(_field.key(j));
}
}
else
{
if (definition.getNameSpace().lookupPersistent(_field.key()) != _field)
{
throw new RuntimeException("Error, namespace mismatch in DBJournal code");
}
definition.getNameSpace().removeHandle(_field.key());
}
}
}
}
base.remove(id);
}
else
{
// put the new object in place
// First we need to clear any back links from the old version
// of the object, if there was any such.
oldObject = base.getObject(id);
if (oldObject != null)
{
oldObject.unregisterAsymmetricLinks();
}
// Second, we need to go through and put these values in the namespace.. note that
// we don't bother checking to see if the values are already allocated in the
// namespace.. we are assuming that the transaction would not have been written
// out to disk with improper namespace management. We pretty much have to make
// this assumption here unless we're going to go to the trouble of doing multiple
// passes through the set of changes in this transaction, first unmarking any
// values freed by object deletion or changes, then going through and allocating
// new values. We may still wind up doing this.
DBField[] tempFields = obj.listDBFields();
for (DBField _field: tempFields)
{
definition = _field.getFieldDef();
if (definition.getNameSpace() != null)
{
if (_field.isVector())
{
// mark the elements in the vector in the namespace
// note that we don't use the namespace mark method here,
// because we are just setting up the namespace, not
// manipulating it in the context of an editset
for (int j = 0; j < _field.size(); j++)
{
definition.getNameSpace().receiveValue(_field.key(j), _field);
}
}
else
{
// mark the scalar value in the namespace
definition.getNameSpace().receiveValue(_field.key(), _field);
}
}
}
// update the backpointers for this object
obj.registerAsymmetricLinks();
base.put(obj);
}
}
public String toString()
{
if (base != null && obj != null)
{
return obj.getPrintString();
}
else if (base != null)
{
return "base: " + base.toString() + "\n" +
"id: " + id + "\n" +
"obj: null";
}
else if (obj != null)
{
return "base: null\n" +
"id: " + id + "\n" +
"obj: \n" + obj.getPrintString();
}
else
{
return "base: null\n" +
"id: " + id + "\n" +
"obj: null";
}
}
/**
* This is a convenience method used by server-side code to send
* debug output to stderr and to any attached admin consoles.
*/
static public void debug(String string)
{
if (debug)
{
System.err.println(string);
}
}
}