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