From aedc34d55462a75e329bbf342251ff6504cd117e Mon Sep 17 00:00:00 2001 From: Benjamin Culkin Date: Sun, 19 May 2024 17:56:33 -0400 Subject: Initial import from SVN --- .../net/wotonomy/control/AbstractObjectStore.java | 762 +++++++++++++++++++++ 1 file changed, 762 insertions(+) create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/AbstractObjectStore.java (limited to 'projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/AbstractObjectStore.java') diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/AbstractObjectStore.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/AbstractObjectStore.java new file mode 100644 index 0000000..ddaacf5 --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/AbstractObjectStore.java @@ -0,0 +1,762 @@ +/* +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.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSMutableArray; +import net.wotonomy.foundation.NSMutableDictionary; +import net.wotonomy.foundation.NSNotification; +import net.wotonomy.foundation.NSNotificationCenter; +import net.wotonomy.foundation.NSNotificationQueue; +import net.wotonomy.foundation.NSSelector; +import net.wotonomy.foundation.internal.WotonomyException; + +/** +* An abstract implementation of object store that +* implements common functionality. Subclasses must +* implement data object creation, initialization, and +* refault logic, as well as logic to commit an editing +* context. +*/ +public abstract class AbstractObjectStore extends EOObjectStore +{ + private NSMutableArray insertedIDsBuffer; + private NSMutableArray updatedIDsBuffer; + private NSMutableArray deletedIDsBuffer; + private NSMutableArray invalidatedIDsBuffer; + + private Map snapshots; + private List exceptionList; + + /** + * Constructs a new instance of this object store. + */ + public AbstractObjectStore() + { + snapshots = new HashMap(); + exceptionList = null; + + insertedIDsBuffer = new NSMutableArray(); + updatedIDsBuffer = new NSMutableArray(); + deletedIDsBuffer = new NSMutableArray(); + invalidatedIDsBuffer = new NSMutableArray(); + + // register for notifications + NSSelector handleNotification = + new NSSelector( "handleNotification", + new Class[] { NSNotification.class } ); + NSNotificationCenter.defaultCenter().addObserver( + this, + handleNotification, + EOClassDescription.ClassDescriptionNeededForEntityNameNotification, + null ); + } + + /** + * This implementation returns an appropriately configured array fault. + */ + public NSArray arrayFaultWithSourceGlobalID( EOGlobalID aGlobalID, + String aRelationship, EOEditingContext aContext ) + { // System.out.println( "arrayFaultWithSourceGlobalID: " + aGlobalID + " : " + aRelationship ); + return new ArrayFault( + aGlobalID, aRelationship, aContext ); + } + + /** + * This implementation returns the actual object for the specified id. + */ + public /*EOEnterpriseObject*/Object faultForGlobalID( EOGlobalID aGlobalID, + EOEditingContext aContext ) + { // System.out.println( "faultForGlobalID: " + aGlobalID ); + return /*(EOEnterpriseObject)*/createInstanceWithEditingContext( aGlobalID, aContext ); + } + + /** + * 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. + * NOTE: Faults are not supported yet. + */ + public /*EOEnterpriseObject*/Object faultForRawRow( Map aDictionary, String anEntityName, + EOEditingContext aContext ) + { + //TODO: raw rows are not yet supported + throw new WotonomyException( "Faults are not yet supported." ); + } + + /** + * Given a newly instantiated object, this method initializes its + * properties to values appropriate for the specified id. The object + * should belong to the specified editing context. This method is called + * to populate faults. + */ + public void initializeObject( Object anObject, EOGlobalID aGlobalID, + EOEditingContext aContext ) + { //System.out.println( "initializeObject: " + aGlobalID ); + try + { + String entity = entityForGlobalIDOrObject( aGlobalID, null ); + EOClassDescription classDesc = + EOClassDescription.classDescriptionForEntityName( entity ); + if ( classDesc == null ) + { + throw new WotonomyException( "Unknown entity type: " + entity ); + } + + Collection attributes = classDesc.attributeKeys(); + Map data = readFromCache( aGlobalID, attributes ); + String key; + Iterator iterator = attributes.iterator(); + while ( iterator.hasNext() ) + { + key = iterator.next().toString(); + + // write the snapshot's reference into the object + if ( anObject instanceof EOKeyValueCoding ) + { + ((EOKeyValueCoding)anObject).takeStoredValueForKey( data.get( key ), key ); + } + else + { + EOKeyValueCodingSupport.takeStoredValueForKey( anObject, data.get( key ), key ); + } + + //NOTE: our objects are expected to make a copy + // of their data before it is modified, so it's okay + // to return them our copy of the data: + // we trust that they will not modify it. + } + } + catch ( Exception exc ) + { + exc.printStackTrace(); + } + } + + /** + * Reads the local data snapshot for the specified id. + * If no snapshot exists, a new snapshot is created. + * If the specified keys are not in the snapshot, + * new data is fetched into the snapshot. + * If null is specified, all known keys are returned. + * Will not return null. + * Result will have values for those keys and only + * those keys requested. Missing keys indicate an + * error occurred. + */ + protected Map readFromCache( EOGlobalID aGlobalID, Collection keys ) + { + Map snapshot = (Map) snapshots.get( aGlobalID ); + + // if no snapshot for this id, create an empty one + if ( snapshot == null ) + { + snapshot = new HashMap(); + snapshots.put( aGlobalID, snapshot ); + } + + // if we don't have all the necessary keys + if ( ( keys == null ) || ( ! snapshot.keySet().containsAll( keys ) ) ) + { + // we need to make a server call + try + { + Map data = readObject( aGlobalID, keys ); + + // compare timestamps + Comparable localTimestamp = (Comparable) timestampForData( snapshot ); + // if our local snapshot has an timestamp (new snapshots don't have timestamp) + if ( localTimestamp != null ) + { + Comparable incomingTimestamp = (Comparable) timestampForData( data ); + if ( incomingTimestamp == null ) + { + // not allowed to happen + new RuntimeException( "Server returned data without an timestamp" ).printStackTrace(); + // however, we can just assume it's a newer timestamp and continue + } + + // if timestamps don't match + if ( ( incomingTimestamp == null ) || ( ! incomingTimestamp.equals( localTimestamp ) ) ) + { + // dump our existing snapshot's data + snapshot.clear(); + // queue for a notification on this oid as updated + //TODO: implement this + } + } + + // copy new data into our local snapshot + snapshot.putAll( data ); + } + catch ( Exception exc ) + { + exc.printStackTrace(); + } + } + + // return just the requested keys from our updated snapshot + Map result = new HashMap(); + if ( keys == null ) + { + result.putAll( snapshot ); + } + else + { + Object key; + Iterator iterator = keys.iterator(); + while ( iterator.hasNext() ) + { + key = iterator.next(); + result.put( key, snapshot.get( key ) ); + } + } + return snapshot; + } + + /** + * Returns a comparable object (typically a Date or Long) for + * the given data map or snapshot. This is used to determine + * whether a local snapshot should be dumped in favor of fetched + * data from the server. + * Returns null if no timestamp can be determined, in which + * case the fetched data will assumed to be more recent than + * any local snapshot. + */ + abstract protected Comparable timestampForData( Map aDataMap ); + + /** + * Extracts the global id for the fetched data or snapshot. + * Some entities have multi-attribute keys that would be + * assembled into a single instance of EOGlobalID. + */ + abstract protected EOGlobalID globalIDForData( Map aDataMap ); + + /** + * Returns the entity that corresponds to the specified global id + * and/or object. Either may be null, but both will not be null. + * //FIXME: This is less than elegant. + */ + abstract protected String entityForGlobalIDOrObject( + EOGlobalID aGlobalID, Object anObject ); + + /** + * Returns the keys that have changed on the specified object. + * If null, all keys are presumed changed, including relationships. + */ + abstract protected Collection changedKeysForObject( Object anObject ); + + /** + * Returns the data for the row corresponding to the specified id + * containing at least the specified keys. Implementations are allowed + * to return more data than requested, and callers are advised to take + * advantage of the returned data. + */ + abstract protected Map readObject( EOGlobalID aGlobalID, Collection keys ); + + /** + * Returns the data for the row corresponding to the specified id. + * //TODO: Need a better return value? How to return invalidated list? + */ + abstract protected Map insertObject( EOGlobalID aGlobalID, Map aDataMap ); + + /** + * Returns the data for the row corresponding to the specified id. + * //TODO: Need a better return value? How to return invalidated list? + */ + abstract protected Object updateObject( EOGlobalID aGlobalID, Map aDataMap ); + + /** + * Returns the data for the row corresponding to the specified id. + * //TODO: Need a better return value? How to return invalidated list? + */ + abstract protected Object deleteObject( EOGlobalID aGlobalID ); + + /** + * Creates a new instance of an object that corresponds to the + * specified global id and is registered in the specified context. + * This implementation extracts the entity type from getEntityForGlobaID + * and construct a new instance from the class description that + * corresponds to the entity type. Override to change this behavior. + */ + protected Object createInstanceWithEditingContext( + EOGlobalID aGlobalID, EOEditingContext aContext ) + { + String entity = entityForGlobalIDOrObject( aGlobalID, null ); + EOClassDescription classDesc = + EOClassDescription.classDescriptionForEntityName( entity ); + if ( classDesc == null ) + { + throw new WotonomyException( "Unknown entity type: " + entity ); + } + + Object result = classDesc.createInstanceWithEditingContext( aContext, aGlobalID ); + if ( result instanceof EOFaulting ) + { + ((EOFaulting)result).turnIntoFault( null ); + } + return result; + } + + /** + * Dumps the snapshot corresponding to the specified id. + */ + protected void invalidateObject( EOGlobalID aGlobalID ) + { + snapshots.remove( aGlobalID ); + } + + /** + * Dumps all snapshots. + */ + protected void invalidateAllCache() + { + snapshots.clear(); + } + + /** + * Remove all values from all objects in memory, turning them into faults, + * and posts a notification that all objects have been invalidated. + */ + public void invalidateAllObjects() + { + invalidateAllCache(); + + // post notification + NSNotificationQueue.defaultQueue().enqueueNotification( + new NSNotification( + InvalidatedAllObjectsInStoreNotification, this ), + NSNotificationQueue.PostNow ); + } + + /** + * Removes values with the specified ids from memory, turning them into + * faults, and posts a notification that those objects have been invalidated. + */ + public void invalidateObjectsWithGlobalIDs( List aList ) + { + NSArray empty = new NSArray(); + NSMutableArray invalidated = new NSMutableArray(); + + Object object; + Iterator iterator = aList.iterator(); + while ( iterator.hasNext() ) + { + object = iterator.next(); + invalidateObject( (EOGlobalID) object ); + invalidated.addObject( object ); + } + + NSMutableDictionary info = new NSMutableDictionary(); + info.setObjectForKey( empty, InsertedKey ); + info.setObjectForKey( empty, UpdatedKey ); + info.setObjectForKey( empty, DeletedKey ); + info.setObjectForKey( invalidated, InvalidatedKey ); + + // post notification + NSNotificationQueue.defaultQueue(). + enqueueNotificationWithCoalesceMaskForModes( new NSNotification( + ObjectsChangedInStoreNotification, this, info ), + NSNotificationQueue.PostNow, + NSNotificationQueue.NotificationNoCoalescing, null ); + } + + /** + * Returns false because locking is not currently permitted. + */ + public boolean isObjectLockedWithGlobalID( EOGlobalID aGlobalID, + EOEditingContext aContext ) + { + return false; + } + + /** + * Does nothing because locking is not currently permitted. + */ + public void lockObjectWithGlobalID( EOGlobalID aGlobalID, + EOEditingContext aContext ) + { + // does nothing + } + + /** + * Returns a 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 aRelationship, EOEditingContext aContext ) + { // System.out.println( "objectsForSourceGlobalID: " + aGlobalID + " : " + aRelationship + " : " ); + + Map snapshot = readFromCache( aGlobalID, new NSArray( aRelationship ) ); + Object value = snapshot.get( aRelationship ); + if ( value == null ) value = new NSArray(); // empty list + if ( ! ( value instanceof Collection ) ) + { + throw new RuntimeException( "Specified relationship is not a collection: " + + aRelationship + " : " + aGlobalID + " : " + value ); + } + + NSArray result = new NSMutableArray(); + + // get fault for each id + EOGlobalID id; + Object fault; + Iterator iterator = ((Collection)value).iterator(); + while ( iterator.hasNext() ) + { + id = (EOGlobalID) iterator.next(); + + // get registered fault + fault = aContext.faultForGlobalID( id, aContext ); + + // assert fault + if ( fault == null ) + { + // this should never happen + throw new RuntimeException( + "Could not find fault for ID: " + id ); + } + + result.add( fault ); + } + + fireObjectsChangedInStore(); + +//System.out.println( "done" ); + return result; + } + + /** + * Returns a List of objects the meet the criteria of + * the supplied specification. Faults are not allowed in the array. + * Each object is registered with the specified editing context. + * If any object is already fetched in the specified context, + * it is not refetched and that object should be used in the array. + */ + public NSArray objectsWithFetchSpecification( + EOFetchSpecification aFetchSpec, EOEditingContext aContext ) + { + NSMutableArray result = new NSMutableArray(); + + //TODO: implement this + + return result; + } + + /** + * Fires ObjectsChangedInStoreNotification + * with contents of buffers and then clears buffers. + * If buffers are empty, does nothing. + */ + private void fireObjectsChangedInStore() + { + // check for changes to broadcast + if ( insertedIDsBuffer.size() + updatedIDsBuffer.size() + + deletedIDsBuffer.size() + invalidatedIDsBuffer.size() == 0 ) + { + return; + } + + // broadcast ObjectsChangedInStoreNotification + // for the benefit of child editing contexts + + NSMutableDictionary storeInfo = new NSMutableDictionary(); + + storeInfo.setObjectForKey( + new NSArray( (Collection) insertedIDsBuffer ), + EOObjectStore.InsertedKey ); + storeInfo.setObjectForKey( + new NSArray( (Collection) updatedIDsBuffer ), + EOObjectStore.UpdatedKey ); + storeInfo.setObjectForKey( + new NSArray( (Collection) deletedIDsBuffer ), + EOObjectStore.DeletedKey ); + storeInfo.setObjectForKey( + new NSArray( (Collection) invalidatedIDsBuffer ), + EOObjectStore.InvalidatedKey ); + + // clear buffers + + insertedIDsBuffer.removeAllObjects(); + updatedIDsBuffer.removeAllObjects(); + deletedIDsBuffer.removeAllObjects(); + invalidatedIDsBuffer.removeAllObjects(); + + // post notification + NSNotificationQueue.defaultQueue(). + enqueueNotificationWithCoalesceMaskForModes( new NSNotification( + ObjectsChangedInStoreNotification, this, storeInfo ), + NSNotificationQueue.PostNow, + NSNotificationQueue.NotificationNoCoalescing, null ); + } + + /** + * Removes all values from the specified object, + * converting it into a fault for the specified id. + * New or deleted objects should not be refaulted. + */ + public void refaultObject( Object anObject, EOGlobalID aGlobalID, + EOEditingContext aContext ) + { +//System.out.println( "refaultObject: " + aGlobalID ); +//new net.wotonomy.ui.swing.util.StackTraceInspector(); + if ( anObject instanceof EOFaulting ) + { + ((EOFaulting)anObject).turnIntoFault( null ); + } + } + + /** + * Writes all changes in the specified editing context + * to the respository. + */ + public void saveChangesInEditingContext ( EOEditingContext aContext ) + { + Object result; // need a container result? + Map updateMap; + Object object; + EOGlobalID id; + Iterator iterator; + + //TODO: the ordering of operations here + // needs to be a lot more sophisticated. + + // process deletes first + iterator = aContext.deletedObjects().iterator(); + while ( iterator.hasNext() ) + { + object = iterator.next(); + id = aContext.globalIDForObject( object ); + try + { + result = deleteObject( id ); + } + catch ( Exception exc ) + { + System.out.println( "Error deleting object: " + id ); + exc.printStackTrace(); + } + } + + // process inserts next + iterator = aContext.insertedObjects().iterator(); + while ( iterator.hasNext() ) + { + object = iterator.next(); + processInsert( aContext, object ); + } + + // process updates last + iterator = aContext.updatedObjects().iterator(); + while ( iterator.hasNext() ) + { + object = iterator.next(); + id = aContext.globalIDForObject( object ); + try + { + updateMap = getUpdateMap( aContext, object ); + result = updateObject( id, updateMap ); + } + catch ( Exception exc ) + { + System.out.println( "Error updating object: " + id ); + exc.printStackTrace(); + } + } + + //aContext.invalidateAllObjects(); + } + + protected Object processInsert( EOEditingContext aContext, Object object ) + { + Map result = null; + EOGlobalID id = aContext.globalIDForObject( object ); + try + { + Map updateMap; + updateMap = getUpdateMap( aContext, object ); + result = insertObject( id, updateMap ); + id = globalIDForData( result ); // read new permanent id + + // broadcast that the global id has changed. + NSMutableDictionary userInfo = new NSMutableDictionary(); + userInfo.setObjectForKey( id, aContext.globalIDForObject( object ) ); + NSNotificationQueue.defaultQueue().enqueueNotification( + new NSNotification( EOGlobalID.GlobalIDChangedNotification, + null , userInfo ), NSNotificationQueue.PostNow ); + + } + catch ( Exception exc ) + { + System.out.println( "Error inserting object: " + id ); + exc.printStackTrace(); + } + return result; + } + + /** + * This method returns a map containing just the keys that are modified + * for a given object, converting any to-one or to-many relationships + * to id references. + */ + protected Map getUpdateMap( EOEditingContext aContext, Object anObject ) + { + Map result = new HashMap(); + EOEditingContext context = aContext; + + String entity = entityForGlobalIDOrObject( null, anObject ); + EOClassDescription classDesc = + EOClassDescription.classDescriptionForEntityName( entity ); + if ( classDesc == null ) + { + throw new WotonomyException( "Unknown entity type: " + entity ); + } + + NSArray oneKeys = classDesc.toOneRelationshipKeys(); + NSArray manyKeys = classDesc.toManyRelationshipKeys(); + + String key; + Object value; + EOGlobalID id; + + Collection changedKeys = changedKeysForObject( anObject ); + if ( changedKeys == null ) + { + // assume all keys changed + changedKeys = classDesc.attributeKeys(); + changedKeys.addAll( oneKeys ); + changedKeys.addAll( manyKeys ); + } + Iterator iterator = changedKeys.iterator(); + while ( iterator.hasNext() ) + { + key = iterator.next().toString(); + if ( anObject instanceof EOKeyValueCoding ) + { + value = ((EOKeyValueCoding)anObject).storedValueForKey( key ); + } + else + { + value = EOKeyValueCodingSupport.storedValueForKey( anObject, key ); + } + + // convert to-one relationship to oid + if ( oneKeys.contains( key ) ) + { + id = context.globalIDForObject( value ); + + // if this id hasn't been persisted, save it first + // NOTE: this won't work for self-referential graphs of objects! + if ( id.isTemporary() ) + { + processInsert( aContext, value ); + id = context.globalIDForObject( value ); + } + + value = id; + } + else + // convert to-many relationship list to oid list + if ( manyKeys.contains( key ) ) + { + //NOTE: we can assume that array faults that + // are marked as changed have been fired. + if ( value instanceof Collection ) + { + Object object; + Collection newValue = new LinkedList(); + Iterator jiterator = ((Collection)value).iterator(); + while ( jiterator.hasNext() ) + { + object = jiterator.next(); + id = context.globalIDForObject( object ); + + // if this id hasn't been persisted, save it first + // NOTE: this won't work for self-referential graphs of objects! + if ( id.isTemporary() ) + { + processInsert( aContext, object ); + id = context.globalIDForObject( object ); + } + newValue.add( id ); + } + value = newValue; + } + else + { + // should never happen + new RuntimeException( + "Can't update to-many relationship because it's not a Collection." ) + .printStackTrace(); + } + } + + // place value in map + result.put( key, value ); + } + +System.out.println( result ); + return result; + } + +/* + * $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.5 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.4 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.3 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.2 2002/01/19 17:27:49 mpowers + * Implemented most of it. + * + * Revision 1.1 2001/11/25 22:44:02 mpowers + * Contributing draft of AbstractObjectStore. + * + * + */ +} + -- cgit v1.2.3