/* Wotonomy: OpenStep design patterns for pure Java applications. Copyright (C) 2001 Michael Powers This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, see http://www.gnu.org */ package net.wotonomy.control; import java.lang.ref.WeakReference; import java.util.Collection; import java.util.Enumeration; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import net.wotonomy.foundation.NSArray; import net.wotonomy.foundation.NSDictionary; import net.wotonomy.foundation.NSMutableArray; import net.wotonomy.foundation.NSMutableDictionary; import net.wotonomy.foundation.NSNotification; import net.wotonomy.foundation.NSNotificationCenter; import net.wotonomy.foundation.NSRunLoop; import net.wotonomy.foundation.NSSelector; import net.wotonomy.foundation.internal.WotonomyException; // swing dependency for undo manager //import javax.swing.undo.UndoManager; /** * EOEditingContext provides transactional support for * fetching, editing, and committing changes made on a * collection of objects to a parent object store.

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