/* 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. * * */ }