/*
Wotonomy: OpenStep design patterns for pure Java applications.
Copyright (C) 2001 Michael Powers
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, see http://www.gnu.org
*/
package net.wotonomy.control;
import java.lang.ref.WeakReference;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import net.wotonomy.foundation.NSArray;
import net.wotonomy.foundation.NSDictionary;
import net.wotonomy.foundation.NSMutableArray;
import net.wotonomy.foundation.NSMutableDictionary;
import net.wotonomy.foundation.NSNotification;
import net.wotonomy.foundation.NSNotificationCenter;
import net.wotonomy.foundation.NSRunLoop;
import net.wotonomy.foundation.NSSelector;
import net.wotonomy.foundation.internal.WotonomyException;
// swing dependency for undo manager
//import javax.swing.undo.UndoManager;
/**
* EOEditingContext provides transactional support for
* fetching, editing, and committing changes made on a
* collection of objects to a parent object store.
*
* EOEditingContext is itself a subclass of EOObjectStore,
* and this means that EOEditingContexts can use other
* EOEditingContexts as their parent. However, there
* still must exist an EOObjectStore as the root of the
* editing hierarchy that can maintain persistent state.
*
* @author michael@mpowers.net
* @author $Author: cgruber $
* @version $Revision: 894 $
*/
public class EOEditingContext
extends EOObjectStore
implements EOObserving
{
/**
* Key for the NSNotification posted after this editing context
* saves changes. Object of the notification will be this editing
* context, and user info will contain InsertedKey, UpdatedKey,
* and DeletedKey (keys are defined in EOObjectStore).
*/
public static final String
EditingContextDidSaveChangesNotification =
"EOEditingContextDidSaveChangesNotification";
/**
* Key for the NSNotification posted after this editing context
* observes changes. Object of the notification will be this editing
* context, and user info will contain InsertedKey, UpdatedKey, InvalidatedKey,
* and DeletedKey (keys are defined in EOObjectStore), however
* the objects in the corresponding Lists will be the actual
* objects, not their ids.
*/
public static final String
ObjectsChangedInEditingContextNotification =
"EOObjectsChangedInEditingContextNotification";
/**
* The default run loop ordering processes recent changes
* before delayed observers are notified and before dispatching
* the AWT event queue.
*/
public static int
EditingContextFlushChangesRunLoopOrdering = 300000;
private static NSSelector runLaterSelector =
new NSSelector( "flushRecentChanges",
new Class[] { Object.class } );
private static EOObjectStore defaultParentObjectStore = null;
private static double defaultFetchTimestampLag = 0;
private static boolean retainsRegisteredObjects = true;
private EOObjectStore parentStore;
private WeakReference delegate;
private WeakReference messageHandler;
private List editorSet;
private double fetchTimestamp;
private boolean lockBeforeModify;
private boolean propagateDeletesAfterEvent;
private boolean stopValidationAfterError;
private NSMutableArray insertedObjects;
private NSMutableArray insertedObjectsBuffer;
private NSArray insertedObjectsProxy;
private NSMutableArray updatedObjects;
private NSMutableArray updatedObjectsBuffer;
private NSArray updatedObjectsProxy;
private NSMutableArray deletedObjects;
private NSMutableArray deletedObjectsBuffer;
private NSArray deletedObjectsProxy;
private NSMutableArray deletedIDsBuffer;
private NSMutableArray invalidatedObjectsBuffer;
private NSMutableArray invalidatedIDsBuffer;
private Registrar registrar;
// private UndoManager undoManager;
// so we don't have to trouble EOObserverCenter
private boolean ignoreChanges;
// for delayed handling of processRecentChanges
private boolean willRunLater;
// for handling of notifications posted
// while we're in the saveChanges method
private boolean isInvalidating;
// for i18n or other customization
static protected String MessageChangeConflict =
"Another user changed an object you are editing: ";
/**
* Default constructor creates a new editing context
* that uses the default object store. If the default
* object store has not been set, an exception is thrown.
*/
public EOEditingContext()
{
this( defaultParentObjectStore() );
}
/**
* Creates a new editing context that uses the specified
* object store as its parent object store.
*/
public EOEditingContext( EOObjectStore anObjectStore )
{
if ( anObjectStore == null )
{
throw new IllegalArgumentException(
"A parent object store must be specified." );
}
parentStore = anObjectStore;
delegate = null;
messageHandler = null;
editorSet = new LinkedList();
fetchTimestamp = 0;
lockBeforeModify = false;
propagateDeletesAfterEvent = true;
stopValidationAfterError = true;
insertedObjects = new NSMutableArray();
insertedObjectsBuffer = new NSMutableArray();
insertedObjectsProxy = NSArray.arrayBackedByList( insertedObjects );
updatedObjects = new NSMutableArray();
updatedObjectsBuffer = new NSMutableArray();
updatedObjectsProxy = NSArray.arrayBackedByList( updatedObjects );
deletedObjects = new NSMutableArray();
deletedObjectsBuffer = new NSMutableArray();
deletedObjectsProxy = NSArray.arrayBackedByList( deletedObjects );
deletedIDsBuffer = new NSMutableArray();
invalidatedObjectsBuffer = new NSMutableArray();
invalidatedIDsBuffer = new NSMutableArray();
if ( instancesRetainRegisteredObjects() )
{
registrar = new Registrar( this );
}
else
{
registrar = new WeakRegistrar( this );
}
ignoreChanges = false;
willRunLater = false;
isInvalidating = false;
// create undo manager
//TODO: this should be NSUndoManager
// undoManager = new UndoManager();
// register for notifications
NSSelector handleNotification =
new NSSelector( "handleNotification",
new Class[] { NSNotification.class } );
// any from parent store
NSNotificationCenter.defaultCenter().addObserver(
this, handleNotification, null, parentStore );
// global id change from any
NSNotificationCenter.defaultCenter().addObserver(
this, handleNotification, EOGlobalID.GlobalIDChangedNotification, null );
//new net.wotonomy.ui.swing.NotificationInspector( null, parentStore );
}
/**
* Registers the specified object as an editor for this
* context. The object is expected to implement
* EOEditingContext.Editor.
*/
public void addEditor ( Object anEditor )
{
if ( anEditor == null ) return;
editorSet.add( new WeakReference( anEditor ) );
}
/**
* Returns a read-only List of objects associated with the object
* with the specified id for the specified property
* relationship, or may return a placeholder array that
* will defer the fetch until needed (aka an array fault).
* All objects must be registered in the specified editing context.
* This implementation calls to its parent object store's
* implementation if the requested source object is not
* registered in this editing context.
* The specified relationship key must produce a result of
* type Collection for the source object or an exception is thrown.
*/
public NSArray arrayFaultWithSourceGlobalID (
EOGlobalID aGlobalID,
String aRelationshipKey,
EOEditingContext aContext )
{
NSArray result = null;
Object source = registrar.objectForGlobalID( aGlobalID );
// if not registered in our context
if ( source == null )
{
// get the object registered into our context
result = parentStore.arrayFaultWithSourceGlobalID(
aGlobalID, aRelationshipKey, this );
}
else // source is registered in our context
{
// get existing value
Object value;
if ( source instanceof EOKeyValueCoding )
{
value = ((EOKeyValueCoding)source).storedValueForKey(
aRelationshipKey );
}
else // handle directly
{
value = EOKeyValueCodingSupport.storedValueForKey(
source, aRelationshipKey );
}
if ( value == null )
{
// do the same as if the source was null
result = parentStore.arrayFaultWithSourceGlobalID(
aGlobalID, aRelationshipKey, this );
}
else
if ( value instanceof NSArray )
{
result = (NSArray) value;
}
else // not NSArray
if ( value instanceof Collection )
{
// convert to NSArray
result = new NSArray( (Collection) value );
}
else
{
throw new WotonomyException(
"Relationship key did not return a collection: "
+ aGlobalID + " : " + aRelationshipKey );
}
}
// if our context is not the specified context
if ( aContext != this )
{
result = (NSArray) clone( this, result, aContext );
}
return result;
}
/**
* Returns a snapshot of the specified object as it
* existed when it was last read or committed to the
* parent object store.
*/
public NSDictionary committedSnapshotForObject (
Object anObject )
{
byte[] snapshot = (byte[])
registrar.getCommitSnapshot( anObject );
if ( snapshot == null )
{
// this object not modified: take a current snapshot
snapshot = takeSnapshot( anObject );
}
return convertSnapshotToDictionary( snapshot );
}
/**
* Returns a snapshot of the specified object as it
* existed before the edits triggered by the current
* event loop were processed.
*/
public NSDictionary currentEventSnapshotForObject (
Object anObject )
{
byte[] result = (byte[])
registrar.getCurrentSnapshot( anObject );
if ( result == null )
{
return committedSnapshotForObject( anObject );
}
return convertSnapshotToDictionary( result );
}
/**
* Returns the delegate for this editing context,
* or null if no delegate has been set.
*/
public Object delegate ()
{
if ( delegate == null ) return null;
return delegate.get();
}
/**
* Deletes the specified object from this editing context.
* The editing context marks the object as deleted and
* will notify the parent store when changes are committed.
*/
public void deleteObject (
Object anObject )
{
willChange();
int i;
// remove from added objects if necessary
i = insertedObjects.indexOfIdenticalObject( anObject );
if ( i != NSArray.NotFound )
{
insertedObjects.removeObjectAtIndex( i );
// if in the inserted objects buffer
int index = insertedObjectsBuffer.indexOfIdenticalObject( anObject );
if ( index != NSArray.NotFound )
{
// remove from inserted objects buffer
insertedObjectsBuffer.removeObjectAtIndex( index );
}
// now forget the object ever existed.
forgetObject( anObject );
// we're done
return;
}
else // otherwise add to deleted objects list
{
deletedObjects.addObject( anObject );
}
// remove from updated objects if necessary
i = updatedObjects.indexOfIdenticalObject( anObject );
if ( i != NSArray.NotFound )
{
updatedObjects.removeObjectAtIndex( i );
}
// add to buffer
deletedObjectsBuffer.addObject( anObject );
deletedIDsBuffer.addObject( globalIDForObject( anObject ) );
}
/**
* Returns a read-only List of all objects marked as deleted
* in this editing context.
*/
public NSArray deletedObjects ()
{
return deletedObjectsProxy;
}
/**
* Called by child editing contexts when they no longer
* need to track the specified id.
* This implementation forwards the call to the parent store.
*/
public void editingContextDidForgetObjectWithGlobalID (
EOEditingContext aContext,
EOGlobalID aGlobalID )
{
parentStore.editingContextDidForgetObjectWithGlobalID(
aContext, aGlobalID );
}
/**
* Returns a read-only List of registered editors of this
* editing context.
*/
public NSArray editors ()
{
NSMutableArray result = new NSMutableArray();
Object o;
Iterator i = editorSet.iterator();
while ( i.hasNext() )
{
o = ((WeakReference)i.next()).get();
if ( o != null )
{
result.addObject( o );
}
else
{
i.remove();
}
}
return result;
}
/*
public static void encodeObjectWithCoder (
Object anObject,
NSCoder aCoder )
{
throw new net.wotonomy.util.WotonomyException("Not implemented yet.");
}
*/
/**
* Returns the object for the specified id.
* If the object is registered in in this context
* but not in the specified context,
* this implementation will create a copy of the object
* and register it in the specified context.
* Otherwise it will forward the call to the parent
* object store.
*/
public /*EOEnterpriseObject*/ Object faultForGlobalID (
EOGlobalID aGlobalID,
EOEditingContext aContext )
{
Object result = registrar.objectForGlobalID( aGlobalID );
// if not registered in our context
if ( result == null )
{
// get the object registered into our context
result = parentStore.faultForGlobalID( aGlobalID, this );
}
// if our context is not the specified context
if ( aContext != this )
{
result = registerClone( aGlobalID, this, result, aContext );
}
return result;
}
/**
* Returns a fault representing an object of
* the specified entity type with values from
* the specified dictionary.
* This implementation calls faultForRawRow
* on the parent store.
*/
public Object faultForRawRow (
Map aDictionary,
String anEntityName )
{
return parentStore.faultForRawRow(
aDictionary, anEntityName, this );
}
/**
* Returns a fault representing an object of
* the specified entity type with values from
* the specified dictionary. The fault should
* belong to the specified editing context.
* This implementation forwards the call to
* the parent store.
*/
public /*EOEnterpriseObject*/ Object faultForRawRow (
Map aDictionary,
String anEntityName,
EOEditingContext aContext )
{
return parentStore.faultForRawRow(
aDictionary, anEntityName, aContext );
}
/**
* Returns the fetch timestamp for this editing context.
*/
public double fetchTimestamp ()
{
return fetchTimestamp;
}
/**
* Unregisters the specified object from this editing context,
* removing all references to it. Use this method to remove
* an object from the context without marking it for deletion.
*/
public void forgetObject (
Object anObject )
{
EOGlobalID id = registrar.globalIDForObject( anObject );
if ( id == null )
{
System.err.println(
"EOEditingContext.forgetObject: not registered: " + anObject );
return;
}
// unregister object
registrar.forgetObject( anObject );
// remove from all, inserted, updated, and deleted lists
int index;
index = updatedObjects.indexOfIdenticalObject( anObject );
if ( index != NSArray.NotFound )
{
updatedObjects.removeObjectAtIndex( index );
}
index = insertedObjects.indexOfIdenticalObject( anObject );
if ( index != NSArray.NotFound )
{
insertedObjects.removeObjectAtIndex( index );
return;
}
index = deletedObjects.indexOfIdenticalObject( anObject );
if ( index != NSArray.NotFound )
{
deletedObjects.removeObjectAtIndex( index );
return;
}
// notify parent context
parentStore.editingContextDidForgetObjectWithGlobalID( this, id );
}
/**
* Returns the id for the specified object, or null
* if the object is not registered in this context.
*/
public EOGlobalID globalIDForObject (
Object anObject )
{
return registrar.globalIDForObject( anObject );
}
/**
* Returns an array of ids for an array of objects.
*/
private NSArray globalIDsForObjects(
List anObjectList )
{
NSMutableArray result = new NSMutableArray();
Iterator it = anObjectList.iterator();
while ( it.hasNext() )
{
result.add( globalIDForObject( it.next() ) );
}
return result;
}
/**
* Returns whether this editing context has changes that
* have not yet been committed to the parent object store.
*/
public boolean hasChanges ()
{
if ( updatedObjects.count() > 0 ) return true;
if ( insertedObjects.count() > 0 ) return true;
if ( deletedObjects.count() > 0 ) return true;
return false;
}
/**
* Given a newly instantiated object, this method
* initializes its properties to values appropriate
* for the specified id. The object should already
* belong to the specified editing context.
* This method is called to populate faults.
* This implementation will try to apply the values
* from an object with a matching id in this editing
* context if possible, calling to the parent object
* store only if such an object is not found.
*/
public void initializeObject (
/*EOEnterpriseObject*/ Object anObject,
EOGlobalID aGlobalID,
EOEditingContext aContext )
{
Object existingObject = registrar.objectForGlobalID( aGlobalID );
// if not registered in our context
if ( existingObject == null )
{
// get the object registered into our context
existingObject = parentStore.faultForGlobalID( aGlobalID, this );
}
if ( aContext == this )
{
// initialize the object
parentStore.initializeObject(
/*(EOEnterpriseObject)*/existingObject, aGlobalID, this );
}
else // ( aContext != this )
{
// translates child relationships
copy( this, existingObject, aContext, anObject );
}
aContext.registrar.setCommitSnapshot( anObject, null );
aContext.registrar.setCurrentSnapshot( anObject, null );
}
/**
* Inserts the specified object into this editing context.
* This implementation calls insertObjectWithGlobalID
* with an EOTemporaryGlobalID.
*/
public void insertObject ( Object anObject )
{
insertObjectWithGlobalID(
anObject, new EOTemporaryGlobalID() );
}
/**
* Inserts the specified object into this editing context
* with the specified id, which is expected to be a
* temporary id.
*/
public void insertObjectWithGlobalID (
Object anObject,
EOGlobalID aGlobalID )
{
willChange();
// if this object was marked for deletion
int index = deletedObjects.indexOfIdenticalObject( anObject );
if ( index != NSArray.NotFound )
{
// don't need to re-register: just update the lists
// remove from deleted list
deletedObjects.removeObjectAtIndex( index );
// if in the deleted ids buffer
index = deletedIDsBuffer.indexOfIdenticalObject( anObject );
if ( index != NSArray.NotFound )
{
// remove from deleted ids buffer
deletedIDsBuffer.removeObjectAtIndex( index );
}
// if in the deleted objects buffer
index = deletedObjectsBuffer.indexOfIdenticalObject( anObject );
if ( index != NSArray.NotFound )
{
// remove from deleted objects buffer
deletedObjectsBuffer.removeObjectAtIndex( index );
}
else // not in the deleted objects buffer
{
// add to the inserted objects buffer
insertedObjectsBuffer.addObject( anObject );
}
// we're done
return;
}
// make sure object is not already in editing context
if ( objectForGlobalID( aGlobalID ) != null )
{
throw new WotonomyException(
"Tried to insert but object was already registered:"
+ aGlobalID );
}
// register object
recordObject( anObject, aGlobalID );
// add to inserted list
insertedObjects.addObject( anObject );
// add to buffer
insertedObjectsBuffer.addObject( anObject );
}
/**
* Returns a read-only List of the objects that have been
* inserted into this editing context.
*/
public NSArray insertedObjects ()
{
return insertedObjectsProxy;
}
/**
* Turn all objects in this editing context into faults,
* so that they will be fetched the next time they are
* accessed, and calls invalidateObjectsWithGlobalIDs
* on the parent object store.
*/
public void invalidateAllObjects ()
{
// register change so processRecentChanges is called
willChange();
invalidateAllObjectsQuietly();
// post notification
NSNotificationCenter.defaultCenter().postNotification(
new NSNotification(
InvalidatedAllObjectsInStoreNotification, this ) );
}
/**
* Only refaults all objects, does not notify will change
* nor post notification, but does call parent store.
* Called by invalidateAllObjects() and handleNotification().
*/
private void invalidateAllObjectsQuietly()
{
// remember the ids
NSMutableArray ids = new NSMutableArray( registrar.registeredGlobalIDs() );
// track of discarded IDs (from inserted objects)
NSMutableArray discardedIDs = new NSMutableArray();
// refault all objects
EOGlobalID id;
Object o;
Enumeration e = ids.objectEnumerator();
while ( e.hasMoreElements() )
{
id = (EOGlobalID) e.nextElement();
o = objectForGlobalID( id );
// some objects may have been manually discarded
if ( o != null )
{
// don't refault newly inserted objects
if ( insertedObjects.indexOfIdenticalObject( o ) == NSArray.NotFound )
{
refaultObject( o, id, this );
}
else
{
// discard inserted objects
forgetObject( o );
discardedIDs.add( id );
}
invalidatedObjectsBuffer.add( o );
}
invalidatedIDsBuffer.add( id );
}
ids.removeAll( discardedIDs );
// call to parent store (should call this after posting instead?)
isInvalidating = true;
parentStore.invalidateObjectsWithGlobalIDs( ids );
isInvalidating = false;
}
/**
* Turns the objects with the specified ids into faults,
* so that they will be fetched the next time they are
* accessed, and forwards the call to the parent object store.
*/
public void invalidateObjectsWithGlobalIDs (
List anArray )
{
// register change so processRecentChanges is called
willChange();
// call to parent to invalidate objects
parentStore.invalidateObjectsWithGlobalIDs( anArray );
Object o;
EOGlobalID id;
Iterator it = anArray.iterator();
while ( it.hasNext() )
{
id = (EOGlobalID) it.next();
if ( id != null )
{
o = objectForGlobalID( id );
if ( o != null )
{
Object result = notifyDelegate(
"editingContextShouldInvalidateObject",
new Class[] { EOEditingContext.class, Object.class, EOGlobalID.class },
new Object[] { this, o, id } );
if ( ( result == null ) || ( Boolean.TRUE.equals( result ) ) )
{
// refault the object
refaultObject( o, id, this );
invalidatedObjectsBuffer.add( o );
invalidatedIDsBuffer.add( id );
}
}
}
else
{
throw new WotonomyException(
"Attempted to invalidate a null global id: " + anArray );
}
}
}
/*
public boolean invalidatesObjectsWhenFinalized ( )
{
throw new net.wotonomy.util.WotonomyException("Not implemented yet.");
}
public boolean invalidatesObjectsWhenFreed ( )
{
throw new net.wotonomy.util.WotonomyException("Not implemented yet.");
}
*/
/**
* Returns whether the object referenced by the
* specified id is locked.
* This implementation simply forwards the call to
* the parent object store.
*/
public boolean isObjectLockedWithGlobalID (
EOGlobalID aGlobalID,
EOEditingContext aContext)
{
return parentStore.isObjectLockedWithGlobalID(
aGlobalID, aContext );
}
/*
public void lock ()
{
throw new net.wotonomy.util.WotonomyException("Not implemented yet.");
}
*/
/**
* Locks the specified object in this editing context
* by calling lockObjectWithGlobalID on the parent store.
*/
public void lockObject (
Object anObject )
{
parentStore.lockObjectWithGlobalID(
globalIDForObject( anObject ), this );
}
/**
* Locks the object referenced by the specified id
* in the specified editing context.
* This implementation simply forwards the call to
* the parent object store.
*/
public void lockObjectWithGlobalID (
EOGlobalID aGlobalID,
EOEditingContext aContext)
{
parentStore.lockObjectWithGlobalID(
aGlobalID, aContext );
}
/**
* Returns whether this editing context attempts to
* lock objects when they are first modified.
*/
public boolean locksObjectsBeforeFirstModification ()
{
return lockBeforeModify;
}
/**
* Returns the message handler for this editing context,
* or null if no message handler has been set.
*/
public Object messageHandler ()
{
if ( messageHandler == null ) return null;
return messageHandler.get();
}
/**
* Returns the object registered in this editing context
* for the specified id, or null if that id is not
* registered.
*/
public Object objectForGlobalID (
EOGlobalID aGlobalID )
{
return registrar.objectForGlobalID( aGlobalID );
}
/**
* Returns a read-only List of objects associated with the object
* with the specified id for the specified property
* relationship. This method may not return an array fault
* because array faults call this method to fetch on demand.
* All objects must be registered the specified editing context.
* The specified relationship key must produce a result of
* type Collection for the source object or an exception is thrown.
*/
public NSArray objectsForSourceGlobalID (
EOGlobalID aGlobalID,
String aRelationshipKey,
EOEditingContext aContext )
{
//System.out.println( "EOEditingContext.objectsForSourceGlobalID: "
//+ aGlobalID + " : " + aRelationshipKey );
NSArray result = null;
if ( aContext == this )
{
throw new WotonomyException( "Assert failed: calling objectsForSourceGlobalID on ourself." );
}
Object source = registrar.objectForGlobalID( aGlobalID );
// if not registered in our context
if ( source == null )
{
// get the object registered into our context
result = parentStore.objectsForSourceGlobalID(
aGlobalID, aRelationshipKey, this );
}
else // source is registered in our context
{
// get existing value
Object value;
if ( source instanceof EOKeyValueCoding )
{
value = ((EOKeyValueCoding)source).storedValueForKey(
aRelationshipKey );
}
else // handle directly
{
value = EOKeyValueCodingSupport.storedValueForKey(
source, aRelationshipKey );
}
// if we don't have a valid value on our object
if ( ( value == null )
|| ( ( value instanceof ArrayFault )
&& ( !((ArrayFault)value).isFetched() ) ) )
{
// do the same as if the source was null
result = parentStore.objectsForSourceGlobalID(
aGlobalID, aRelationshipKey, this );
// set our value since we have it
if ( source instanceof EOKeyValueCoding )
{
((EOKeyValueCoding)source).takeStoredValueForKey(
result, aRelationshipKey );
}
else // handle directly
{
EOKeyValueCodingSupport.takeStoredValueForKey(
source, result, aRelationshipKey );
}
}
else
if ( ( value instanceof ArrayFault )
&& ( !((ArrayFault)value).isFetched() ) )
{
// do the same as if the source was null
result = parentStore.objectsForSourceGlobalID(
aGlobalID, aRelationshipKey, this );
}
else
if ( value instanceof NSArray )
{
result = (NSArray) value;
}
else // not NSArray
if ( value instanceof Collection )
{
// convert to NSArray
result = new NSArray( (Collection) value );
}
else
{
throw new WotonomyException(
"Relationship key did not return a collection: "
+ aGlobalID + " : " + aRelationshipKey );
}
}
// if our context is not the specified context
if ( aContext != this )
{
result = (NSArray) clone( this, result, aContext );
}
return result;
}
/**
* Returns a read-only List of objects the meet the criteria of
* the supplied specification. This method simply calls
* objectsWithFetchSpecification on this editing context
* with this editing context as the parameter.
*/
public NSArray objectsWithFetchSpecification (
EOFetchSpecification aFetchSpec )
{
return objectsWithFetchSpecification( aFetchSpec, this );
}
/**
* Returns a read-only List of objects the meet the criteria of
* the supplied specification. Faults are not allowed in the array.
* If any objects are already fetched, they should not be
* refetched. All objects should belong to the specified editing context.
* This implementation forwards the call to the parent object
* store, which will register each object in the specified editing
* context only if it does not already exist.
*/
public NSArray objectsWithFetchSpecification (
EOFetchSpecification aFetchSpec,
EOEditingContext aContext)
{
if ( aContext == this )
{
Object result = notifyDelegate(
"editingContextShouldFetchObjects",
new Class[] { EOEditingContext.class, EOFetchSpecification.class },
new Object[] { aContext, aFetchSpec } );
if ( result instanceof NSArray ) return (NSArray) result;
}
return parentStore.objectsWithFetchSpecification( aFetchSpec, aContext );
}
/**
* Returns the parent object store for this editing context.
* The result will not be null.
*/
public EOObjectStore parentObjectStore ()
{
return parentStore;
}
/**
* Updates the inserted, updated, and deleted objects lists,
* and posts notifications about which objects have been changed.
* This method is called at the end of an event loop in which
* objects were modified. This method is additionally called
* by saveChanges() so that any changes in the same event loop
* will be processed correctly before calling to the parent
* object store.
* This implementation updates those lists immediately, but
* only posts notifications when this method is called.
*/
public void processRecentChanges ()
{ // System.out.println( "EOEditingContext.processRecentChanges: " + invalidatedObjectsBuffer );
/*
* This implementation actually updates those lists immediately,
* but keeps a separate buffer of changes in the current event
* loop for the purposes of posting a notification.
* NOTE: to reenable buffering, uncomment lines from this method
* body and reenable the RecentChangesObserver in the constructor.
*/
// broadcast ObjectsChangedInStoreNotification
// for the benefit of child editing contexts
boolean postStoreInfo =
( insertedObjectsBuffer.size() +
updatedObjectsBuffer.size() +
deletedIDsBuffer.size() +
invalidatedIDsBuffer.size() > 0 );
NSMutableDictionary storeInfo = new NSMutableDictionary();
if ( postStoreInfo )
{
storeInfo.setObjectForKey(
globalIDsForObjects( insertedObjectsBuffer ),
// globalIDsForObjects( insertedObjects ),
EOObjectStore.InsertedKey );
storeInfo.setObjectForKey(
globalIDsForObjects( updatedObjectsBuffer ),
// globalIDsForObjects( updatedObjects ),
EOObjectStore.UpdatedKey );
storeInfo.setObjectForKey(
new NSArray( (Collection) deletedIDsBuffer ),
// globalIDsForObjects( deletedObjects ),
EOObjectStore.DeletedKey );
storeInfo.setObjectForKey(
new NSArray( (Collection) invalidatedIDsBuffer ),
EOObjectStore.InvalidatedKey );
}
// broadcast ObjectsChangedInEditingContextNotification
// for the benefit of attached display groups
boolean postContextInfo =
( insertedObjectsBuffer.size() +
updatedObjectsBuffer.size() +
deletedObjectsBuffer.size() +
invalidatedObjectsBuffer.size() > 0 );
NSMutableDictionary contextInfo = new NSMutableDictionary();
if ( postContextInfo )
{
contextInfo.setObjectForKey(
new NSArray( (Collection) insertedObjectsBuffer ),
// new NSArray( (Collection) insertedObjects ),
EOObjectStore.InsertedKey );
contextInfo.setObjectForKey(
new NSArray( (Collection) updatedObjectsBuffer ),
// new NSArray( (Collection) updatedObjects ),
EOObjectStore.UpdatedKey );
contextInfo.setObjectForKey(
new NSArray( (Collection) deletedObjectsBuffer ),
// new NSArray( (Collection) deletedObjects ),
EOObjectStore.DeletedKey );
contextInfo.setObjectForKey(
new NSArray( (Collection) invalidatedObjectsBuffer ),
EOObjectStore.InvalidatedKey );
}
// update the current snapshots
Object o;
Iterator it;
it = insertedObjectsBuffer.iterator();
while ( it.hasNext() )
{
o = it.next();
registrar.setCurrentSnapshot( o, takeSnapshot( o ) );
}
it = updatedObjectsBuffer.iterator();
while ( it.hasNext() )
{
o = it.next();
registrar.setCurrentSnapshot( o, takeSnapshot( o ) );
}
// clear buffers
insertedObjectsBuffer.removeAllObjects();
updatedObjectsBuffer.removeAllObjects();
deletedObjectsBuffer.removeAllObjects();
deletedIDsBuffer.removeAllObjects();
invalidatedObjectsBuffer.removeAllObjects();
invalidatedIDsBuffer.removeAllObjects();
// post notifications (does order matter?)
if ( postStoreInfo )
{
NSNotificationCenter.defaultCenter().postNotification(
new NSNotification(
ObjectsChangedInStoreNotification, this, storeInfo ) );
}
if ( postContextInfo )
{
NSNotificationCenter.defaultCenter().postNotification(
new NSNotification(
ObjectsChangedInEditingContextNotification, this, contextInfo ) );
}
}
/**
* Returns whether this editing context propagates deletes
* immediately after the event that triggered the delete.
* Otherwise, propagation occurs only before commit.
*/
public boolean propagatesDeletesAtEndOfEvent ()
{
return propagateDeletesAfterEvent;
}
/**
* Registers the specified object in this editing context
* for the specified id. This method is called by an object
* store when fetching objects for a display group, or when
* objects are inserted into a display group.
* This implementation will re-register the object under the
* new id if it is already registered under a different id.
*/
public void recordObject (
Object anObject,
EOGlobalID aGlobalID )
{
// find state for re-registration
boolean inserted = false;
boolean updated = false;
boolean deleted = false;
// is the object already registered?
EOGlobalID existingID = globalIDForObject( anObject );
if ( existingID != null )
{
// remember object state
int index;
index = insertedObjects.indexOfIdenticalObject( anObject );
if ( index != NSArray.NotFound ) inserted = true;
index = updatedObjects.indexOfIdenticalObject( anObject );
if ( index != NSArray.NotFound ) updated = true;
index = deletedObjects.indexOfIdenticalObject( anObject );
if ( index != NSArray.NotFound ) deleted = true;
// forget the object
forgetObject( anObject );
}
// is the global id already in use?
Object existingObject = objectForGlobalID( aGlobalID );
if ( existingObject != null )
{
// forget it (don't worry about state?)
forgetObject( existingObject );
}
registrar.registerObject( anObject, aGlobalID );
// restore state if necessary
if ( inserted ) insertedObjects.addObject( anObject );
if ( updated ) updatedObjects.addObject( anObject );
if ( deleted ) deletedObjects.addObject( anObject );
}
/**
* Undoes the last undo operation.
*/
public void redo ()
{
//TODO: not supported yet
throw new UnsupportedOperationException("Not implemented yet.");
}
/**
* Refaults this editing context, turning all unmodified
* objects into faults. This implementation calls
* editingContextWillSaveChanges() on all editors, and
* then calls refaultObjects().
*/
public void refault ()
{
fireWillSaveChanges();
refaultObjects();
}
/**
* Refaults the specified object, turning it into a fault
* for the specified global id in the specified context.
*/
public void refaultObject (
Object anObject,
EOGlobalID aGlobalID,
EOEditingContext aContext)
{
aContext.registrar.setCurrentSnapshot( anObject, null );
ignoreChanges = true;
parentStore.refaultObject( anObject, aGlobalID, aContext );
ignoreChanges = false;
// remove from updated objects if necessary
int i = updatedObjects.indexOfIdenticalObject( anObject );
if ( i != NSArray.NotFound )
{
updatedObjects.removeObjectAtIndex( i );
}
// add to invalidated notification queue
invalidatedObjectsBuffer.addObject( anObject );
invalidatedIDsBuffer.addObject( aGlobalID );
}
/**
* Turns all unmodified objects into faults, calling
* processRecentChanges() and then refaultObject() for
* each unmodified object.
*/
public void refaultObjects ()
{
// is this call really needed?
// processRecentChanges();
Object o;
EOGlobalID id;
Iterator it = registeredObjects().iterator();
while ( it.hasNext() )
{
o = it.next();
if ( ( updatedObjects.indexOfIdenticalObject( o ) == NSArray.NotFound )
&& ( insertedObjects.indexOfIdenticalObject( o ) == NSArray.NotFound )
&& ( deletedObjects.indexOfIdenticalObject( o ) == NSArray.NotFound ) )
{
id = globalIDForObject( o );
refaultObject( o, id, this );
}
}
}
/**
* Calls editingContextWillSaveChanges() on all editors,
* and then calls invalidateAllObjects().
*/
public void refetch ()
{
fireWillSaveChanges();
invalidateAllObjects();
}
/**
* Returns a read-only List of all objects registered in this
* editing context.
*/
public NSArray registeredObjects ()
{
return registrar.registeredObjects();
}
/**
* Unregisters the specified editor with this editing context.
*/
public void removeEditor ( Object anObject )
{
if ( anObject == null ) return;
Object o;
Iterator i = editorSet.iterator();
while ( i.hasNext() )
{
o = ((WeakReference)i.next()).get();
if ( ( o == null ) || ( o == anObject ) )
{
i.remove();
}
}
}
/**
* Unregisters all objects from this editing context,
* and resets the fetch timestamp.
*/
public void reset ()
{
Iterator it = registeredObjects().iterator();
while ( it.hasNext() )
{
forgetObject( it.next() );
}
fetchTimestamp = 0; //FIXME: reset timestamp properly
}
/**
* Reverts the objects in this editing context to
* their original state.
* Calls editingContextWillSaveChanges on all editors,
* discards all inserted objects, restores deleted
* objects, and applies the fetch snapshot to all
* registered objects.
*/
public void revert ()
{
willChange();
fireWillSaveChanges();
Iterator it;
// forget inserted objects
it = new NSArray( insertedObjects ).iterator();
while ( it.hasNext() )
{
forgetObject( it.next() );
}
EOGlobalID id;
Object o;
byte[] snapshot;
// re-initialize updated objects
it = new NSArray( updatedObjects ).iterator();
while ( it.hasNext() )
{
o = it.next();
snapshot = (byte[]) registrar.getCommitSnapshot( o );
if ( snapshot != null )
{
applySnapshot( snapshot, o );
}
registrar.setCommitSnapshot( o, null );
updatedObjectsBuffer.addObject( o );
}
// re-initialize deleted objects
it = new NSArray( deletedObjects ).iterator();
while ( it.hasNext() )
{
o = it.next();
snapshot = (byte[]) registrar.getCommitSnapshot( o );
if ( snapshot != null )
{
applySnapshot( snapshot, o );
}
registrar.setCommitSnapshot( o, null );
updatedObjectsBuffer.addObject( o );
}
// reset lists
insertedObjects.removeAllObjects(); // unneccessary?
deletedObjects.removeAllObjects();
updatedObjects.removeAllObjects();
// post notification
processRecentChanges();
}
/**
* Returns the root object store, which is the parent
* of all parent object stores of this editing context.
*/
public EOObjectStore rootObjectStore ()
{
EOObjectStore parent = parentObjectStore();
while ( parent instanceof EOEditingContext )
{
parent = ((EOEditingContext)parent).parentObjectStore();
}
return parent;
}
/**
* Calls editingContextWillSaveChanges on all editors,
* and commits all changes in this editing context to
* the parent editing context by calling
* saveChangesInEditingContext to the parent.
* Then posts EditingContextDidSaveChangeNotification.
*/
public void saveChanges ()
{
//System.out.println( "EOEditingContext.saveChanges: " + this );
willChange();
// process any changes
processRecentChanges();
// set up user info for notification to be posted.
NSMutableDictionary userInfo = new NSMutableDictionary();
userInfo.setObjectForKey(
new NSArray( (Collection) insertedObjects ),
EOObjectStore.InsertedKey );
userInfo.setObjectForKey(
new NSArray( (Collection) updatedObjects ),
EOObjectStore.UpdatedKey );
userInfo.setObjectForKey(
new NSArray( (Collection) deletedObjects ),
EOObjectStore.DeletedKey );
// notify the editors
fireWillSaveChanges();
// notify the delegate
notifyDelegate(
"editingContextWillSaveChanges",
new Class[] { EOEditingContext.class },
new Object[] { this } );
// needed for notification handling
isInvalidating = true;
try
{
// ask parent to save us
parentStore.saveChangesInEditingContext( this );
}
catch ( RuntimeException e )
{
// unset save flag and rethrow
isInvalidating = false;
throw e;
}
isInvalidating = false;
// no exceptions: proceed!
Object o, key;
Iterator it;
// update the committed snapshots
it = insertedObjects.iterator();
while ( it.hasNext() )
{
o = it.next();
registrar.setCommitSnapshot( o, null );
registrar.setCurrentSnapshot( o, null );
}
it = updatedObjects.iterator();
while ( it.hasNext() )
{
o = it.next();
registrar.setCommitSnapshot( o, null );
registrar.setCurrentSnapshot( o, null );
}
// clear the lists
updatedObjects.removeAllObjects();
insertedObjects.removeAllObjects();
it = new NSArray( deletedObjects() ).iterator();
while ( it.hasNext() )
{ // parent is doing this as well?
forgetObject( it.next() );
}
// post notification
NSNotificationCenter.defaultCenter().postNotification(
new NSNotification(
EditingContextDidSaveChangesNotification, this, userInfo ) );
}
/**
* Commits all changes in the specified editing context
* to this one. Called by child editing contexts in
* their saveChanges() method.
*/
public void saveChangesInEditingContext (
EOEditingContext aContext)
{
Object o;
Iterator it;
// process deletes
List deletes = new NSArray( aContext.deletedObjects() );
it = deletes.iterator();
while ( it.hasNext() )
{
o = it.next();
EOGlobalID id = aContext.globalIDForObject( o );
Object localVersion = objectForGlobalID( id );
if ( localVersion == null )
{
// make a local copy and register it
localVersion = registerClone( id, aContext, o, this );
if ( localVersion == null )
{
throw new WotonomyException(
"Deleted object could not be serialized: "
+ id + " : " + o );
}
}
else // we have a local version, copy changes
{
copy( aContext, o, this, localVersion );
// copy marks the object as updated: will be on both lists
}
// delete our copy -- marks context as changed
deleteObject( localVersion );
}
// process inserts - all inserts are new objects
List inserts = new NSArray( aContext.insertedObjects() );
it = inserts.iterator();
while ( it.hasNext() )
{
o = it.next();
// make a local copy and register it
EOGlobalID id = aContext.globalIDForObject( o );
willChange(); // need to mark editing context as changed
Object localVersion = registerClone( id, aContext, o, this );
if ( localVersion == null )
{
throw new WotonomyException(
"Inserted object could not be serialized: "
+ o );
}
// insert our copy manually so a new id is not generated
insertedObjects.addObject( localVersion );
insertedObjectsBuffer.addObject( localVersion );
}
// process updates
List updates = new NSArray( aContext.updatedObjects() );
it = updates.iterator();
while ( it.hasNext() )
{
willChange(); // need to mark editing context as changed
o = it.next();
EOGlobalID id = aContext.globalIDForObject( o );
Object localVersion = objectForGlobalID( id );
if ( localVersion == null )
{
// make a local copy and register it
localVersion = registerClone( id, aContext, o, this );
if ( localVersion == null )
{
throw new WotonomyException(
"Updated object could not be serialized: "
+ id + " : " + o );
}
if ( id.isTemporary() )
{
// mark this object as inserted
insertedObjects.addObject( localVersion );
insertedObjectsBuffer.addObject( localVersion );
}
else
{
// mark this object as updated
updatedObjects.addObject( localVersion );
// notify of update only if not on deleted list
if ( deletedObjectsBuffer.indexOfIdenticalObject(
localVersion ) == NSArray.NotFound )
{
updatedObjectsBuffer.addObject( localVersion );
}
}
}
else // we have a local version, copy changes
{
copy( aContext, o, this, localVersion );
// copy marks the object as updated
}
}
}
/**
* Sets the delegate for this editing context.
* Note: this implementation retains only a
* weak reference to the specified object.
*/
public void setDelegate (
Object anObject )
{
if ( anObject == null ) delegate = null;
delegate = new WeakReference( anObject );
}
/**
* Sets the fetch timestamp for this editing context.
*/
public void setFetchTimestamp (
double aDouble )
{
fetchTimestamp = aDouble;
}
/*
public void setInvalidatesObjectsWhenFinalized (
boolean invalidatesObjects )
{
throw new net.wotonomy.util.WotonomyException("Not implemented yet.");
}
public void setInvalidatesObjectsWhenFreed (
boolean invalidatesObjects )
{
throw new net.wotonomy.util.WotonomyException("Not implemented yet.");
}
*/
/**
* Sets whether this editing context attempts to
* lock objects when they are first modified.
* Default is false.
*/
public void setLocksObjectsBeforeFirstModification (
boolean locksObjects )
{
lockBeforeModify = locksObjects;
}
/**
* Sets the message handler for this editing context.
* Note: this implementation retains only a
* weak reference to the specified object.
*/
public void setMessageHandler (
Object anObject )
{
if ( anObject == null ) messageHandler = null;
messageHandler = new WeakReference( anObject );
}
/**
* Sets whether this editing context propagates deletes
* immediately after the event that triggered the delete.
* Otherwise, propagation occurs only before commit.
* Default is true.
*/
public void setPropagatesDeletesAtEndOfEvent (
boolean propagatesDeletes )
{
propagateDeletesAfterEvent = propagatesDeletes;
}
/*
public void setSharedEditingContext (
EOSharedEditingContext aSharedEditingContext )
{
throw new net.wotonomy.util.WotonomyException("Not implemented yet.");
}
*/
/**
* Sets whether validation is stopped after the
* first error occurs. Otherwise, validation will
* continue for all other objects.
* Default is true.
*/
public void setStopsValidationAfterFirstError (
boolean stopsValidation )
{
stopValidationAfterError = stopsValidation;
}
/**
* Sets the undo manager to be used for this context.
* Note: This is currently javax.swing.undo.UndoManager,
* until we have a implementation of NSUndoManager.
*/
/*
public void setUndoManager (
UndoManager anUndoManager )
{
undoManager = anUndoManager;
}
*/
/*
public EOSharedEditingContext sharedEditingContext ()
{
throw new net.wotonomy.util.WotonomyException("Not implemented yet.");
}
*/
/**
* Returns whether validation is stopped after the
* first error occurs. Otherwise, validation will
* continue for all other objects.
*/
public boolean stopsValidationAfterFirstError ()
{
return stopValidationAfterError;
}
/**
* Reverts the last change on the undo stack.
*/
public void undo ()
{
//TODO: not supported yet
throw new UnsupportedOperationException("Not implemented yet.");
}
/*
public NSUndoManager undoManager ()
{
throw new net.wotonomy.util.WotonomyException("Not implemented yet.");
}
*/
/**
public void unlock ()
{
throw new net.wotonomy.util.WotonomyException("Not implemented yet.");
}
*/
/**
* Returns a read-only list of all objects marked as modified,
* but not inserted or deleted, in this editing context.
*/
public NSArray updatedObjects ()
{
return updatedObjectsProxy;
}
/**
* Notify editors of changes.
*/
private void fireWillSaveChanges()
{
Object o = null;
Iterator i = editors().iterator();
while ( i.hasNext() )
{
try
{
o = i.next();
NSSelector.invoke( "editingContextWillSaveChanges",
new Class[] { EOEditingContext.class }, o, this );
}
catch ( NoSuchMethodException e )
{
// ignore: not implemented
}
catch ( Exception exc )
{
// log to standard error
System.err.println( "Error while notifying editor of pending save: " + o );
exc.printStackTrace();
}
}
}
/**
* Handles notifications from parent store, looking for
* InvalidatedAllObjectsInStoreNotification and
* ObjectsChangedInStoreNotification.
* The former causes all objects in this store to be
* invalidated.
* The latter refaults the invalidated ids, merges changes
* from the updated ids, and forgets the deleted ids, then
* posts a ObjectsChangedInStoreNotification.
* Note: This method is not in the public specification.
*/
public void handleNotification( NSNotification aNotification )
{ // System.out.println( "EOEditingContext: " + this + " : " + aNotification );
willChange();
if ( InvalidatedAllObjectsInStoreNotification
.equals( aNotification.name() ) )
{
refaultObjects();
// relay notification
NSNotificationCenter.defaultCenter().postNotification(
new NSNotification(
InvalidatedAllObjectsInStoreNotification, this ) );
}
else
if ( EOGlobalID.GlobalIDChangedNotification
.equals( aNotification.name() ) )
{
NSDictionary userInfo = aNotification.userInfo();
// if any keys in userInfo are registered ids,
// re-register with new permanent values.
Object o;
EOGlobalID id;
Enumeration e = userInfo.keyEnumerator();
while ( e.hasMoreElements() )
{
id = (EOGlobalID) e.nextElement();
o = objectForGlobalID( id );
if ( o != null )
{
// record object is assumed to handle key updates
recordObject( o, (EOGlobalID) userInfo.objectForKey( id ) );
}
}
}
else
if ( EOObjectStore.ObjectsChangedInStoreNotification
.equals( aNotification.name() ) )
{
//System.out.println( "EOEditingContext.handleNotification: " + aNotification + " : " + this );
// post so child contexts are notified
willChange();
Object o;
EOGlobalID id;
Enumeration e;
NSDictionary userInfo = aNotification.userInfo();
// inserted objects are ignored
// existing deleted objects are removed
NSArray deletes = (NSArray) userInfo.objectForKey(
EOObjectStore.DeletedKey );
e = deletes.objectEnumerator();
while ( e.hasMoreElements() )
{
id = (EOGlobalID) e.nextElement();
o = objectForGlobalID( id );
if ( o != null )
{
//System.out.println( "EOEditingContext: deleted: " + id );
forgetObject( o );
deletedObjectsBuffer.addObject( o );
deletedIDsBuffer.addObject( id );
}
}
// existing updated objects are merged
NSArray updates = (NSArray) userInfo.objectForKey(
EOObjectStore.UpdatedKey );
e = updates.objectEnumerator();
while ( e.hasMoreElements() )
{
id = (EOGlobalID) e.nextElement();
o = objectForGlobalID( id );
if ( o != null )
{
//System.out.println( "EOEditingContext: updated: " + id );
if ( updatedObjects // only update if unchanged
.indexOfIdenticalObject( o ) == NSArray.NotFound )
{
refaultObject( o, id, this );
updatedObjectsBuffer.addObject( o );
}
else
{
// notify user and/or merge
handleUpdateConflict( id, o );
}
}
}
// existing invalidated objects are refaulted
NSArray invalidates = (NSArray) userInfo.objectForKey(
EOObjectStore.InvalidatedKey );
e = invalidates.objectEnumerator();
while ( e.hasMoreElements() )
{
id = (EOGlobalID) e.nextElement();
o = objectForGlobalID( id );
if ( o != null )
{
if ( updatedObjects // only invalidate if unchanged
.indexOfIdenticalObject( o ) == NSArray.NotFound )
{
refaultObject( o, id, this );
}
else
{
// notify user and/or merge
handleUpdateConflict( id, o );
}
if ( invalidatedObjectsBuffer
.indexOfIdenticalObject( o ) == NSArray.NotFound )
{
invalidatedObjectsBuffer.addObject( o );
}
if ( invalidatedIDsBuffer
.indexOfIdenticalObject( id ) == NSArray.NotFound )
{
invalidatedIDsBuffer.addObject( id );
}
}
}
}
}
/**
* Called by handleNotification to resolve the case where we have
* received notification that another user or context has updated
* an object that is currently marked as edited in this context.
* This implementation first asks the delegate if it should merge.
* If true or no delegate, the changes are merged. True or false,
* the delegate is then sent editingContextDidMergeChanges.
*/
private void handleUpdateConflict( EOGlobalID anID, Object anObject )
{
// if we're causing the invalidation, ignore
// (this is probably better handled by the caller...)
if ( isInvalidating )
{
ignoreChanges = true;
parentStore.refaultObject( anObject, anID, this );
ignoreChanges = false;
return;
}
Boolean result = (Boolean) notifyDelegate(
"editingContextShouldMergeChangesForObject",
new Class[] { EOEditingContext.class, Object.class },
new Object[] { this, anObject } );
if ( ( result == null ) || ( Boolean.TRUE.equals( result ) ) )
{
// do merge
mergeExternalChanges( anID, anObject );
}
else // Boolean.FALSE
{
// do nothing: don't lose the user's changes
}
// notify merge did happen
notifyDelegate(
"editingContextDidMergeChanges",
new Class[] { EOEditingContext.class },
new Object[] { this } );
}
/**
* For the currently modified object with the specified global id,
* this method merges changes with the updated version in the parent
* object store. This implementation looks at the fetch snapshot
* to determine which changes where made by the user, fetches the
* updated version of the object, and then determine what external
* changes were made. If the changes do not overlap, the original
* changes are applied to the updated version. If there is a conflict,
* notifies the user of the conflict.
*/
private boolean mergeExternalChanges( EOGlobalID anID, Object anObject )
{
try
{
Iterator i;
Object key;
// get fetch snapshot
Map fetchSnapshot = committedSnapshotForObject( anObject );
// get current snapshot
Map currentSnapshot = currentEventSnapshotForObject( anObject );
// diff against fetch snapshot
Map currentDiff = new HashMap();
i = currentSnapshot.keySet().iterator();
while ( i.hasNext() )
{
key = i.next();
if ( ! currentSnapshot.get( key ).equals( fetchSnapshot.get( key ) ) )
{
currentDiff.put( key, currentSnapshot.get( key ) );
}
}
// refault
ignoreChanges = true;
parentStore.refaultObject( anObject, anID, this );
ignoreChanges = false;
// get updated snapshot
Map updatedSnapshot = convertSnapshotToDictionary( takeSnapshot( anObject ) );
// diff against fetch snapshot
Map updatedDiff = new HashMap();
i = updatedSnapshot.keySet().iterator();
while ( i.hasNext() )
{
key = i.next();
if ( ! updatedSnapshot.get( key ).equals( fetchSnapshot.get( key ) ) )
{
updatedDiff.put( key, updatedSnapshot.get( key ) );
}
}
// determine if there's a conflict
boolean proceed = true;
Set updatedKeys = updatedDiff.keySet();
i = currentDiff.keySet().iterator();
while ( i.hasNext() )
{
if ( updatedKeys.contains( i.next() ) )
{
proceed = false;
break;
}
}
// if no conflicts, apply original diff to current object and exit
if ( proceed )
{
KeyValueCodingUtilities.takeStoredValuesFromDictionary( anObject, currentDiff );
return true; // exit!
}
}
catch ( Exception exc )
{
// log error to standard out
exc.printStackTrace();
}
// notify user we're unable to merge
notifyMessageHandler( MessageChangeConflict + anObject );
return false;
}
/**
* Sends the specified message to the message handler.
*/
private void notifyMessageHandler( String aMessage )
{
Object handler = null;
try
{
handler = messageHandler();
if ( handler == null ) return;
NSSelector.invoke( "editingContextPresentErrorMessage",
new Class[] { EOEditingContext.class, String.class },
handler, this, aMessage );
}
catch ( NoSuchMethodException e )
{
// ignore: not implemented
}
catch ( Exception exc )
{
// log to standard error
System.err.println(
"Error while notifying message handler: " +
handler + " : " + aMessage );
exc.printStackTrace();
}
}
/**
* Sends the specified message to the delegate.
* Returns the return value of the method,
* or null if no return value or no delegate
* or no implementation.
*/
private Object notifyDelegate(
String aMethodName, Class[] types, Object[] params )
{
try
{
Object delegate = delegate();
if ( delegate == null ) return null;
return NSSelector.invoke(
aMethodName, types, delegate, params );
}
catch ( NoSuchMethodException e )
{
// ignore: not implemented
}
catch ( Exception exc )
{
// log to standard error
System.err.println(
"Error while messaging delegate: " +
delegate + " : " + aMethodName );
exc.printStackTrace();
}
return null;
}
// interface EOObserving
/**
* Implementation of the EOObserving interface.
* Called before objects are modified.
*/
public void objectWillChange (
Object anObject )
{
if ( ignoreChanges ) return;
//NSNotificationCenter.defaultCenter().postNotification( "objectWillChange", this, new NSDictionary( "object", anObject ) );
//new RuntimeException().printStackTrace();
willChange();
// mark as updated if not marked already
int i = updatedObjects.indexOfIdenticalObject( anObject );
if ( i == NSArray.NotFound )
{
// don't mark inserted objects as updated
i = insertedObjects.indexOfIdenticalObject( anObject );
if ( i == NSArray.NotFound )
{
i = deletedObjects.indexOfIdenticalObject( anObject );
if ( i == NSArray.NotFound )
{
// add object
updatedObjects.addObject( anObject );
// record revert snapshot
registrar.setCommitSnapshot( anObject, takeSnapshot( anObject ) );
}
}
}
// add to buffer
if ( updatedObjectsBuffer.indexOfIdenticalObject( anObject )
== NSArray.NotFound )
{
updatedObjectsBuffer.addObject( anObject );
}
}
// static methods
public static double defaultFetchTimestampLag ()
{
return defaultFetchTimestampLag;
}
/**
* Returns the default parent object store for all
* object stores created with the parameterless
* constructor.
*/
public static EOObjectStore defaultParentObjectStore ()
{
return defaultParentObjectStore;
}
/*
public static Object initObjectWithCoder (
Object anObject,
NSCoder aCoder )
{
throw new net.wotonomy.util.WotonomyException("Not implemented yet.");
}
*/
/**
* Returns whether editing contexts are configured to retain strong
* references to their registered objects. If false, editing contexts
* will only retain weak references to their registered objects.
*/
public static boolean instancesRetainRegisteredObjects()
{
return retainsRegisteredObjects;
}
/**
* Sets the global default fetch timestamp lag.
*/
public static void setDefaultFetchTimestampLag (
double aDouble )
{
defaultFetchTimestampLag = aDouble;
}
/**
* Sets the global default parent object store,
* used for the parameterless constructor.
*/
public static void setDefaultParentObjectStore (
EOObjectStore anObjectStore )
{
defaultParentObjectStore = anObjectStore;
}
public static void setInstancesRetainRegisteredObjects (
boolean retainsObjects )
{
retainsRegisteredObjects = retainsObjects;
}
/*
public static void setSubstitutionEditingContext (
EOEditingContext aContext)
{
throw new net.wotonomy.util.WotonomyException("Not implemented yet.");
}
public static void setUsesContextRelativeEncoding (
boolean usesRelativeEncoding )
{
throw new net.wotonomy.util.WotonomyException("Not implemented yet.");
}
public static EOEditingContext substitutionEditingContext ()
{
throw new net.wotonomy.util.WotonomyException("Not implemented yet.");
}
public static boolean usesContextRelativeEncoding ()
{
throw new net.wotonomy.util.WotonomyException("Not implemented yet.");
}
*/
public String toString()
{
return "[EOEditingContext@"+Integer.toHexString(System.identityHashCode(this))+":"+
" inserted="+idsForObjects(insertedObjects)+
" updated="+idsForObjects(updatedObjects)+
" deleted="+idsForObjects(deletedObjects)+
" registered="+registrar.registeredGlobalIDs()+" ]";
}
private List idsForObjects( List objects )
{
List result = new LinkedList();
Iterator i = objects.iterator();
while ( i.hasNext() ) result.add( globalIDForObject( i.next() ) );
return result;
}
// snapshots
/**
* Returns a NSDictionary containing only the mutable properties
* for the specified object and deep clones of their values.
* Nulls are represented by NSNull.nullValue().
*/
private byte[] takeSnapshot( Object anObject )
{ // System.out.println( "takeSnapshot: " + anObject );
return KeyValueCodingUtilities.freeze( anObject, this, anObject, true );
}
/**
* Applies the map of properties and values to the
* specified object. Null values for properties must
* be represented by the NSNull.nullValue().
* Posts a willChange event before applying changes.
*/
private void applySnapshot( byte[] aSnapshot, Object anObject )
{
// must clone snapshot to avoid changing existing snapshot
NSDictionary values = convertSnapshotToDictionary( aSnapshot );
//System.out.println( "applySnapshot: " + aSnapshot + " : " + values );
//ignoreChanges = true;
willChange();
KeyValueCodingUtilities.takeStoredValuesFromDictionary( anObject, values );
//ignoreChanges = false;
}
/**
* Snapshots are stored internally in binary format,
* but exposed to the user as NSDictionaries.
*/
private NSDictionary convertSnapshotToDictionary( byte[] aSnapshot )
{
// get the object
Object clone = KeyValueCodingUtilities.thaw( aSnapshot, this, true );
// get all keys for this object
EOClassDescription classDesc =
EOClassDescription.classDescriptionForClass( clone.getClass() );
List keys = new LinkedList();
keys.addAll( classDesc.attributeKeys() );
keys.addAll( classDesc.toOneRelationshipKeys() );
keys.addAll( classDesc.toManyRelationshipKeys() );
return KeyValueCodingUtilities.valuesForKeys( clone, keys );
}
/**
* Creates a deep clone of the specified object.
* (Object.clone() only creates a shallow clone.)
* Returns null if operation fails.
*/
static private Object clone(
EOEditingContext aSourceContext,
Object aSource,
EOEditingContext aDestinationContext )
{ // System.out.println( "clone: " + aSource );
return KeyValueCodingUtilities.clone(
aSourceContext, aSource, aDestinationContext );
}
/**
* Creates a deep clone of the specified object.
* but does not transpose references. This allows
* us to register an object before transposing
* references so that child objects will be able
* to resolve references to their parent.
* After recording the object, we copy the source
* object into the clone, which does transpose
* and resolve properly.
* Returns null if operation fails.
*/
static private Object registerClone(
EOGlobalID aGlobalID,
EOEditingContext aSourceContext,
Object aSource,
EOEditingContext aDestinationContext )
{
// while we'd like to just transpose/clone at the same time
// we must record a clone without transposing: this
// avoids a endless loop if the object graph has a cycle
Object clone = KeyValueCodingUtilities.thaw(
KeyValueCodingUtilities.freeze(
aSource, aSourceContext, aSource, false ),
aDestinationContext, false );
aDestinationContext.recordObject( clone, aGlobalID );
// need to copy to transpose references into this context
// while preserving the same instance of the object
aDestinationContext.ignoreChanges = true;
copy( aSourceContext, aSource, aDestinationContext, clone );
aDestinationContext.ignoreChanges = false;
// return our clone
return clone;
}
/**
* Copies values from one object to another.
* Returns the destination object, or throws exception
* if operation fails.
*/
static private Object copy(
EOEditingContext aSourceContext,
Object aSource,
EOEditingContext aDestinationContext,
Object aDestination )
{ // System.out.println( "copy: " );
EOObserverCenter.notifyObserversObjectWillChange( aDestination );
KeyValueCodingUtilities.copy( aSourceContext, aSource, aDestinationContext, aDestination );
return aDestination;
}
// process recent changes
/**
* Called to notify observers of changes.
* Also calls runLater().
*/
private void willChange()
{
EOObserverCenter.notifyObserversObjectWillChange( this );
runLater();
}
/**
* Called to ensure that processRecentChanges
* will be called on the next event loop.
*/
private void runLater()
{
if ( ! willRunLater )
{
willRunLater = true;
NSRunLoop.currentRunLoop().performSelectorWithOrder(
runLaterSelector, this, null, EditingContextFlushChangesRunLoopOrdering, null );
}
}
/**
* This method is called by the event queue run loop
* and calls processRecentChanges.
* NOTE: This method is not part of the specification.
*/
public void flushRecentChanges( Object anObject )
{
//System.out.println( "EODelayedObserverQueue: running" );
processRecentChanges();
willRunLater = false;
}
// inner classes
/**
* Gatekeeper for all access to registered objects.
*/
static private class Registrar
{
EOEditingContext context;
NSMutableDictionary IDsToObjects;
NSMutableDictionary objectsToIDs;
NSMutableDictionary objectsToCommitSnapshots;
NSMutableDictionary objectsToCurrentSnapshots;
ReferenceKey comparisonKey; //FIXME not thread safe!
public Registrar( EOEditingContext aContext )
{
context = aContext;
IDsToObjects = new NSMutableDictionary();
objectsToIDs = new NSMutableDictionary();
objectsToCommitSnapshots = new NSMutableDictionary();
objectsToCurrentSnapshots = new NSMutableDictionary();
comparisonKey = new ReferenceKey();
}
public NSArray registeredObjects()
{
return IDsToObjects.allValues();
}
public NSArray registeredGlobalIDs()
{
return IDsToObjects.allKeys();
}
public Object objectForGlobalID( EOGlobalID aGlobalID )
{
return IDsToObjects.objectForKey( aGlobalID );
}
public EOGlobalID globalIDForObject( Object anObject )
{
comparisonKey.set( anObject );
return (EOGlobalID) objectsToIDs.objectForKey( comparisonKey );
}
public byte[] getCommitSnapshot( Object anObject )
{
comparisonKey.set( anObject );
return (byte[]) objectsToCommitSnapshots.objectForKey( comparisonKey );
}
public void setCommitSnapshot( Object anObject, byte[] aSnapshot )
{
if ( aSnapshot == null )
{
comparisonKey.set( anObject );
objectsToCommitSnapshots.removeObjectForKey( comparisonKey );
}
else
{
objectsToCommitSnapshots.setObjectForKey(
aSnapshot, new ReferenceKey( anObject ) );
}
}
public byte[] getCurrentSnapshot( Object anObject )
{
comparisonKey.set( anObject );
return (byte[]) objectsToCurrentSnapshots.objectForKey( comparisonKey );
}
public void setCurrentSnapshot( Object anObject, byte[] aSnapshot )
{
if ( aSnapshot == null )
{
comparisonKey.set( anObject );
objectsToCurrentSnapshots.removeObjectForKey( comparisonKey );
}
else
{
objectsToCurrentSnapshots.setObjectForKey(
aSnapshot, new ReferenceKey( anObject ) );
}
}
public void registerObject( Object anObject, EOGlobalID aGlobalID )
{
IDsToObjects.setObjectForKey( anObject, aGlobalID );
objectsToIDs.setObjectForKey( aGlobalID, new ReferenceKey( anObject ) );
EOObserverCenter.addObserver( context, anObject );
}
public void forgetObject( Object anObject )
{
comparisonKey.set( anObject );
Object id = objectsToIDs.objectForKey( comparisonKey );
IDsToObjects.removeObjectForKey( id );
objectsToIDs.removeObjectForKey( comparisonKey );
EOObserverCenter.removeObserver( context, anObject );
}
public void disposeSnapshots( Object anObject )
{
setCommitSnapshot( anObject, null );
setCurrentSnapshot( anObject, null );
}
}
/**
* Registrar that uses only WeakReferences.
* Used if retainsRegisteredObjects is false.
*/
static private class WeakRegistrar extends Registrar
{
private WeakReferenceKey weakComparisonKey; //FIXME not thread safe!
public WeakRegistrar( EOEditingContext aContext )
{
super( aContext );
weakComparisonKey = new WeakReferenceKey();
}
public NSArray registeredObjects()
{
Object object;
WeakReferenceKey weakKey;
NSMutableArray result = new NSMutableArray();
Enumeration e = new NSArray( objectsToIDs.allKeys() ).objectEnumerator();
while ( e.hasMoreElements() )
{
weakKey = (WeakReferenceKey) e.nextElement();
object = weakKey.get();
if ( object != null )
{
result.addObject( object );
}
else
{
// object has been released: perform cleanup
disposeObject( null, weakKey );
}
}
return result;
}
public Object objectForGlobalID( EOGlobalID aGlobalID )
{
WeakReference ref = (WeakReference) super.objectForGlobalID( aGlobalID );
if ( ref == null ) return null;
Object result = ref.get();
if ( result == null )
{
// clean up manually
IDsToObjects.removeObjectForKey( aGlobalID );
Iterator i = new LinkedList( objectsToIDs.allKeysForObject( ref ) ).iterator();
while ( i.hasNext() )
{
objectsToIDs.removeObjectForKey( i.next() );
}
disposeSnapshots( aGlobalID );
}
return result;
}
public byte[] getCommitSnapshot( Object anObject )
{
weakComparisonKey.set( anObject );
return (byte[]) objectsToCommitSnapshots.objectForKey( weakComparisonKey );
}
public void setCommitSnapshot( Object anObject, byte[] aSnapshot )
{
if ( aSnapshot == null )
{
weakComparisonKey.set( anObject );
objectsToCommitSnapshots.removeObjectForKey( weakComparisonKey );
}
else
{
objectsToCommitSnapshots.setObjectForKey(
aSnapshot, new WeakReferenceKey( anObject ) );
}
}
public byte[] getCurrentSnapshot( Object anObject )
{
weakComparisonKey.set( anObject );
return (byte[]) objectsToCurrentSnapshots.objectForKey( weakComparisonKey );
}
public void setCurrentSnapshot( Object anObject, byte[] aSnapshot )
{
if ( aSnapshot == null )
{
weakComparisonKey.set( anObject );
objectsToCurrentSnapshots.removeObjectForKey( weakComparisonKey );
}
else
{
objectsToCurrentSnapshots.setObjectForKey(
aSnapshot, new WeakReferenceKey( anObject ) );
}
}
public void registerObject( Object anObject, EOGlobalID aGlobalID )
{ // new net.wotonomy.ui.swing.ReferenceInspector( anObject );
IDsToObjects.setObjectForKey( new WeakReference( anObject ), aGlobalID );
objectsToIDs.setObjectForKey( aGlobalID, new WeakReferenceKey( anObject ) );
EOObserverCenter.addObserver( context, anObject );
}
public void forgetObject( Object anObject )
{
disposeObject( anObject, null );
}
// must specify one or the other
private void disposeObject( Object anObject, WeakReferenceKey key )
{
if ( key == null ) key = new WeakReferenceKey( anObject );
EOGlobalID id = (EOGlobalID) objectsToIDs.objectForKey( key );
if ( id != null ) IDsToObjects.removeObjectForKey( id );
objectsToIDs.removeObjectForKey( key );
disposeSnapshots( id );
if ( anObject != null )
{
EOObserverCenter.removeObserver( context, anObject );
}
}
}
/**
* Private class used to force a hashmap to
* perform key comparisons by reference.
*/
static private class ReferenceKey
{
int hashCode;
Object referent;
public ReferenceKey()
{
referent = null;
hashCode = -1;
}
public ReferenceKey( Object anObject )
{
set( anObject );
}
public Object get()
{
return referent;
}
public void set( Object anObject )
{
referent = anObject;
hashCode = anObject.hashCode();
}
/**
* Returns the actual key's hash code.
*/
public int hashCode()
{
return hashCode;
}
/**
* Compares by reference.
*/
public boolean equals( Object anObject )
{
if ( anObject == this ) return true;
if ( anObject instanceof ReferenceKey )
{
return ((ReferenceKey)anObject).get() == referent;
}
return false;
}
}
/**
* Private class used to force a hashmap to
* perform key comparisons by reference.
*/
static private class WeakReferenceKey extends ReferenceKey
{
public WeakReferenceKey()
{
super();
}
public WeakReferenceKey( Object anObject )
{
super( anObject );
}
public Object get()
{
return ((WeakReference)referent).get();
}
public void set( Object anObject )
{
referent = new WeakReference( anObject );
hashCode = anObject.hashCode();
}
/**
* Compares by reference.
*/
public boolean equals( Object anObject )
{
if ( anObject == this ) return true;
if ( anObject instanceof ReferenceKey )
{
return ((ReferenceKey)anObject).get() == get();
}
return false;
}
}
/**
* Key combining an object with a string.
* Object is compared by reference.
*/
static private class CompoundKey
{
private Object object;
private String string;
private int hashCode;
/**
* Creates compound key.
* Neither name nor object may be null.
*/
public CompoundKey (
Object anObject, String aString )
{
object = anObject;
string = aString;
hashCode = object.hashCode() + string.hashCode();
}
public int hashCode()
{
return hashCode;
}
public boolean equals( Object anObject )
{
if ( anObject instanceof CompoundKey )
{
CompoundKey key = (CompoundKey) anObject;
return ( ( key.object == object ) && ( key.string.equals( string ) ) );
}
return false;
}
public String toString()
{
return "[CompoundKey:"+object+":"+string+"]";
}
}
/**
* Used by EditingContext to delegate behavior to another class.
* Note that EditingContext doesn't require its delegates to implement
* this interface: rather, this interface defines the methods that
* EditingContext will attempt to invoke dynamically on its delegate.
* The delegate may choose to implement only a subset of the methods
* on the interface.
*/
public interface Delegate
{
/**
* Called after the editing context has completed merge operations
* on one or more objects after receiving an ObjectChangedInStore
* notification.
*/
void editingContextDidMergeChanges(
EOEditingContext anEditingContext );
/**
* Called by objectsWithFetchSpecification.
* If null, the editing context will pass the fetch specification
* on to its parent store, as normal. Otherwise, the context
* will use the returned array to service the request.
*/
NSArray editingContextShouldFetchObjects(
EOEditingContext anEditingContext,
EOFetchSpecification fetchSpecification );
/**
* Called to determine whether an object should be invalidated.
* Return false to prevent the object from being invalidated.
* Default is true.
*/
boolean editingContextShouldInvalidateObject(
EOEditingContext anEditingContext,
Object anObject,
EOGlobalID aGlobalID );
/**
* Called to determine whether the editing context should attempt
* to merge changes in the specified object that the parent store
* says has changed via an ObjectChangedInStore notification.
* Default is true. Return false if you wish to handle the merge
* yourself, by extracting the values in the object now and comparing
* them to the values when editingContextDidMergeChanges is called.
*/
boolean editingContextShouldMergeChangesForObject(
EOEditingContext anEditingContext,
Object anObject );
/**
* Returns whether the editing context should ask its message handler
* to display a message. Return false if the delegate will display the error.
* Default is true.
*/
boolean editingContextShouldPresentException(
EOEditingContext anEditingContext,
Throwable exception );
/**
* Returns whether the editing context should undo the most
* recent set of changes that resulted in a validation failure.
* Default is true.
*/
boolean editingContextShouldUndoUserActionsAfterFailure(
EOEditingContext anEditingContext );
/**
* Returns whether the editing context should validate the
* most recent set of changes. Default is true.
*/
boolean editingContextShouldValidateChanges(
EOEditingContext anEditingContext );
/**
* Called before the editing context saves its changes
* to the parent object store.
*/
void editingContextWillSaveChanges(
EOEditingContext anEditingContext );
}
/**
* Editors register themselves with the editing context
* so that they may receive notification before the context
* commits changes. This is useful for associations whose
* components do not immediately commit their changes to
* the object they are editing.
*/
public interface Editor
{
/**
* Called before the editing context saves its changes
* to the parent object store.
*/
void editingContextWillSaveChanges(
EOEditingContext anEditingContext );
/**
* Called to determine whether this editor has changes
* that have not been committed to the object in the context.
*/
boolean editorHasChangesForEditingContext(
EOEditingContext anEditingContext );
}
/**
* Used by EditingContext to delegate messaging handling to another class,
* typically the display group that has the currently active association.
* Note that EditingContext doesn't require its message handlers to implement
* this interface: rather, this interface defines the methods that
* EditingContext will attempt to invoke dynamically on its delegate.
* The delegate may choose to implement only a subset of the methods
* on the interface.
*/
public interface MessageHandler
{
/**
* Called to display a message for an error that occurred
* in the specified editing context.
*/
void editingContextPresentErrorMessage(
EOEditingContext anEditingContext,
String aMessage );
/**
* Called by the specified object store to determine whether
* fetching should continue, where count is the current count
* and limit is the limit as specified by the fetch specification.
* Default is false.
*/
boolean editingContextShouldContinueFetching(
EOEditingContext anEditingContext,
int count,
int limit,
EOObjectStore anObjectStore );
}
}
/*
* $Log$
* Revision 1.2 2006/02/16 16:47:14 cgruber
* Move some classes in to "internal" packages and re-work imports, etc.
*
* Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions.
*
* Revision 1.1 2006/02/16 13:19:57 cgruber
* Check in all sources in eclipse-friendly maven-enabled packages.
*
* Revision 1.86 2003/12/18 15:37:38 mpowers
* Changes to retain ability to work with objects that don't necessarily
* implement EOEnterpriseObject. I would still like to preserve this case
* for general usage, however the access package is free to assume that
* those objects will be EOs and cast appropriately.
*
* Revision 1.85 2003/08/19 01:53:12 chochos
* EOObjectStore had some incompatible return types (Object instead of EOEnterpriseObject, in fault methods mostly). It's internally consistent but I hope it doesn't break anything based on this, even though fault methods mostly throw exceptions for now.
*
* Revision 1.84 2003/08/06 23:07:52 chochos
* general code cleanup (mostly, removing unused imports)
*
* Revision 1.83 2003/02/13 15:24:33 mpowers
* hasChanges is now derived, not tracked.
* refaultObject now more consistently removes object from updated list.
*
* Revision 1.82 2002/12/16 15:46:00 mpowers
* Major refactoring to implement setInstancesRetainRegisteredObjects().
*
* Revision 1.81 2002/11/18 22:10:58 mpowers
* Now resetting hasChanges flag on reset.
*
* Revision 1.80 2002/10/24 21:15:33 mpowers
* New implementations of NSArray and subclasses.
*
* Revision 1.79 2002/10/24 18:18:12 mpowers
* NSArray's are now considered read-only, so we can return our internal
* representation to reduce unnecessary object allocation.
*
* Revision 1.78 2002/06/21 21:44:33 mpowers
* No longer marking deleted objects as updated (thanks to dwang).
*
* Revision 1.77 2002/05/20 15:10:17 mpowers
* No longer refaulting if delegate does not wish to handle the merge.
*
* Revision 1.76 2002/03/26 21:46:06 mpowers
* Contributing EditingContext as a java-friendly convenience.
*
* Revision 1.75 2002/03/06 16:14:57 mpowers
* More attempts at ignoring update conflicts that come from ourself.
*
* Revision 1.74 2002/02/21 21:57:50 mpowers
* Implemented default merge behavior.
*
* Revision 1.73 2002/02/20 16:46:54 mpowers
* Implemented better support for EOEditingContext.Delegate.
*
* Revision 1.70 2002/02/19 22:26:05 mpowers
* Implemented EOEditingContext.MessageHandler support.
*
* Revision 1.69 2002/02/19 16:33:42 mpowers
* Implemented support for EditingContext.Editor
*
* Revision 1.68 2002/02/13 22:00:34 mpowers
* Fixed: invalidateAllObjects tries to invalidate inserted objects,
* typically causing class cast exceptions involving EOTemporaryGlobalID.
*
* Revision 1.67 2002/02/06 21:15:35 mpowers
* No longer refaulting a dirty object when we receive an invalidation notif.
*
* Revision 1.66 2002/01/08 19:31:03 mpowers
* refaultObject now correctly refaults the object.
*
* Revision 1.65 2001/12/20 18:56:15 mpowers
* Refinements to snapshotting and calling processRecentChanges.
*
* Revision 1.64 2001/12/10 15:11:41 mpowers
* Now only tracking a commit snapshot after an object has been modified.
*
* Revision 1.63 2001/11/14 00:08:10 mpowers
* Now marking context changed when objects are inserted or deleted
* and when child contexts save their changes into this context.
*
* Revision 1.62 2001/11/07 14:49:31 mpowers
* invalidateAllObjects now handles objects manually discarded in the course
* of invalidation.
*
* Revision 1.61 2001/10/26 20:02:49 mpowers
* No longer using NSNotificationQueue: all notifications are posted immed.
*
* Revision 1.60 2001/10/26 18:37:50 mpowers
* Now using NSRunLoop to correctly flush recent changes before delayed
* observers and AWT events.
*
* Revision 1.59 2001/10/23 22:29:59 mpowers
* Now posting notifications immediately.
* Recent changes are flushed at ObserverPrioritySixth, soon to change.
*
* Revision 1.58 2001/09/10 14:16:51 mpowers
* EditingContexts now relay InvalidatedAllObjectsInStore notifications.
*
* Revision 1.57 2001/06/18 14:11:15 mpowers
* Inserting a deleted object simply cancels the delete operation.
*
* Revision 1.56 2001/06/07 22:07:59 mpowers
* Now handling delete notifications before update notifications.
* Eliminated the case where deleted objects were also being put
* on the updated list when notifying child contexts.
*
* Revision 1.55 2001/05/18 21:04:33 mpowers
* Reimplemented EditingContext.initializeObject.
*
* Revision 1.54 2001/05/05 23:05:42 mpowers
* Implemented Array Faults.
*
* Revision 1.53 2001/05/05 15:00:06 mpowers
* Tested load-on-demand: still works.
* Now using registerClone for consistency.
* Editing context is temporarily posting notification on objectWillChange.
*
* Revision 1.52 2001/05/05 14:11:48 mpowers
* Implemented registerClone.
*
* Revision 1.51 2001/05/04 16:57:56 mpowers
* Now correctly transposing references to editing contexts when
* cloning/copying between editing contexts.
*
* Revision 1.50 2001/05/02 17:58:41 mpowers
* Removed debugging code, added comments.
*
* Revision 1.49 2001/05/02 15:47:40 mpowers
* Fixed the pernicious problem with reverts: recordObject was recording
* a snapshot of the clone before the transposition-copy happened,
* so the revert object would lose all of its transposed relationships.
*
* Revision 1.48 2001/05/02 12:39:05 mpowers
* Fixed a nasty problem with transpose-cloning and faultForGlobalID.
* Now we're forced to create a deep clone, registered it, and then
* transpose it after it has been registered.
*
* Revision 1.47 2001/04/30 13:15:24 mpowers
* Child contexts re-initializing objects invalidated in parent now
* propery transpose relationships.
*
* Revision 1.46 2001/04/29 22:02:45 mpowers
* Work on id transposing between editing contexts.
*
* Revision 1.45 2001/04/29 02:29:31 mpowers
* Debugging relationship faulting.
*
* Revision 1.44 2001/04/28 16:18:44 mpowers
* Implementing relationships.
*
* Revision 1.43 2001/04/28 14:12:23 mpowers
* Refactored cloning/copying into KeyValueCodingUtilities.
*
* Revision 1.42 2001/04/27 00:27:11 mpowers
* Provided description to not-implemented exceptions.
*
* Revision 1.41 2001/04/26 01:16:44 mpowers
* Major bug fix so we no longer accumulate objects in the all objects
* list with every InvalidateAllObjectsInStore.
*
* Revision 1.40 2001/04/21 23:07:49 mpowers
* Now only broadcasts notifications if there's actually a change.
*
* Revision 1.39 2001/04/13 16:33:11 mpowers
* Corrected the refaulting behavior.
*
* Revision 1.38 2001/04/09 21:42:10 mpowers
* Debugging and optimizing notifications.
*
* Revision 1.37 2001/04/08 20:59:47 mpowers
* objectsForFetchSpecification now relies on faultForGlobalID.
*
* Revision 1.36 2001/04/03 20:36:01 mpowers
* Fixed refaulting/reverting/invalidating to be self-consistent.
*
* Revision 1.35 2001/03/29 03:29:49 mpowers
* Now using KeyValueCoding and Support instead of Introspector.
*
* Revision 1.34 2001/03/28 14:06:29 mpowers
* Implemented snapshots. Revert now uses snapshots.
*
* Revision 1.33 2001/03/20 23:20:33 mpowers
* invalidating all objects now sets the dirty flag to false.
*
* Revision 1.32 2001/03/19 21:44:36 mpowers
* Reverts reinitialize for now.
* Testing for inserted objects instead of temp id when invalidating object.
*
* Revision 1.31 2001/03/15 21:10:26 mpowers
* Implemented global id re-registration for newly saved inserts.
*
* Revision 1.30 2001/03/13 21:41:34 mpowers
* Broadcasting willChange for any change to hasChanges.
* Fixed major bug with inserted objects treated as updated objects
* in child display groups.
*
* Revision 1.29 2001/03/09 22:10:30 mpowers
* Fine tuned initializeObject.
*
* Revision 1.28 2001/03/06 23:23:55 mpowers
* objectForGlobalID now returns null if not found.
* objectsForFetchSpecification again does things the old way, for now.
*
* Revision 1.27 2001/03/02 16:31:45 mpowers
* Trying to better handle fetches from child contexts.
* No longer trying to invalidate temporary objects.
*
* Revision 1.26 2001/02/28 16:25:19 mpowers
* Now calling globalIDForObject internally.
*
* Revision 1.25 2001/02/27 17:36:55 mpowers
* Objects inserted from child now preserve the existing temporary id.
*
* Revision 1.24 2001/02/27 02:11:17 mpowers
* Now throwing exception when cloning fails.
* Removed debugging printlns.
*
* Revision 1.23 2001/02/26 22:41:51 mpowers
* Implemented null placeholder classes.
* Duplicator now uses NSNull.
* No longer catching base exception class.
*
* Revision 1.22 2001/02/26 21:18:45 mpowers
* Now marking edited objects from child contexts that were not already
* recorded in parent as changed in saveChangesInEditingContext.
*
* Revision 1.21 2001/02/26 15:53:22 mpowers
* Fine-tuning notification firing.
* Child display groups now update properly after parent save or invalidate.
*
* Revision 1.20 2001/02/24 17:03:22 mpowers
* Implemented the notification queue, and changed editing context to use it.
*
* Revision 1.19 2001/02/23 23:44:15 mpowers
* Fine-tuning notification handling.
*
* Revision 1.18 2001/02/22 22:56:57 mpowers
* Only refaulting edited objects on parent store invalidateAll.
*
* Revision 1.17 2001/02/22 20:54:39 mpowers
* Implemented notification handling.
*
* Revision 1.16 2001/02/21 22:10:55 mpowers
* Editing context is now posting appropriate notifications.
*
* Revision 1.15 2001/02/21 21:17:32 mpowers
* Now retaining a reference to the recent changes observer.
* Better documented need to retain reference.
* Started implementing notifications.
*
* Revision 1.14 2001/02/20 17:24:22 mpowers
* Now using reference keys in objectToID.
*
* Revision 1.13 2001/02/20 16:45:36 mpowers
* Child data sources now accept a data source instead of an editing context
* for more flexibility. Child data sources now forward relationship
* methods to parent source.
*
* Revision 1.12 2001/02/16 22:51:29 mpowers
* Now deep-cloning objects passed between editing contexts.
*
* Revision 1.11 2001/02/16 18:34:19 mpowers
* Implementing nested contexts.
*
* Revision 1.9 2001/02/15 21:13:30 mpowers
* First draft implementation is complete. Now on to debugging.
*
* Revision 1.7 2001/02/13 23:24:29 mpowers
* Implementing more of editing context.
*
* Revision 1.4 2001/02/09 22:09:34 mpowers
* Completed implementation of EOObjectStore.
*
* Revision 1.3 2001/02/06 15:24:11 mpowers
* Widened parameters on abstract method to fix build.
*
* Revision 1.2 2001/02/05 03:45:37 mpowers
* Starting work on EOEditingContext.
*
* Revision 1.1.1.1 2000/12/21 15:46:42 mpowers
* Contributing wotonomy.
*
*/