/*
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;
// TODO I think this is EOGlobalID but not certain
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(EOEditingContext.Editor 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.
*/
@Override
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.
*/
@Override
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 = 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.
*/
@Override
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.
*/
@Override
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();
for (Object obj : anObjectList) {
result.add(globalIDForObject(obj));
}
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.
*/
@Override
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
// TODO is this
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.
*/
@Override
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 = 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.
*/
@Override
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 = 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.
*/
@Override
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.
*/
@Override
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.
*/
@Override
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.
*/
@Override
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.
*/
@Override
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 = 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());
}
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;
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.
*/
@Override
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 = 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 = (NSDictionary>) aNotification.userInfo();
// inserted objects are ignored
// existing deleted objects are removed
NSArray deletes = userInfo.objectForKey(EOObjectStore.DeletedKey);
e = deletes.objectEnumerator();
while (e.hasMoreElements()) {
id = 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 = userInfo.objectForKey(EOObjectStore.UpdatedKey);
e = updates.objectEnumerator();
while (e.hasMoreElements()) {
id = 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 = userInfo.objectForKey(EOObjectStore.InvalidatedKey);
e = invalidates.objectEnumerator();
while (e.hasMoreElements()) {
id = 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.
*/
@Override
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."); }
*/
@Override
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();
}
@Override
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;
}
@Override
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;
}
@Override
public byte[] getCommitSnapshot(Object anObject) {
weakComparisonKey.set(anObject);
return (byte[]) objectsToCommitSnapshots.objectForKey(weakComparisonKey);
}
@Override
public void setCommitSnapshot(Object anObject, byte[] aSnapshot) {
if (aSnapshot == null) {
weakComparisonKey.set(anObject);
objectsToCommitSnapshots.removeObjectForKey(weakComparisonKey);
} else {
objectsToCommitSnapshots.setObjectForKey(aSnapshot, new WeakReferenceKey(anObject));
}
}
@Override
public byte[] getCurrentSnapshot(Object anObject) {
weakComparisonKey.set(anObject);
return (byte[]) objectsToCurrentSnapshots.objectForKey(weakComparisonKey);
}
@Override
public void setCurrentSnapshot(Object anObject, byte[] aSnapshot) {
if (aSnapshot == null) {
weakComparisonKey.set(anObject);
objectsToCurrentSnapshots.removeObjectForKey(weakComparisonKey);
} else {
objectsToCurrentSnapshots.setObjectForKey(aSnapshot, new WeakReferenceKey(anObject));
}
}
@Override
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);
}
@Override
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.
*/
@Override
public int hashCode() {
return hashCode;
}
/**
* Compares by reference.
*/
@Override
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);
}
@Override
public Object get() {
return ((WeakReference) referent).get();
}
@Override
public void set(Object anObject) {
referent = new WeakReference(anObject);
hashCode = anObject.hashCode();
}
/**
* Compares by reference.
*/
@Override
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();
}
@Override
public int hashCode() {
return hashCode;
}
@Override
public boolean equals(Object anObject) {
if (anObject instanceof CompoundKey) {
CompoundKey key = (CompoundKey) anObject;
return ((key.object == object) && (key.string.equals(string)));
}
return false;
}
@Override
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.
*
*/