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