/*
GASH 2
DBNameSpace.java
The GANYMEDE object storage system.
Created: 2 July 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.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.rmi.RemoteException;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;
import arlut.csd.Util.NamedStack;
import arlut.csd.Util.TranslationService;
import arlut.csd.ganymede.common.Invid;
import arlut.csd.ganymede.rmi.NameSpace;
/*------------------------------------------------------------------------------
class
DBNameSpace
------------------------------------------------------------------------------*/
/**
* <p>DBNameSpaces are the objects used to manage unique value
* tracking in {@link arlut.csd.ganymede.server.DBField DBFields} that are
* unique value constrained. DBNameSpace is smart enough to
* coordinate unique value allocation and management during object
* editing across concurrent transactions.</p>
*
* <p>In general, transactions in Ganymede are not able to affect each
* other at all, save through the acquisition of exclusive editing
* locks on invidivual objects, and through the atomic acquisition of
* values for unique value constrained DBFields. Once a transaction
* allocates a unique value using either the {@link
* arlut.csd.ganymede.server.DBNameSpace#mark(arlut.csd.ganymede.server.DBEditSet,
* java.lang.Object,arlut.csd.ganymede.server.DBField) mark()}, {@link
* arlut.csd.ganymede.server.DBNameSpace#unmark(arlut.csd.ganymede.server.DBEditSet,
* java.lang.Object,arlut.csd.ganymede.server.DBField oldField)
* unmark()}, or {@link
* arlut.csd.ganymede.server.DBNameSpace#reserve(arlut.csd.ganymede.server.DBEditSet,
* java.lang.Object) reserve()} methods, no other transaction can
* allocate that value, until the first transaction calls the {@link
* arlut.csd.ganymede.server.DBNameSpace#commit(arlut.csd.ganymede.server.DBEditSet)
* commit()}, {@link
* arlut.csd.ganymede.server.DBNameSpace#abort(arlut.csd.ganymede.server.DBEditSet)
* abort()}, or {@link
* arlut.csd.ganymede.server.DBNameSpace#rollback(arlut.csd.ganymede.server.DBEditSet,
* java.lang.String) rollback()} methods.</p>
*
* <p>In order to perform this unique value management, DBNameSpace
* maintains a private Hashtable, {@link
* arlut.csd.ganymede.server.DBNameSpace#uniqueHash uniqueHash}, that
* associates the allocated vales in the namespace with {@link
* arlut.csd.ganymede.server.DBNameSpaceHandle DBNameSpaceHandle}
* objects which track the transaction that is manipulating the value,
* if any, as well as the DBField object in the database that is
* checked in with that value. The {@link
* arlut.csd.ganymede.server.GanymedeSession GanymedeSession} query
* logic takes advantage of this to do optimized, hashed look-ups of
* values for unique value constrained fields to locate objects in the
* database rather than having to iterate over all objects of a given
* type to find a particular match.</p>
*
* <p>DBNameSpaces may be defined in the server's schema editor to be
* either case sensitive or case insensitive. The DBNameSpace class
* uses the {@link arlut.csd.ganymede.server.GHashtable GHashtable}
* class to handle the representational issues in the unique value
* hash for this.</p>
*/
public final class DBNameSpace implements NameSpace {
static final boolean debug = true;
/**
* TranslationService object for handling string localization in
* the Ganymede server.
*/
static final TranslationService ts = TranslationService.getTranslationService("arlut.csd.ganymede.server.DBNameSpace");
/**
* The number of simultaneous transactions in progress
* that we will size the transactions hash for initially.
*/
static final int TRANSCOUNT = 30;
/**
* The initial number of slots that we will reserve in our
* uniqueHash hashtable, if we are starting from scratch with a new
* namespace.
*/
static final int DEFAULTSIZE = 397; // prime
/**
* The number of spaces we save in our hashtable before it will need
* to grow again.
*/
static final int GROWTHSPACE = 250;
// ---
/**
* At startup, if we are reading from a Ganymede db file of version
* 2.14 or later, we will read the size of the hash to allocate at
* database loading time so we don't have to do a lot of hash
* expansion.
*/
private int hashSize;
/**
* treat differently-cased Strings as the same for key?
*/
private boolean caseInsensitive;
/**
* the name of this namespace
*/
private String name;
/**
* Hashtable mapping values allocated (permanently, for objects
* checked in to the database, or temporarily, for objects being
* manipulated by active transactions) in this namespace to {@link
* arlut.csd.ganymede.server.DBNameSpaceHandle DBNameSpaceHandle}
* objects that track the current status of the values.
*/
private GHashtable uniqueHash;
/**
* During schema editing, we keep a copy of the uniqueHash that we had
* when schema edited started. If fields are detached or attached to this
* namespace during schema editing, we will make the appropriate changes
* to the uniqueHash. If the schema edit is committed, uniqueHash is kept
* and saveHash is cleared. If the schema edit is cancelled, uniqueHash
* is set back to saveHash and the saveHash reference is cleared. saveHash
* will always be null except during schema editing.
*/
private GHashtable saveHash = null;
/**
* Hashtable mapping {@link arlut.csd.ganymede.server.DBEditSet
* DBEditSet's} currently active modifying values in this namespace
* to {@link arlut.csd.ganymede.server.DBNameSpace.DBNameSpaceTransaction
* DBNameSpaceTransaction} objects.
*/
private Hashtable<DBEditSet, DBNameSpaceTransaction> transactions;
/* -- */
/**
* Constructor for new DBNameSpace for DBStore initialization.
*
* @param name Name for this name space
* @param caseInsensitive If true, case is disregarded in this namespace
*/
public DBNameSpace(String name, boolean caseInsensitive)
{
this.name = name;
this.caseInsensitive = caseInsensitive;
this.uniqueHash = new GHashtable(DEFAULTSIZE, caseInsensitive);
this.transactions = new Hashtable<DBEditSet, DBNameSpaceTransaction>(TRANSCOUNT);
}
/**
* Create a new DBNameSpace object from a stream definition.
*/
public DBNameSpace(DataInput in) throws IOException
{
receive(in);
this.uniqueHash = new GHashtable(hashSize, caseInsensitive);
this.transactions = new Hashtable<DBEditSet, DBNameSpaceTransaction>(TRANSCOUNT);
}
/**
* Read in a namespace definition from a DataInput stream.
*/
private void receive(DataInput in) throws IOException
{
this.name = in.readUTF();
this.caseInsensitive = in.readBoolean();
if (Ganymede.db.isAtLeast(2, 14))
{
this.hashSize = gnu.trove.PrimeFinder.nextPrime((int)Math.ceil((in.readInt() + GROWTHSPACE) / 0.75f));
}
else
{
this.hashSize = DEFAULTSIZE;
}
}
/**
* Write out a namespace definition to a DataOutput stream.
*/
public synchronized void emit(DataOutput out) throws IOException
{
out.writeUTF(this.name);
out.writeBoolean(this.caseInsensitive);
out.writeInt(this.uniqueHash.size()); // added at DBStore version 2.14
}
/**
* Write out an XML entity for this namespace.
*/
public synchronized void emitXML(XMLDumpContext xDump) throws IOException
{
xDump.startElementIndent("namespace");
xDump.attribute("name", getName());
if (this.caseInsensitive)
{
xDump.attribute("case-sensitive", "false");
}
else
{
xDump.attribute("case-sensitive", "true");
}
xDump.endElement("namespace");
}
/**
* Returns the name of this namespace.
*
* @see arlut.csd.ganymede.rmi.NameSpace
*/
public synchronized String getName()
{
return this.name;
}
/**
* Sets the name of this namespace. Returns false
* if the name is already taken by another namespace
*
* @see arlut.csd.ganymede.rmi.NameSpace
*/
public synchronized boolean setName(String newName)
{
// XXX need to make sure this new name isn't in conflict
// with existing names XXX
this.name = newName;
return true;
}
/**
* Returns true if case is to be disregarded in comparing
* entries in namespace managed fields.
*
* @see arlut.csd.ganymede.rmi.NameSpace
*/
public synchronized boolean isCaseInsensitive()
{
return this.caseInsensitive;
}
/**
* Turns case sensitivity on/off. If b is true, case will be
* disregarded in comparing entries in namespace managed fields.
*
* @see arlut.csd.ganymede.rmi.NameSpace
*/
public synchronized void setInsensitive(boolean b)
{
if (b == this.caseInsensitive)
{
return;
}
// let's see if we can do this safely.. we'll throw an
// IllegalStateException if changing the case sensitivity would
// cause a collision, otherwise this will take care of things
this.uniqueHash.setInSensitivity(b);
// if we've got here, we are okay to go
this.caseInsensitive = b;
}
/**
* Returns true if this namespace has value allocated.
*/
public synchronized boolean containsKey(Object value)
{
return this.uniqueHash.containsKey(value);
}
/**
* <p>Publicly accessible function used to record the presence of a
* namespace value in this namespace during Ganymede database
* loading.</p>
*
* <p>Used by the {@link arlut.csd.ganymede.server.DBObject
* DBObject} receive() method and the {@link
* arlut.csd.ganymede.server.DBJournal DBJournal}'s {@link
* arlut.csd.ganymede.server.JournalEntry JournalEntry} class'
* process() method to build up the namespace during server
* start-up.</p>
*/
public synchronized void receiveValue(Object value, DBField field)
{
putHandle(value, new DBNameSpaceHandle(field));
}
/**
* <p>Publicly accessible function used to clear the given
* value from this namespace in a non-transactional fashion.</p>
*
* <p>Used by {@link arlut.csd.ganymede.server.DBJournal
* DBJournal}'s {@link arlut.csd.ganymede.server.JournalEntry
* JournalEntry} class' process() method to rectify the namespace
* during server start-up.</p>
*/
public synchronized void removeHandle(Object value)
{
this.uniqueHash.remove(value);
}
/**
* <p>This method allows the namespace to be used as a unique valued
* search index.</p>
*
* <p>Note that this lookup is case sensitive or not according to
* the case sensitivity of this DBNameSpace. If this DBNameSpace is
* case insensitive, the DBField returned may contain the value (if
* value is a String) with different capitalization.</p>
*
* <p>As well, this method is really probably only useful in the
* context of a DBReadLock, but we're not doing anything to enforce
* this requirement at this point.</p>
*
* @param value The value to search for in the namespace hash.
*/
public synchronized DBField lookupPersistent(Object value)
{
DBNameSpaceHandle handle = (DBNameSpaceHandle) this.uniqueHash.get(value);
if (handle == null)
{
return null;
}
return handle.getPersistentField();
}
/**
* <p>If the value is attached to an object that is being created or
* edited in a transaction, this method will return the editable
* DBField that contains the constrained value during editing.</p>
*
* <p>Note that this lookup is case sensitive or not according to
* the case sensitivity of this DBNameSpace. If this DBNameSpace is
* case insensitive, the DBField returned may contain the value (if
* value is a String) with different capitalization.</p>
*
* <p>As well, this method is really probably useful in the context
* of a DBReadLock, but we're not doing anything to enforce this
* requirement at this point.</p>
*
* @param value The value to search for in the namespace hash.
*/
public synchronized DBField lookupShadow(Object value)
{
DBNameSpaceHandle handle = (DBNameSpaceHandle) this.uniqueHash.get(value);
if (handle == null)
{
return null;
}
return handle.getShadowField();
}
/**
* <p>This method looks to find where the given value is bound in
* the namespace, taking into account the transactional view the
* calling session has. If the value is attached to an object in
* the current transaction, this method will return a reference to
* the editable shadow DBField containing the value. If not, this
* method will either return the DBField containing the read-only
* persistent version from the DBStore, or null if the value sought
* has been cleared from use in the objects being edited by the
* transaction.</p>
*
* <p>Note that this lookup is case sensitive or not according to
* the case sensitivity of this DBNameSpace. If this DBNameSpace is
* case insensitive, the DBField returned may contain the value (if
* value is a String) with different capitalization.</p>
*
* <p>As well, this method is really probably useful in the context
* of a DBReadLock, but we're not doing anything to enforce this
* requirement at this point.</p>
*
* @param session The GanymedeSession to use to lookup the containing object..
* useful when a GanymedeSession is doing the looking up of value
* @param value The value to search for in the namespace hash.
*/
public synchronized DBField lookupMyValue(GanymedeSession session, Object value)
{
DBNameSpaceHandle handle = (DBNameSpaceHandle) this.uniqueHash.get(value);
if (handle == null)
{
return null;
}
if (handle.isEditedByUs(session))
{
return handle.getShadowField();
}
return handle.getPersistentField(session);
}
/**
* <p>This method reserves a value so that the given editSet is
* assured of being able to use this value at some point before the
* transaction is commited or canceled. reserve() is different from
* mark() in that there is no field specified to be holding the
* value, and that when the transaction is committed or canceled,
* the value will be returned to the available list. During the
* transaction, the transaction code can mark the value at any time
* with assurance that they will be able to do so.</p>
*
* <p>If a transaction attempts to reserve() a value that is already
* being held by an object in the transaction, reserve() will return
* false.</p>
*
* @param editSet The transaction claiming the unique value <value>
* @param value The unique value that transaction editset is attempting to claim
*
* @return true if the value could be reserved in the given editSet.
*/
public boolean reserve(DBEditSet editSet, Object value)
{
checkSchemaEditInProgress(false);
if (editSet == null || value == null)
{
throw new IllegalArgumentException();
}
DBNameSpaceHandle handle;
/* -- */
// Is this value already taken?
if (this.uniqueHash.containsKey(value))
{
handle = (DBNameSpaceHandle) this.uniqueHash.get(value);
if (!handle.isCheckedOut())
{
return false;
}
else
{
if (!handle.isEditedByUs(editSet))
{
return false;
}
if (handle.getShadowField() != null)
{
return false;
}
if (handle.isReserved())
{
return false;
}
}
return true;
}
handle = new DBNameSpaceEditingHandle(editSet, null);
handle.setReserved(true);
putHandle(value, handle);
remember(editSet, value);
return true;
}
/**
* <p>This method reserves a value so that the given editSet is
* assured of being able to use this value at some point before the
* transaction is commited or canceled. reserve() is different from
* mark() in that there is no field specified to be holding the
* value, and that when the transaction is committed or canceled,
* the value will be returned to the available list. During the
* transaction, the transaction code can mark the value at any time
* with assurance that they will be able to do so.</p>
*
* <p>If onlyUsed is false and a transaction attempts to reserve() a
* value that is already being held by an object in the transaction,
* reserve() will return true, even though a subsequent mark()
* attempt would fail unless the value is first unmarked.</p>
*
* @param editSet The transaction claiming the unique value <value>
* @param value The unique value that transaction editset is
* attempting to claim
* @param onlyUnused If true, reserve() will return false if the
* value is already attached to a field connected to this namespace,
* even if in an object attached to the editSet provided.
*
* @return true if the value was successfully reserved in the given
* editSet.
*
* @deprecated We are no longer supporting false values of the
* onlyUnused paramater. We never did so properly before, anyway.
*/
@Deprecated
public boolean reserve(DBEditSet editSet, Object value, boolean onlyUnused)
{
if (onlyUnused != true)
{
throw new UnsupportedOperationException("reserve() no longer accepts a false onlyUnused parameter");
}
return this.reserve(editSet, value);
}
/**
* <p>This method tests to see whether a value in the namespace can
* be marked as in use. Such a marking is done to allow an editset
* to have the ability to juggle values in namespace associated
* fields without allowing another editset to acquire a value needed
* if the editset transaction is aborted.</p>
*
* <p>For array db fields, all elements in the array should be
* testmark'ed in the context of a synchronized block on the
* namespace before going back and marking each value (while still
* in the synchronized block on the namespace).. this ensures that
* we won't mark several values in an array before discovering that
* one of the values in a DBArrayField is already taken.</p>
*
* <p>The success of testmark() is no guarantee of a future
* successful mark() operation, of course, unless the testmark and
* mark operations are done within a synchronization block on the
* namespace.</p>
*
* @param editSet The transaction testing permission to claim value.
* @param value The unique value desired by editSet.
*/
public synchronized boolean testmark(DBEditSet editSet, Object value)
{
checkSchemaEditInProgress(false);
if (editSet == null || value == null)
{
throw new IllegalArgumentException();
}
DBNameSpaceHandle handle;
/* -- */
if (!uniqueHash.containsKey(value))
{
return true;
}
handle = (DBNameSpaceHandle) uniqueHash.get(value);
if (handle.isEditedByOtherTransaction(editSet))
{
return false;
}
if (handle.getShadowField() == null ||
(!editSet.isInteractive() && handle.getShadowFieldB() == null))
{
return true;
}
return false;
}
/**
* This method marks a value as being used in this namespace.
* Marked values are held in editSet's, which are free to shuffle
* these reserved values around during processing. The persistent
* fieldInvid and fieldId fields in the namespace handle object are
* used during unique value searches to do direct hash lookups
* without getting confused by any shuffling being performed by a
* current thread.
*
* @param editSet The transaction claiming the unique value <value>
* @param value The unique value that transaction editset is attempting to claim
* @param field The DBField which will take the unique value.
*/
public synchronized boolean mark(DBEditSet editSet, Object value, DBField field)
{
checkSchemaEditInProgress(false);
if (editSet == null || value == null || field == null)
{
throw new IllegalArgumentException();
}
DBNameSpaceHandle handle;
/* -- */
if (!uniqueHash.containsKey(value))
{
handle = new DBNameSpaceEditingHandle(editSet, null);
handle.setShadowField(field);
putHandle(value, handle);
remember(editSet, value);
return true;
}
handle = (DBNameSpaceHandle) uniqueHash.get(value);
if (handle.isEditedByOtherTransaction(editSet))
{
return false;
}
if (editSet.isInteractive())
{
if (!handle.isCheckedOut() || handle.getShadowField() != null)
{
return false;
}
handle.setShadowField(field);
return true;
}
else
{
if (!handle.isCheckedOut())
{
handle = handle.checkout(editSet);
putHandle(value, handle);
remember(editSet, value);
handle.setShadowField(handle.getPersistentField(editSet));
handle.setShadowFieldB(field);
return true;
}
else if (handle.getShadowField() == null)
{
handle.setShadowField(field);
return true;
}
else if (!field.matches(handle.getShadowField()) &&
(handle.getShadowFieldB() == null ||
field.matches(handle.getShadowFieldB())))
{
handle.setShadowFieldB(field);
return true;
}
}
return false;
}
/**
* <p>This method tests to see whether a value in the namespace can
* be marked as not in use. Such a marking is done to allow an
* editset to have the ability to juggle values in namespace
* associated fields without allowing another editset to acquire a
* value needed if the editset transaction is aborted.</p>
*
* <p>For array db fields, all elements in the array should be
* testunmark'ed in the context of a synchronized block on the
* namespace before actually unmarking all the values. See the
* comments in testmark() for the logic here. Note that
* testunmark() is less useful than testmark() because we really
* aren't expecting anything to prevent us from unmarking()
* something.</p>
*
* @param editSet The transaction that is determining whether value can be freed.
* @param value The unique value being tested.
*/
public synchronized boolean testunmark(DBEditSet editSet, Object value, DBField oldField)
{
checkSchemaEditInProgress(false);
if (oldField == null || editSet == null || value == null)
{
throw new IllegalArgumentException();
}
if (!uniqueHash.containsKey(value))
{
throw new RuntimeException("ASSERT: testunmark called on value '" + GHashtable.keyString(value) +
"' not in namespace: " + this.getName());
}
DBNameSpaceHandle handle = (DBNameSpaceHandle) uniqueHash.get(value);
if (handle.isEditedByOtherTransaction(editSet))
{
throw new RuntimeException("ASSERT: testunmark called on value '" + GHashtable.keyString(value) +
"' that namespace: " + this.getName() +
" believes is being edited by another transaction. Field is " + oldField);
}
if (!handle.matchesAnySlot(oldField))
{
throw new RuntimeException("ASSERT: testunmark called on value '" + GHashtable.keyString(value) +
"' that namespace: " + this.getName() + " believes is not in field " + oldField);
}
return true;
}
/**
* Used to mark a value as not used in the namespace. Unmarked
* values are not available for other threads / editset's until
* commit or abort is called on this namespace on behalf of this
* editset.
*
* @param editSet The transaction that is freeing value.
* @param value The unique value being tentatively marked as unused.
* @param oldField The old DBField that the namespace value is being
* unmarked for
*/
public synchronized boolean unmark(DBEditSet editSet, Object value, DBField oldField)
{
checkSchemaEditInProgress(false);
if (editSet == null || value == null || oldField == null)
{
throw new IllegalArgumentException();
}
if (!uniqueHash.containsKey(value))
{
throw new RuntimeException("ASSERT: unmark called on value '" + GHashtable.keyString(value) +
"' not in namespace: " + this.getName());
}
DBNameSpaceHandle handle = (DBNameSpaceHandle) uniqueHash.get(value);
if (handle.isEditedByOtherTransaction(editSet))
{
throw new RuntimeException("ASSERT: unmark called on value '" + GHashtable.keyString(value) +
"' that namespace: " + this.getName() +
" believes is being edited by another transaction. Field is " + oldField);
}
if (!handle.matchesAnySlot(oldField))
{
throw new RuntimeException("ASSERT: unmark called on value '" + GHashtable.keyString(value) +
"' that namespace: " + this.getName() + " believes is not in field " + oldField);
}
if (!handle.isCheckedOut())
{
handle = handle.checkout(editSet);
putHandle(value, handle);
remember(editSet, value);
return true;
}
if (!editSet.isInteractive())
{
if (!oldField.matches(handle.getShadowFieldB()) &&
!oldField.matches(handle.getShadowField()))
{
throw new RuntimeException("ASSERT: mismatched field in non-interactive unmark");
}
if (oldField.matches(handle.getShadowFieldB()))
{
// I really don't expect getShadowFieldB() to be equal to
// the oldField, given how the non-interactive xmlclient
// works, but we'll handle that case in the event we do
// have some very weird non-interactive client talking to
// us which decided to set a prospective mark and then
// clear it
handle.setShadowFieldB(null);
return true;
}
if (handle.getShadowFieldB() != null)
{
// promote B to A
handle.setShadowField(handle.getShadowFieldB());
handle.setShadowFieldB(null);
return true;
}
}
if (oldField.matches(handle.getShadowField()))
{
handle.setShadowField(null);
return true;
}
return false;
}
/**
* Method to checkpoint namespace changes made by a specific transaction
* so that state can be rolled back if necessary at a later time.
*
* @param editSet The transaction that needs to be checkpointed.
* @param name The name of the checkpoint to be marked.
*/
public synchronized void checkpoint(DBEditSet editSet, String name)
{
checkSchemaEditInProgress(false);
getTransactionRecord(editSet).pushCheckpoint(name, new DBNameSpaceCkPoint(this, editSet));
}
/**
* <p>Method to remove a checkpoint from this namespace's
* DBNameSpaceCkPoint hash. This is to be done when the calling
* code knows that it will no longer need to be able to rollback to
* the named checkpoint.</p>
*
* <p>This method really isn't very important, because when the
* transaction is committed or aborted, the checkpoints hashtable
* will be cleared of editSet anyway.</p>
*
* @param editSet The transaction that is requesting the checkpoint pop.
* @param name The name of the checkpoint to be popped.
*/
public synchronized void popCheckpoint(DBEditSet editSet, String name)
{
checkSchemaEditInProgress(false);
getTransactionRecord(editSet).popCheckpoint(name);
}
/**
* Method to rollback namespace changes made by a specific
* transaction to a checkpoint.
*
* @param editSet The transaction that needs to be checkpointed.
* @param name The name of the checkpoint to be rolled back to.
*
* @return false if the checkpoint could not be found.
*/
public synchronized boolean rollback(DBEditSet editSet, String name)
{
checkSchemaEditInProgress(false);
Object value;
Vector elementsToRemove = new Vector();
DBNameSpaceTransaction tRecord;
DBNameSpaceCkPoint point;
DBNameSpaceEditingHandle handle;
Enumeration en;
/* -- */
tRecord = getTransactionRecord(editSet);
// try to pop off the named checkpoint we're looking for
point = tRecord.popCheckpoint(name);
if (point == null)
{
throw new RuntimeException("ASSERT: couldn't find checkpoint for " + name + "\n" +
"In transaction " + editSet.description + "\n\n" +
"Currently registered checkpoints:\n" +
tRecord.getCheckpointStack());
}
en = tRecord.getReservedEnum();
while (en.hasMoreElements())
{
value = en.nextElement();
handle = (DBNameSpaceEditingHandle) uniqueHash.get(value);
if (!point.containsValue(value))
{
DBNameSpaceHandle oldHandle = handle.getOriginal();
if (oldHandle == null)
{
removeHandle(value);
}
else
{
putHandle(value, oldHandle);
}
elementsToRemove.addElement(value);
}
else
{
putHandle(value, point.getValueHandle(value));
}
}
for (Object element: elementsToRemove)
{
tRecord.forget(element);
}
return true;
}
/**
* <p>This method returns null if the given transaction doesn't have
* any shadowFieldB's outstanding. This is the desired case. If
* the given transaction has some values with shadowFieldB's set,
* that means that those unique values are left "bound" to more than
* one field, which is an error that non-interactive clients (the
* xmlclient) can run into.</p>
*
* <p>If we have such conflicts, we return a vector of namespace
* values that are in conflict at transaction commit time, else we
* return null, which indicates that this namespace has been
* verified.</p>
*/
public synchronized Vector<String> verify_noninteractive(DBEditSet editSet)
{
DBNameSpaceTransaction tRecord;
Enumeration en;
Object value;
DBNameSpaceEditingHandle handle;
Vector results = null;
/* -- */
tRecord = getTransactionRecord(editSet);
en = tRecord.getReservedEnum();
while (en.hasMoreElements())
{
value = en.nextElement();
handle = (DBNameSpaceEditingHandle) getHandle(value);
if (handle.getShadowFieldB() == null)
{
continue;
}
// we've got a shadowFieldB, which we ought not to have.
if (results == null)
{
results = new Vector();
}
if (handle.getShadowField() != null)
{
// "{0}, value in conflict = {1}"
results.addElement(ts.l("verify_noninteractive.template",
handle.getConflictString(), GHashtable.keyString(value)));
}
else
{
// we've got a conflict between a checked-in object in
// the datastore and an object we're creating or
// editing in the xml session. shadowFieldB() points
// to the in-xml session object.
String editingRefStr = String.valueOf(handle.getShadowFieldB());
DBField conflictField = handle.getPersistentField(editSet);
// "{0} conflicts with checked-in {1}"
String checkedInConflictStr = ts.l("verify_noninteractive.persistent_conflict",
String.valueOf(handle.getShadowFieldB()),
String.valueOf(conflictField));
// "{0}, value in conflict = {1}"
results.addElement(ts.l("verify_noninteractive.template",
checkedInConflictStr, GHashtable.keyString(value)));
}
}
return results;
}
/**
* <p>Method to put the editSet's current namespace modifications
* into final effect and to make any abandoned values available for
* other namespaces.</p>
*
* <p>Note that a NameSpace should really never fail here. We
* assume that all NameSpace management code up to this point has
* functioned properly.. at this point, the EditSet has already
* committed changes to the DBStore and to any external processes,
* we're just doing paperwork at this point.</p>
*
* @param editSet The transaction being committed.
*/
public synchronized void commit(DBEditSet editSet)
{
checkSchemaEditInProgress(false);
DBNameSpaceTransaction tRecord;
Enumeration en;
Object value;
DBNameSpaceEditingHandle handle;
/* -- */
tRecord = getTransactionRecord(editSet);
en = tRecord.getReservedEnum();
// loop over the values in the namespace that were changed or
// affected by this editset, and commit them into the database,
// copying the shadowField DBField into the handle's permanent
// field pointer, or getting rid of them entirely if they are not
// in use anymore
while (en.hasMoreElements())
{
value = en.nextElement();
handle = (DBNameSpaceEditingHandle) getHandle(value);
if (!handle.isEditedByUs(editSet))
{
if (debug)
{
Ganymede.debug("DBNameSpace.commit(): trying to commit handle for value " + GHashtable.keyString(value) +
" that is being edited by another transaction.");
}
continue;
}
if (handle.getShadowFieldB() != null)
{
throw new RuntimeException("ASSERT: lingering shadowFieldB at commit time for transaction " +
editSet.session.getKey());
}
DBNameSpaceHandle newHandle = handle.getNewHandle();
if (newHandle == null)
{
removeHandle(value);
}
else
{
putHandle(value, newHandle);
}
}
tRecord.cleanup();
transactions.remove(editSet);
}
/**
* Method to revert an editSet's namespace modifications to its
* original state. Used when a transaction is rolled back.
*
* @param editSet The transaction whose claimed values in this
* namespace need to be freed.
*/
public synchronized void abort(DBEditSet editSet)
{
checkSchemaEditInProgress(false);
DBNameSpaceTransaction tRecord;
Enumeration en;
Object value;
DBNameSpaceEditingHandle handle;
/* -- */
tRecord = getTransactionRecord(editSet);
en = tRecord.getReservedEnum();
// loop over the values in the namespace that were changed or affected
// by this editset, and revert them to their checked-in status, or
// get rid of them entirely if they weren't allocated in this namespace
// before this transaction allocated them
while (en.hasMoreElements())
{
value = en.nextElement();
handle = (DBNameSpaceEditingHandle) getHandle(value);
if (!handle.isEditedByUs(editSet))
{
if (debug)
{
Ganymede.debug("DBNameSpace.abort(): trying to abort handle for value" + GHashtable.keyString(value) +
" that is being edited by another transaction.");
}
continue;
}
DBNameSpaceHandle oldHandle = handle.getOriginal();
if (oldHandle == null)
{
removeHandle(value);
}
else
{
putHandle(value, oldHandle);
}
}
tRecord.cleanup();
transactions.remove(editSet);
}
public String toString()
{
return name;
}
/**
* This method prepares this DBNameSpace for changes to be made
* during schema editing. A back-up copy of the uniqueHash is made,
* to allow reversion in case the schema edit is aborted. Between
* schemaEditCheckout() and either schemaEditCommit() or
* schemaEditAbort(), the schemaEditRegister() and
* schemaEditUnregister() methods may be called to handle
* attaching/detaching fields to the namespace.
*/
public synchronized void schemaEditCheckout()
{
checkSchemaEditInProgress(false);
this.saveHash = this.uniqueHash;
uniqueHash = new GHashtable(saveHash.size(), caseInsensitive);
Enumeration en = saveHash.keys();
while (en.hasMoreElements())
{
Object key = en.nextElement();
DBNameSpaceHandle handle = (DBNameSpaceHandle) saveHash.get(key);
DBNameSpaceHandle handleCopy = new DBNameSpaceHandle(handle);
if (handleCopy.isCheckedOut())
{
// "Error, non-null handle owner found during copy of namespace {0} for key: {1}."
throw new RuntimeException(ts.l("schemaEditCheckout.non_null_owner",
this.toString(),
key));
}
if (handleCopy.getShadowField() != null)
{
// "Error, non-null handle shadowField found during copy of namespace {0} for key {1}."
throw new RuntimeException(ts.l("schemaEditCheckout.non_null_shadowField",
this.toString(),
key));
}
putHandle(key, handleCopy);
}
}
/**
* Returns true if this namespace has already been checked out for schema editing.
*/
public synchronized boolean isSchemaEditInProgress()
{
return (this.saveHash != null);
}
public synchronized void checkSchemaEditInProgress(boolean expecting)
{
if (expecting)
{
if (this.saveHash == null)
{
// "Can''t perform, not in schema edit."
throw new RuntimeException(ts.l("global.not_editing"));
}
}
else
{
if (this.saveHash != null)
{
// "Can''t perform, still in schema edit."
throw new RuntimeException(ts.l("global.editing"));
}
}
}
/**
* This method locks in any changes made after schema editing is complete.
*/
public synchronized void schemaEditCommit()
{
if (this.saveHash == null)
{
return;
}
this.saveHash = null;
}
/**
* This method aborts any changes made during schema editing.
*/
public synchronized void schemaEditAbort()
{
if (this.saveHash == null)
{
return;
}
this.uniqueHash = this.saveHash;
this.saveHash = null;
}
/**
* This method links the given value to the specified field. If the
* value has already been allocated, schemaEditRegister will return false
* and the value won't be attached to the field in question.
*
* @return true if the value could be registered with the specified field,
* false otherwise
*/
public synchronized boolean schemaEditRegister(Object value, DBField field)
{
checkSchemaEditInProgress(true);
if (uniqueHash.containsKey(value))
{
return false;
}
putHandle(value, new DBNameSpaceHandle(field));
return true;
}
/**
* This method unlinks a single item from this namespace index, if and only
* if the value is associated with the object and field id specified.
*
* @return true if the value was associated with the object and field specified,
* false otherwise. If false is returned, no value was removed from this
* namespace.
*/
public synchronized boolean schemaEditUnregister(Object value, Invid objid, short field)
{
checkSchemaEditInProgress(true);
DBNameSpaceHandle handle = (DBNameSpaceHandle) uniqueHash.get(value);
if (handle == null)
{
return false;
}
if (!handle.matches(objid, field))
{
return false;
}
removeHandle(value);
return true;
}
/**
* This method unlinks all values that are associated with the specified
* object type and field id from this namespace.
*/
public synchronized void schemaEditUnregister(short objectType, short fieldId)
{
checkSchemaEditInProgress(true);
Vector elementsToRemove = new Vector();
Enumeration en = this.uniqueHash.keys();
while (en.hasMoreElements())
{
Object value = en.nextElement();
DBNameSpaceHandle handle = (DBNameSpaceHandle) this.uniqueHash.get(value);
if (handle.matchesFieldType(objectType, fieldId))
{
elementsToRemove.addElement(value);
}
}
for (Object element: elementsToRemove)
{
removeHandle(element);
}
}
/**
* <p>This method is designed for doing conflict detection between
* this DBNameSpace and another DBNameSpace that we might be looking
* at merging into this namespace. This is usually necessary when a
* Ganymede adopter is collapsing two namespaces into one.</p>
*
* <p>In addition to returning true if there are values in conflict
* between the two namespaces, or false if conflicts exist, a report
* of the conflicting values will be written to the Ganymede admin
* console and the Ganymede server's stdout.</p>
*/
public synchronized boolean findConflicts(DBNameSpace otherSpace)
{
boolean success = true;
Enumeration en = this.uniqueHash.keys();
while (en.hasMoreElements())
{
Object value = en.nextElement();
if (!otherSpace.containsKey(value))
{
continue;
}
success = false;
DBField thisField = this.lookupPersistent(value);
DBField otherField = otherSpace.lookupPersistent(value);
if (thisField == null || otherField == null)
{
// oops, the conflict isn't really between persistent
// registrations across the two name spaces.. never mind.
continue;
}
DBObject thisObject = thisField.getOwner();
DBObject otherObject = otherField.getOwner();
Ganymede.debug("Namespace " + this.getName() + " has a conflict for value " + value.toString() +
" in " + thisObject.getTypeName() + " " + thisObject.getLabel() + "'s " +
thisField.getName() + " field, and in " + otherObject.getTypeName() + " " +
otherObject.getLabel() + "'s " + otherField.getName() + " field.");
}
return success;
}
/**
* Performs some assertinon checking, then places the given
* DBNameSpaceHandle in the uniqueHash using value as they key.
*/
private void putHandle(Object value, DBNameSpaceHandle handle)
{
assert (handle instanceof DBNameSpaceEditingHandle) || handle.isPersisted();
uniqueHash.put(value, handle);
}
/**
* Returns the DBNameSpaceHandle associated with the value
* in this namespace, or null if the value is not allocated in
* this namespace.
*/
private DBNameSpaceHandle getHandle(Object value)
{
return (DBNameSpaceHandle) uniqueHash.get(value);
}
/**
* <p>This method returns the {@link
* arlut.csd.ganymede.server.DBNameSpace.DBNameSpaceTransaction
* DBNameSpaceTransaction} associated with the given transaction,
* creating one if one was not previously so associated.</p>
*
* <p>This method will always return a valid DBNameSpaceTransaction
* record.</p>
*/
private synchronized DBNameSpaceTransaction getTransactionRecord(DBEditSet transaction)
{
DBNameSpaceTransaction transRecord = this.transactions.get(transaction);
if (transRecord == null)
{
transRecord = new DBNameSpaceTransaction(transaction, caseInsensitive);
this.transactions.put(transaction, transRecord);
}
return transRecord;
}
/**
* <p>Remember that this editSet has changed the location/status of
* this value.</p>
*
* <p>This is a private convenience method.</p>
*
* <p>This method associates the value with the given editset in a
* stored transaction record, so that we can rollback the namespace
* to a fixed state later.</p>
*/
private void remember(DBEditSet editSet, Object value)
{
getTransactionRecord(editSet).remember(value);
}
/*----------------------------------------------------------------------------
nested class
DBNameSpaceTransaction
----------------------------------------------------------------------------*/
/**
* This nested class holds information associated with an active
* transaction (a {@link arlut.csd.ganymede.server.DBEditSet
* DBEditSet}) in care of a {@link
* arlut.csd.ganymede.server.DBNameSpace DBNameSpace}.
*/
static class DBNameSpaceTransaction {
private NamedStack<DBNameSpaceCkPoint> checkpointStack;
private GHashtable reservedValues;
private DBEditSet transaction;
/* -- */
DBNameSpaceTransaction(DBEditSet transaction, boolean caseInsensitive)
{
this.transaction = transaction;
this.reservedValues = new GHashtable(caseInsensitive);
this.checkpointStack = new NamedStack<DBNameSpaceCkPoint>();
}
public synchronized void remember(Object value)
{
if (reservedValues.containsKey(value))
{
Ganymede.logAssert("ASSERT: DBNameSpaceTransaction.remember(): transaction " + transaction +
" already contains value " + GHashtable.keyString(value));
return;
}
reservedValues.put(value, value);
}
public synchronized void forget(Object value)
{
if (!reservedValues.containsKey(value))
{
Ganymede.logAssert("ASSERT: DBNameSpaceTransaction.forget(): transaction " + transaction +
" does not contain value " + GHashtable.keyString(value));
return;
}
reservedValues.remove(value);
}
/**
* This method dissolves everything referenced by this DBNameSpaceTransaction,
* in order to facilitate speedy garbage collection.
*/
public synchronized void cleanup()
{
if (checkpointStack != null)
{
while (checkpointStack.size() != 0)
{
DBNameSpaceCkPoint ckpoint = checkpointStack.pop();
ckpoint.cleanup();
}
checkpointStack = null;
}
if (reservedValues != null)
{
reservedValues.clear();
reservedValues = null;
}
transaction = null;
}
public Enumeration getReservedEnum()
{
return reservedValues.elements();
}
public GHashtable getReservedHash()
{
return reservedValues;
}
public DBEditSet getDBEditSet()
{
return transaction;
}
public void pushCheckpoint(String name, DBNameSpaceCkPoint cPoint)
{
checkpointStack.push(name, cPoint);
}
public DBNameSpaceCkPoint popCheckpoint(String name)
{
DBNameSpaceCkPoint point = checkpointStack.pop(name);
if (point == null)
{
Ganymede.logAssert("ASSERT: DBNameSpaceTransaction.popCheckpoint(): transaction " + transaction +
" does not contain a checkpoint named " + name);
}
return point;
}
public NamedStack getCheckpointStack()
{
return checkpointStack;
}
}
/*----------------------------------------------------------------------------
nested class
DBNameSpaceCkPoint
----------------------------------------------------------------------------*/
/**
* This nested class holds checkpoint information associated with
* an active transaction (a {@link
* arlut.csd.ganymede.server.DBEditSet DBEditSet}) in care of a
* {@link arlut.csd.ganymede.server.DBNameSpace DBNameSpace}.
*/
static class DBNameSpaceCkPoint {
GHashtable reserved;
Hashtable uniqueHash;
/* -- */
DBNameSpaceCkPoint(DBNameSpace space, DBEditSet transaction)
{
DBNameSpaceTransaction tRecord = space.getTransactionRecord(transaction);
reserved = tRecord.getReservedHash();
if (reserved == null)
{
return;
}
// clone the hash to avoid sync problems with other threads
reserved = (GHashtable) reserved.clone();
if (reserved.size() > 0)
{
uniqueHash = new Hashtable(reserved.size(), 1.0f);
}
else
{
uniqueHash = new Hashtable(10);
}
// now copy our hash to preserve the namespace handles
for (Object value: reserved.values())
{
DBNameSpaceHandle handle = space.getHandle(value);
// XXX clone() rather than use a copy constructor because
// some of our values will be DBNameSpaceEditingHandles
// rather than DBNameSpaceHandles, and we need to get the
// subclass properly.
handle = (DBNameSpaceHandle) handle.clone();
uniqueHash.put(value, handle);
}
}
public boolean containsValue(Object value)
{
return reserved.containsKey(value);
}
public DBNameSpaceHandle getValueHandle(Object value)
{
return (DBNameSpaceHandle) uniqueHash.get(value);
}
/**
* This method dissolves everything referenced by this DBNameSpaceCkPoint
* in order to facilitate speedy garbage collection.
*/
public synchronized void cleanup()
{
if (reserved != null)
{
reserved.clear();
reserved = null;
}
if (uniqueHash != null)
{
uniqueHash.clear();
uniqueHash = null;
}
}
}
}
/*------------------------------------------------------------------------------
class
DBNameSpaceHandle
------------------------------------------------------------------------------*/
/**
* This class is intended to be the targets of elements of a name
* space's {@link arlut.csd.ganymede.server.DBNameSpace#uniqueHash
* uniqueHash}. The fields in this class are used to keep track of
* who currently 'owns' a given value, and whether or not there is
* actually any field in the namespace that really contains that
* value.
*/
class DBNameSpaceHandle implements Cloneable {
/**
* <p>So that the namespace hash can be used as an index,
* persistentFieldInvid always points to the object that contained
* the field that contained this value at the time this field was
* last committed in a transaction.</p>
*
* <p>persistentFieldInvid will be null if the value pointing to
* this handle has not been committed into the database outside of
* an active transaction.</p>
*/
private Invid persistentFieldInvid = null;
/**
* If this handle is associated with a value that has been
* checked into the database, persistentFieldId will be the field
* number for the field that holds that value in the database,
* within the object referenced by persistentFieldInvid.
*/
private short persistentFieldId = -1;
/* -- */
/**
* Constructor used by the system to originate a value when reading
* from the database.
*/
public DBNameSpaceHandle(DBField field)
{
setPersistentField(field);
}
/**
* Copy constructor
*/
public DBNameSpaceHandle(DBNameSpaceHandle orig)
{
this.persistentFieldId = orig.persistentFieldId;
this.persistentFieldInvid = orig.persistentFieldInvid;
}
/**
* <p>This method creates a new editable DBNameSpaceEditingHandle
* that will revert to this handle if the DBEditSet is aborted.</p>
*
* <p>The checked out handle is returned with a null shadowField and
* shadowFieldB, as would be appropriate if a user was unmarking a
* value in the namespace uniqueHash.</p>
*
* <p>If the handle is being checked out by a non-interactive
* xmlclient that needs to address the shadowFieldB member, it will
* need to be sure to set the shadowField() to point to the
* appropriate field at check out time.</p>
*/
public DBNameSpaceEditingHandle checkout(DBEditSet editSet)
{
DBNameSpaceHandle newHandle = new DBNameSpaceEditingHandle(editSet, this);
newHandle.persistentFieldInvid = persistentFieldInvid;
newHandle.persistentFieldId = persistentFieldId;
return (DBNameSpaceEditingHandle) newHandle;
}
/**
* This method returns true if this handle has been checked out of
* the DBNameSpace.uniqueHash by a transaction, or false otherwise.
*/
public boolean isCheckedOut()
{
return false;
}
/**
* <p>This method returns true if the namespace-managed value that
* this handle is associated with is held in a committed object in
* the Ganymede data store.</p>
*
* <p>If this method returns false, that means that this handle must
* be associated with a field in an active DBEditSet's transaction
* set, or else we wouldn't have a handle for it.</p>
*/
public boolean isPersisted()
{
return persistentFieldInvid != null;
}
/**
* This method associates this value with a DBField that is
* persisted (or will be persisted?) in the Ganymede persistent
* store.
*/
public void setPersistentField(DBField field)
{
if (field != null)
{
persistentFieldInvid = field.getOwner().getInvid();
persistentFieldId = field.getID();
}
else
{
persistentFieldInvid = null;
persistentFieldId = -1;
}
}
/**
* If the value that this handle is associated with is stored in
* the Ganymede server's persistent data store (i.e., that this
* handle is associated with a field in an already-committed
* object), this method will return a pointer to the DBField that
* contains this handle's value in the committed data store.
*/
public DBField getPersistentField()
{
return getPersistentField((DBSession) null);
}
/**
* <p>If the value that this handle is associated with is stored in
* the Ganymede server's persistent data store (i.e., that this
* handle is associated with a field in an already-committed
* object), this method will return a pointer to the DBField that
* contains this handle's value in the committed data store.</p>
*
* <p>Note that if the DBEditSet passed in is currently editing the
* object which is identified by persistentFieldInvid, the DBField
* returned will be the editable version of the field from the
* DBEditObject the session is working with. This may be something
* of a surprise, as the field returned may not actually contain the
* value sought.</p>
*/
public DBField getPersistentField(DBEditSet editSet)
{
return getPersistentField(editSet.session);
}
/**
* <p>If the value that this handle is associated with is stored in
* the Ganymede server's persistent data store (i.e., that this
* handle is associated with a field in an already-committed
* object), this method will return a pointer to the DBField that
* contains this handle's value in the committed data store.</p>
*
* <p>Note that if the GanymedeSession passed in is currently
* editing the object which is identified by persistentFieldInvid,
* the DBField returned will be the editable version of the field
* from the DBEditObject the session is working with. This may be
* something of a surprise, as the field returned may not actually
* contain the value sought.</p>
*/
public DBField getPersistentField(GanymedeSession gsession)
{
return getPersistentField(gsession.getDBSession());
}
/**
* <p>If the value that this handle is associated with is stored in
* the Ganymede server's persistent data store (i.e., that this
* handle is associated with a field in an already-committed
* object), this method will return a pointer to the DBField that
* contains this handle's value in the committed data store.</p>
*
* <p>Note that if the DBSession passed in is currently editing the
* object which is identified by persistentFieldInvid, the DBField
* returned will be the editable version of the field from the
* DBEditObject the session is working with. This may be something
* of a surprise, as the field returned may not actually contain the
* value sought.</p>
*/
public DBField getPersistentField(DBSession session)
{
if (persistentFieldInvid == null)
{
return null;
}
if (session != null)
{
DBObject _obj = session.viewDBObject(persistentFieldInvid);
if (_obj == null)
{
return null;
}
return _obj.getField(persistentFieldId);
}
else
{
// during start-up, before we have a session available
DBObject _obj = Ganymede.db.viewDBObject(persistentFieldInvid);
if (_obj == null)
{
return null;
}
return _obj.getField(persistentFieldId);
}
}
/**
* This method returns true if the namespace-constrained value
* controlled by this handle is being edited by the GanymedeSession
* provided.
*/
public boolean isEditedByUs(GanymedeSession session)
{
return false;
}
/**
* This method returns true if the namespace-constrained value
* controlled by this handle is being edited by the transaction
* provided.
*/
public boolean isEditedByUs(DBEditSet editSet)
{
return false;
}
/**
* This method returns true if the namespace-constrained value
* controlled by this handle is being edited by some active
* transaction other than the editSet passed in.
*/
public boolean isEditedByOtherTransaction(DBEditSet editSet)
{
return false;
}
/**
* If this namespace-managed value is being edited in an active
* Ganymede transaction, this method may be used to set a pointer to
* the editable DBField which contains the constrained value in the
* active transaction.
*/
public void setShadowField(DBField newShadow)
{
throw new UnsupportedOperationException();
}
/**
* If this namespace-managed value is being edited in an active
* Ganymede transaction, this method will return a pointer to the
* editable DBField which contains the constrained value in the active
* transaction.
*/
public DBField getShadowField()
{
return null;
}
/**
* <p>If this namespace-managed value is being edited in an active,
* non-interactive Ganymede transaction, this method may be used to
* set a pointer to the editable DBField which aspires to contain
* the constrained value in the active transaction.</p>
*
* <p>This is the 'B' shadowField because it is not a firm
* association, and cannot be one unless and until the original
* persistent field that contains the constrained value is made to
* release the value. At the time the constrained value is released
* from the earlier field, shadowField will be set to shadowFieldB,
* and shadowFieldB will be cleared.</p>
*/
public void setShadowFieldB(DBField newShadow)
{
throw new UnsupportedOperationException();
}
/**
* If this namespace-managed value is being edited in an active,
* non-interactive Ganymede transaction, this method will return a
* pointer to the editable DBField which is proposed to contain the
* constrained value once the existing use of the value is cleared.
*/
public DBField getShadowFieldB()
{
return null;
}
/**
* This method is used to verify that this handle points to the same
* field as the one specified by the parameter list.
*/
public boolean matches(DBField field)
{
return (this.matches(field.getOwner().getInvid(), field.getID()));
}
/**
* This method is used to verify that this handle points to the same
* field as the one specified by the parameter list.
*/
public boolean matches(Invid persistentFieldInvid, short persistentFieldId)
{
return (this.persistentFieldInvid == persistentFieldInvid) &&
(this.persistentFieldId == persistentFieldId);
}
/**
* This method is used to verify that this handle points to the same
* kind of field as the one specified by the parameter list.
*/
public boolean matchesFieldType(short objectType, short persistentFieldId)
{
return (this.persistentFieldInvid.getType() == objectType) &&
(this.persistentFieldId == persistentFieldId);
}
/**
* Returns true if the given field is associated with this handle in
* any of the persistent, transaction-local, or xml transaction
* secondary field slots.
*/
public boolean matchesAnySlot(DBField field)
{
assert field != null;
return this.matches(field);
}
/**
* Returns true if this handle has been reserved during an editing
* transaction.
*/
public boolean isReserved()
{
return false;
}
/**
* Used to mark this handle as being reserved by the editing
* transaction.
*/
public void setReserved(boolean reserved)
{
throw new UnsupportedOperationException();
}
/**
* We want to allow cloning.
*/
public Object clone()
{
try
{
return super.clone();
}
catch (CloneNotSupportedException ex)
{
throw new RuntimeException(ex.getMessage());
}
}
}
/*------------------------------------------------------------------------------
class
DBNameSpaceEditingHandle
------------------------------------------------------------------------------*/
class DBNameSpaceEditingHandle extends DBNameSpaceHandle {
/**
* TranslationService object for handling string localization in
* the Ganymede server.
*/
static final TranslationService ts =
TranslationService.getTranslationService("arlut.csd.ganymede.server.DBNameSpaceEditingHandle");
// ---
/**
* <p>The DBNameSpaceHandle that this handle is replacing during value
* manipulation by a transaction. If the transaction's namespace
* manipulations are cancelled or rolled back, this handle will be
* take this editing handle's place in the namespace's uniqueHash.</p>
*/
private DBNameSpaceHandle original;
/**
* <p>If editingTransaction is null, that means that the field
* containing the value tracked by this handle is 'checked in' to
* the datastore.</p>
*
* <p>If editingTransaction is not null, a transaction has checked the
* value tracked by this handle out for manipulation, and no other
* transaction may mess with that value's binding until the first
* transaction has released this handle.</p>
*/
private DBEditSet editingTransaction;
/**
* <p>If this handle is currently being edited by an editset,
* shadowField points to the field in the transaction that contains
* this value. If the transaction is committed, the DBField pointer
* in shadowField will be transferred to persistentFieldInvid and
* persistentFieldId. If this value is not being manipulated by a
* transaction, shadowField will be equal to null.</p>
*/
private DBField shadowField;
/**
* <p>Non-interactive transactions need to be able to shuffle
* namespace values between two fields in the data store, even if
* the operation to mark the unique value for association with a
* second field is done before the operation to unlink the unique
* value from the persistently stored field is done.</p>
*
* <p>To support this, we have both shadowField and shadowFieldB,
* along the lines of the A-B-C values used in swapping values
* between two memory locations with the aid of a third.</p>
*
* <p>This is the 'B' shadowField because it is not a firm
* association, and cannot be one unless and until the original
* persistent field that contains the constrained value is made to
* release the value. At the time the constrained value is released
* from the earlier field, shadowField will be set to shadowFieldB,
* and shadowFieldB will be cleared.</p>
*/
private DBField shadowFieldB;
/**
* If true, this value has been reserved.
*/
private boolean reserved;
/* -- */
/**
* Constructor used by a transaction marking a value that is new to
* the namespace.
*/
public DBNameSpaceEditingHandle(DBEditSet owner, DBNameSpaceHandle original)
{
super((DBField) null);
this.editingTransaction = owner;
this.original = original;
}
/**
* This method returns true if this handle has been checked out of
* the DBNameSpace.uniqueHash by a transaction, or false otherwise.
*/
public boolean isCheckedOut()
{
return true;
}
/**
* <p>This method creates a new editable DBNameSpaceEditingHandle
* that will revert to this handle if the DBEditSet is aborted.</p>
*
* <p>The checked out handle is returned with a null shadowField and
* shadowFieldB, as would be appropriate if a user was unmarking a
* value in the namespace uniqueHash.</p>
*
* <p>If the handle is being checked out by a non-interactive xmlclient
* that needs to address the shadowFieldB member, it will need to be
* sure to set the shadowField() to point to the appropriate field
* at check out time.</p>
*/
public DBNameSpaceEditingHandle checkout(DBEditSet editSet)
{
throw new UnsupportedOperationException();
}
/**
* This method returns the DBNameSpaceHandle that should be used to
* replace this DBNameSpaceHandle if this transaction is committed.
*/
public DBNameSpaceHandle getNewHandle()
{
if (getShadowField() == null)
{
return null;
}
else
{
return new DBNameSpaceHandle(getShadowField());
}
}
/**
* This method returns the DBNameSpaceHandle that should be used to
* replace this DBNameSpaceHandle if this transaction is aborted.
*/
public DBNameSpaceHandle getOriginal()
{
return original;
}
/**
* This method returns true if the namespace-constrained value
* controlled by this handle is being edited by the GanymedeSession
* provided.
*/
public boolean isEditedByUs(GanymedeSession session)
{
return session.getDBSession().getEditSet() == editingTransaction;
}
/**
* This method returns true if the namespace-constrained value
* controlled by this handle is being edited by the transaction
* provided.
*/
public boolean isEditedByUs(DBEditSet editSet)
{
return editSet == editingTransaction;
}
/**
* This method returns true if the namespace-constrained value
* controlled by this handle is being edited by some active
* transaction other than the editSet passed in.
*/
public boolean isEditedByOtherTransaction(DBEditSet editSet)
{
return editSet != editingTransaction;
}
/**
* Returns true if this handle has been reserved during an editing
* transaction.
*/
public boolean isReserved()
{
return this.reserved;
}
/**
* Used to mark this handle as being reserved by the editing
* transaction.
*/
public void setReserved(boolean reserved)
{
this.reserved = reserved;
}
/**
* If this namespace-managed value is being edited in an active
* Ganymede transaction, this method may be used to set a pointer to
* the editable DBField which contains the constrained value in the
* active transaction.
*/
public void setShadowField(DBField newShadow)
{
shadowField = newShadow;
}
/**
* If this namespace-managed value is being edited in an active
* Ganymede transaction, this method will return a pointer to the
* editable DBField which contains the constrained value in the active
* transaction.
*/
public DBField getShadowField()
{
return shadowField;
}
/**
* <p>If this namespace-managed value is being edited in an active,
* non-interactive Ganymede transaction, this method may be used to
* set a pointer to the editable DBField which aspires to contain
* the constrained value in the active transaction.</p>
*
* <p>This is the 'B' shadowField because it is not a firm
* association, and cannot be one unless and until the original
* persistent field that contains the constrained value is made to
* release the value. At the time the constrained value is released
* from the earlier field, shadowField will be set to shadowFieldB,
* and shadowFieldB will be cleared.</p>
*/
public void setShadowFieldB(DBField newShadow)
{
shadowFieldB = newShadow;
}
/**
* If this namespace-managed value is being edited in an active,
* non-interactive Ganymede transaction, this method will return a
* pointer to the editable DBField which is proposed to contain the
* constrained value once the existing use of the value is cleared.
*/
public DBField getShadowFieldB()
{
return shadowFieldB;
}
/**
* Returns true if the given field is associated with this handle in
* any of the persistent, transaction-local, or xml transaction
* secondary field slots.
*/
public boolean matchesAnySlot(DBField field)
{
assert field != null;
return (field == shadowField) || (field == shadowFieldB) || this.matches(field);
}
/**
* This helper method is intended to report when a non-interactive
* xml session has two objects checked out for editing with a
* conflicting value at transaction commit time.
*/
public String getConflictString()
{
// "Conflict: {0} and {1}"
return ts.l("getConflictString.template",
String.valueOf(shadowField), String.valueOf(shadowFieldB));
}
public String toString()
{
StringBuilder result = new StringBuilder();
if (editingTransaction != null)
{
result.append("editingTransaction == " + editingTransaction.toString());
}
if (result.length() != 0)
{
result.append(", ");
}
if (shadowField != null)
{
result.append(", shadowField == " + shadowField.toString());
}
if (shadowFieldB != null)
{
result.append(", shadowFieldB == " + shadowFieldB.toString());
}
return result.toString();
}
}