/* 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())) { @SuppressWarnings("unchecked") NSDictionary userInfo = (NSDictionary) 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. * */