View Javadoc

1   /*
2   Wotonomy: OpenStep design patterns for pure Java applications.
3   Copyright (C) 2001 Michael Powers
4   
5   This library is free software; you can redistribute it and/or
6   modify it under the terms of the GNU Lesser General Public
7   License as published by the Free Software Foundation; either
8   version 2.1 of the License, or (at your option) any later version.
9   
10  This library is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  Lesser General Public License for more details.
14  
15  You should have received a copy of the GNU Lesser General Public
16  License along with this library; if not, see http://www.gnu.org
17  */
18  
19  package net.wotonomy.control;
20  
21  import java.util.Collection;
22  import java.util.HashMap;
23  import java.util.Iterator;
24  import java.util.LinkedList;
25  import java.util.List;
26  import java.util.Map;
27  
28  import net.wotonomy.foundation.NSArray;
29  import net.wotonomy.foundation.NSMutableArray;
30  import net.wotonomy.foundation.NSMutableDictionary;
31  import net.wotonomy.foundation.NSNotification;
32  import net.wotonomy.foundation.NSNotificationCenter;
33  import net.wotonomy.foundation.NSNotificationQueue;
34  import net.wotonomy.foundation.NSSelector;
35  import net.wotonomy.foundation.internal.WotonomyException;
36  
37  /***
38  * An abstract implementation of object store that 
39  * implements common functionality.  Subclasses must
40  * implement data object creation, initialization, and
41  * refault logic, as well as logic to commit an editing 
42  * context.
43  */
44  public abstract class AbstractObjectStore extends EOObjectStore
45  {
46      private NSMutableArray insertedIDsBuffer;
47      private NSMutableArray updatedIDsBuffer;
48      private NSMutableArray deletedIDsBuffer;
49      private NSMutableArray invalidatedIDsBuffer;
50  
51      private Map snapshots;
52      private List exceptionList;
53      
54      /***
55      * Constructs a new instance of this object store.
56      */
57      public AbstractObjectStore()
58      {
59          snapshots = new HashMap();
60          exceptionList = null;
61          
62          insertedIDsBuffer = new NSMutableArray();
63          updatedIDsBuffer = new NSMutableArray();
64          deletedIDsBuffer = new NSMutableArray();
65          invalidatedIDsBuffer = new NSMutableArray();
66  
67          // register for notifications
68          NSSelector handleNotification =
69              new NSSelector( "handleNotification",
70                  new Class[] { NSNotification.class } );
71          NSNotificationCenter.defaultCenter().addObserver(
72              this,
73              handleNotification,
74              EOClassDescription.ClassDescriptionNeededForEntityNameNotification,
75              null );
76      }
77  
78      /***
79      * This implementation returns an appropriately configured array fault.
80      */
81      public NSArray arrayFaultWithSourceGlobalID( EOGlobalID aGlobalID,
82                              String aRelationship, EOEditingContext aContext )
83      { // System.out.println( "arrayFaultWithSourceGlobalID: " + aGlobalID + " : " + aRelationship );
84          return new ArrayFault(
85              aGlobalID, aRelationship, aContext );
86      }
87  
88      /***
89      * This implementation returns the actual object for the specified id.
90      */
91      public /*EOEnterpriseObject*/Object faultForGlobalID( EOGlobalID aGlobalID,
92                                      EOEditingContext aContext )
93      { // System.out.println( "faultForGlobalID: " + aGlobalID );
94          return /*(EOEnterpriseObject)*/createInstanceWithEditingContext( aGlobalID, aContext );
95      }
96  
97      /***
98      * Returns a fault representing an object of the specified entity type with
99      * values from the specified dictionary.  The fault should belong to the
100     * specified editing context.
101     * NOTE: Faults are not supported yet.
102     */
103     public /*EOEnterpriseObject*/Object faultForRawRow( Map aDictionary, String anEntityName,
104                                   EOEditingContext aContext )
105     {
106         //TODO: raw rows are not yet supported
107         throw new WotonomyException( "Faults are not yet supported." );
108     }
109 
110     /***
111     * Given a newly instantiated object, this method initializes its
112     * properties to values appropriate for the specified id.  The object
113     * should belong to the specified editing context. This method is called
114     * to populate faults.
115     */
116     public void initializeObject( Object anObject, EOGlobalID aGlobalID,
117                                   EOEditingContext aContext )
118     { //System.out.println( "initializeObject: " + aGlobalID );
119         try
120         {
121             String entity = entityForGlobalIDOrObject( aGlobalID, null );
122             EOClassDescription classDesc = 
123                 EOClassDescription.classDescriptionForEntityName( entity );
124             if ( classDesc == null )
125             {
126                 throw new WotonomyException( "Unknown entity type: " + entity );   
127             }
128         
129             Collection attributes = classDesc.attributeKeys();
130             Map data = readFromCache( aGlobalID, attributes );
131             String key;
132             Iterator iterator = attributes.iterator();
133             while ( iterator.hasNext() )
134             {
135                 key = iterator.next().toString();
136                 
137                 // write the snapshot's reference into the object
138                 if ( anObject instanceof EOKeyValueCoding )
139                 {
140                     ((EOKeyValueCoding)anObject).takeStoredValueForKey( data.get( key ), key );
141                 }
142                 else
143                 {
144                     EOKeyValueCodingSupport.takeStoredValueForKey( anObject, data.get( key ), key );
145                 }
146                 
147                 //NOTE: our objects are expected to make a copy
148                 // of their data before it is modified, so it's okay
149                 // to return them our copy of the data: 
150                 // we trust that they will not modify it.
151             }
152         }
153         catch ( Exception exc )
154         {	
155             exc.printStackTrace();
156         }
157     }
158 
159     /***
160     * Reads the local data snapshot for the specified id.
161     * If no snapshot exists, a new snapshot is created.
162     * If the specified keys are not in the snapshot,
163     * new data is fetched into the snapshot.
164     * If null is specified, all known keys are returned.
165     * Will not return null.  
166     * Result will have values for those keys and only
167     * those keys requested.  Missing keys indicate an
168     * error occurred.
169     */
170     protected Map readFromCache( EOGlobalID aGlobalID, Collection keys )
171     {
172         Map snapshot = (Map) snapshots.get( aGlobalID );
173 
174         // if no snapshot for this id, create an empty one
175         if ( snapshot == null )
176         {
177             snapshot = new HashMap();
178             snapshots.put( aGlobalID, snapshot );
179         }
180 
181         // if we don't have all the necessary keys
182         if ( ( keys == null ) || ( ! snapshot.keySet().containsAll( keys ) ) )
183         {
184             // we need to make a server call
185             try
186             {
187                 Map data = readObject( aGlobalID, keys );
188                 
189                 // compare timestamps 
190                 Comparable localTimestamp = (Comparable) timestampForData( snapshot );
191                 // if our local snapshot has an timestamp (new snapshots don't have timestamp)
192                 if ( localTimestamp != null )
193                 {
194                     Comparable incomingTimestamp = (Comparable) timestampForData( data );
195                     if ( incomingTimestamp == null )
196                     {
197                         // not allowed to happen
198                         new RuntimeException( "Server returned data without an timestamp" ).printStackTrace();
199                         // however, we can just assume it's a newer timestamp and continue
200                     }
201                     
202                     // if timestamps don't match
203                     if ( ( incomingTimestamp == null ) || ( ! incomingTimestamp.equals( localTimestamp ) ) )
204                     {
205                         // dump our existing snapshot's data
206                         snapshot.clear();
207                         // queue for a notification on this oid as updated
208                         //TODO: implement this
209                     }
210                 }
211                     
212                 // copy new data into our local snapshot
213                 snapshot.putAll( data );
214             }
215             catch ( Exception exc )
216             {	
217                 exc.printStackTrace();
218             }
219         }
220         
221         // return just the requested keys from our updated snapshot
222         Map result = new HashMap();
223         if ( keys == null )
224         {
225             result.putAll( snapshot );
226         }
227         else
228         {
229             Object key;
230             Iterator iterator = keys.iterator();
231             while ( iterator.hasNext() )
232             {
233                 key = iterator.next();
234                 result.put( key, snapshot.get( key ) );
235             }
236         }
237         return snapshot;
238     }
239     
240     /***
241     * Returns a comparable object (typically a Date or Long) for
242     * the given data map or snapshot.  This is used to determine
243     * whether a local snapshot should be dumped in favor of fetched
244     * data from the server.
245     * Returns null if no timestamp can be determined, in which
246     * case the fetched data will assumed to be more recent than
247     * any local snapshot.
248     */
249     abstract protected Comparable timestampForData( Map aDataMap );
250     
251     /***
252     * Extracts the global id for the fetched data or snapshot.
253     * Some entities have multi-attribute keys that would be 
254     * assembled into a single instance of EOGlobalID.
255     */
256     abstract protected EOGlobalID globalIDForData( Map aDataMap );
257     
258     /***
259     * Returns the entity that corresponds to the specified global id
260     * and/or object.  Either may be null, but both will not be null.
261     * //FIXME: This is less than elegant.
262     */
263     abstract protected String entityForGlobalIDOrObject( 
264         EOGlobalID aGlobalID, Object anObject );
265     
266     /***
267     * Returns the keys that have changed on the specified object.
268     * If null, all keys are presumed changed, including relationships.
269     */
270     abstract protected Collection changedKeysForObject( Object anObject );
271     
272     /***
273     * Returns the data for the row corresponding to the specified id
274     * containing at least the specified keys.  Implementations are allowed
275     * to return more data than requested, and callers are advised to take
276     * advantage of the returned data.
277     */
278     abstract protected Map readObject( EOGlobalID aGlobalID, Collection keys );
279     
280     /***
281     * Returns the data for the row corresponding to the specified id.
282     * //TODO: Need a better return value?  How to return invalidated list?
283     */
284     abstract protected Map insertObject( EOGlobalID aGlobalID, Map aDataMap );
285     
286     /***
287     * Returns the data for the row corresponding to the specified id.
288     * //TODO: Need a better return value?  How to return invalidated list?
289     */
290     abstract protected Object updateObject( EOGlobalID aGlobalID, Map aDataMap );
291     
292     /***
293     * Returns the data for the row corresponding to the specified id.
294     * //TODO: Need a better return value?  How to return invalidated list?
295     */
296     abstract protected Object deleteObject( EOGlobalID aGlobalID );
297     
298     /***
299     * Creates a new instance of an object that corresponds to the
300     * specified global id and is registered in the specified context.
301     * This implementation extracts the entity type from getEntityForGlobaID
302     * and construct a new instance from the class description that 
303     * corresponds to the entity type.  Override to change this behavior.
304     */
305     protected Object createInstanceWithEditingContext( 
306         EOGlobalID aGlobalID, EOEditingContext aContext )
307     {
308         String entity = entityForGlobalIDOrObject( aGlobalID, null );
309         EOClassDescription classDesc = 
310             EOClassDescription.classDescriptionForEntityName( entity );
311         if ( classDesc == null )
312         {
313              throw new WotonomyException( "Unknown entity type: " + entity );   
314         }
315         
316         Object result = classDesc.createInstanceWithEditingContext( aContext, aGlobalID );
317         if ( result instanceof EOFaulting )
318         {
319             ((EOFaulting)result).turnIntoFault( null );
320         }
321         return result;
322     }
323     
324     /***
325     * Dumps the snapshot corresponding to the specified id.
326     */
327     protected void invalidateObject( EOGlobalID aGlobalID )
328     {
329         snapshots.remove( aGlobalID );
330     }
331      
332     /***
333     * Dumps all snapshots.
334     */
335     protected void invalidateAllCache()
336     {
337         snapshots.clear();
338     }
339      
340     /***
341     * Remove all values from all objects in memory, turning them into faults,
342     * and posts a notification that all objects have been invalidated.
343     */
344     public void invalidateAllObjects()
345     {
346         invalidateAllCache();
347 
348         // post notification
349         NSNotificationQueue.defaultQueue().enqueueNotification(
350             new NSNotification(
351                 InvalidatedAllObjectsInStoreNotification, this ),
352             NSNotificationQueue.PostNow );
353     }
354 
355     /***
356     * Removes values with the specified ids from memory, turning them into
357     * faults, and posts a notification that those objects have been invalidated.
358     */
359     public void invalidateObjectsWithGlobalIDs( List aList )
360     {
361         NSArray empty = new NSArray();
362         NSMutableArray invalidated = new NSMutableArray();
363 
364         Object object;
365         Iterator iterator = aList.iterator();
366         while ( iterator.hasNext() )
367         {
368             object = iterator.next();
369             invalidateObject( (EOGlobalID) object );
370             invalidated.addObject( object );
371         }
372 
373         NSMutableDictionary info = new NSMutableDictionary();
374         info.setObjectForKey( empty, InsertedKey );
375         info.setObjectForKey( empty, UpdatedKey );
376         info.setObjectForKey( empty, DeletedKey );
377         info.setObjectForKey( invalidated, InvalidatedKey );
378 
379         // post notification
380         NSNotificationQueue.defaultQueue().
381             enqueueNotificationWithCoalesceMaskForModes( new NSNotification(
382                 ObjectsChangedInStoreNotification, this, info ),
383             NSNotificationQueue.PostNow,
384             NSNotificationQueue.NotificationNoCoalescing, null );
385     }
386 
387     /***
388     * Returns false because locking is not currently permitted.
389     */
390     public boolean isObjectLockedWithGlobalID( EOGlobalID aGlobalID,
391                                                EOEditingContext aContext )
392     {
393         return false;
394     }
395 
396     /***
397     * Does nothing because locking is not currently permitted.
398     */
399     public void lockObjectWithGlobalID( EOGlobalID aGlobalID,
400                                         EOEditingContext aContext )
401     {
402         // does nothing
403     }
404 
405     /***
406     * Returns a List of objects associated with the object
407     * with the specified id for the specified property
408     * relationship.  This method may not return an array fault
409     * because array faults call this method to fetch on demand.
410     * All objects must be registered the specified editing context.
411     * The specified relationship key must produce a result of
412     * type Collection for the source object or an exception is thrown.
413     */
414     public NSArray objectsForSourceGlobalID( EOGlobalID aGlobalID,
415                             String aRelationship, EOEditingContext aContext )
416     { // System.out.println( "objectsForSourceGlobalID: " + aGlobalID + " : " + aRelationship + " : " ); 
417 
418         Map snapshot = readFromCache( aGlobalID, new NSArray( aRelationship ) );
419         Object value = snapshot.get( aRelationship );
420         if ( value == null ) value = new NSArray(); // empty list
421         if ( ! ( value instanceof Collection ) )
422         {
423             throw new RuntimeException( "Specified relationship is not a collection: " 
424                 + aRelationship + " : " + aGlobalID + " : " + value );
425         }
426         
427         NSArray result = new NSMutableArray();
428 
429         // get fault for each id
430         EOGlobalID id;
431         Object fault;
432         Iterator iterator = ((Collection)value).iterator();
433         while ( iterator.hasNext() )
434         {
435             id = (EOGlobalID) iterator.next();
436 
437             // get registered fault
438             fault = aContext.faultForGlobalID( id, aContext );
439 
440             // assert fault
441             if ( fault == null )
442             {
443                 // this should never happen
444                 throw new RuntimeException(
445                     "Could not find fault for ID: " + id );
446             }
447 
448             result.add( fault );
449         }
450 
451         fireObjectsChangedInStore();
452 
453 //System.out.println( "done" );
454         return result;
455     }
456     
457     /***
458     * Returns a List of objects the meet the criteria of
459     * the supplied specification.  Faults are not allowed in the array.
460     * Each object is registered with the specified editing context.
461     * If any object is already fetched in the specified context,
462     * it is not refetched and that object should be used in the array.
463     */
464     public NSArray objectsWithFetchSpecification(
465                 EOFetchSpecification aFetchSpec, EOEditingContext aContext )
466     {
467         NSMutableArray result = new NSMutableArray();
468         
469         //TODO: implement this
470         
471         return result;
472     }
473 
474     /***
475     * Fires ObjectsChangedInStoreNotification
476     * with contents of buffers and then clears buffers.
477     * If buffers are empty, does nothing.
478     */
479     private void fireObjectsChangedInStore()
480     {
481         // check for changes to broadcast
482         if ( insertedIDsBuffer.size() + updatedIDsBuffer.size() +
483              deletedIDsBuffer.size() + invalidatedIDsBuffer.size() == 0 )
484         {
485             return;
486         }
487 
488         // broadcast ObjectsChangedInStoreNotification
489         // for the benefit of child editing contexts
490 
491         NSMutableDictionary storeInfo = new NSMutableDictionary();
492 
493         storeInfo.setObjectForKey(
494             new NSArray( (Collection) insertedIDsBuffer ),
495             EOObjectStore.InsertedKey );
496         storeInfo.setObjectForKey(
497             new NSArray( (Collection) updatedIDsBuffer ),
498             EOObjectStore.UpdatedKey );
499         storeInfo.setObjectForKey(
500             new NSArray( (Collection) deletedIDsBuffer ),
501             EOObjectStore.DeletedKey );
502         storeInfo.setObjectForKey(
503             new NSArray( (Collection) invalidatedIDsBuffer ),
504             EOObjectStore.InvalidatedKey );
505 
506         // clear buffers
507 
508         insertedIDsBuffer.removeAllObjects();
509         updatedIDsBuffer.removeAllObjects();
510         deletedIDsBuffer.removeAllObjects();
511         invalidatedIDsBuffer.removeAllObjects();
512 
513         // post notification
514         NSNotificationQueue.defaultQueue().
515             enqueueNotificationWithCoalesceMaskForModes( new NSNotification(
516                 ObjectsChangedInStoreNotification, this, storeInfo ),
517             NSNotificationQueue.PostNow,
518             NSNotificationQueue.NotificationNoCoalescing, null );
519     }
520 
521     /***
522     * Removes all values from the specified object,
523     * converting it into a fault for the specified id.
524     * New or deleted objects should not be refaulted.
525     */
526     public void refaultObject( Object anObject, EOGlobalID aGlobalID,
527                                EOEditingContext aContext )
528     {
529 //System.out.println( "refaultObject: " + aGlobalID ); 
530 //new net.wotonomy.ui.swing.util.StackTraceInspector();
531         if ( anObject instanceof EOFaulting )
532         {
533             ((EOFaulting)anObject).turnIntoFault( null );
534         }
535     }
536 
537     /***
538     * Writes all changes in the specified editing context
539     * to the respository.
540     */
541     public void saveChangesInEditingContext ( EOEditingContext aContext )
542     {
543         Object result; // need a container result?
544         Map updateMap;
545         Object object;
546         EOGlobalID id;
547         Iterator iterator;
548         
549         //TODO: the ordering of operations here 
550         // needs to be a lot more sophisticated.
551     
552         // process deletes first
553         iterator = aContext.deletedObjects().iterator();
554         while ( iterator.hasNext() )
555         {
556             object = iterator.next();
557             id = aContext.globalIDForObject( object );
558             try
559             {
560                 result = deleteObject( id );
561             }
562             catch ( Exception exc )
563             {
564                 System.out.println( "Error deleting object: " + id );
565                 exc.printStackTrace();
566             }
567         }
568         
569         // process inserts next
570         iterator = aContext.insertedObjects().iterator();
571         while ( iterator.hasNext() )
572         {
573             object = iterator.next();
574             processInsert( aContext, object );
575         }
576         
577         // process updates last
578         iterator = aContext.updatedObjects().iterator();
579         while ( iterator.hasNext() )
580         {
581             object = iterator.next();
582             id = aContext.globalIDForObject( object );
583             try
584             {
585                 updateMap = getUpdateMap( aContext, object );
586                 result = updateObject( id, updateMap );
587             }
588             catch ( Exception exc )
589             {
590                 System.out.println( "Error updating object: " + id );
591                 exc.printStackTrace();
592             }
593         }
594         
595         //aContext.invalidateAllObjects();
596     }
597     
598     protected Object processInsert( EOEditingContext aContext, Object object )
599     {
600         Map result = null; 
601         EOGlobalID id = aContext.globalIDForObject( object );
602         try
603         {
604             Map updateMap;
605             updateMap = getUpdateMap( aContext, object ); 
606             result = insertObject( id, updateMap );
607             id = globalIDForData( result ); // read new permanent id
608 
609             // broadcast that the global id has changed.
610             NSMutableDictionary userInfo = new NSMutableDictionary();
611             userInfo.setObjectForKey( id, aContext.globalIDForObject( object ) );
612             NSNotificationQueue.defaultQueue().enqueueNotification(
613                 new NSNotification( EOGlobalID.GlobalIDChangedNotification,
614                 null , userInfo ), NSNotificationQueue.PostNow );
615 
616         }
617         catch ( Exception exc )
618         {
619             System.out.println( "Error inserting object: " + id );
620             exc.printStackTrace();
621         }
622         return result;
623     }
624 
625     /***
626     * This method returns a map containing just the keys that are modified
627     * for a given object, converting any to-one or to-many relationships
628     * to id references.
629     */
630     protected Map getUpdateMap( EOEditingContext aContext, Object anObject )
631     {
632         Map result = new HashMap();
633         EOEditingContext context = aContext;
634         
635         String entity = entityForGlobalIDOrObject( null, anObject );
636         EOClassDescription classDesc = 
637             EOClassDescription.classDescriptionForEntityName( entity );
638         if ( classDesc == null )
639         {
640             throw new WotonomyException( "Unknown entity type: " + entity );   
641         }
642         
643         NSArray oneKeys = classDesc.toOneRelationshipKeys();
644         NSArray manyKeys = classDesc.toManyRelationshipKeys();
645         
646         String key;
647         Object value;
648         EOGlobalID id;
649 
650         Collection changedKeys = changedKeysForObject( anObject );
651         if ( changedKeys == null ) 
652         {
653             // assume all keys changed
654             changedKeys = classDesc.attributeKeys();
655             changedKeys.addAll( oneKeys );
656             changedKeys.addAll( manyKeys );
657         }
658         Iterator iterator = changedKeys.iterator();
659         while ( iterator.hasNext() )
660         {
661             key = iterator.next().toString();
662             if ( anObject instanceof EOKeyValueCoding )
663             {
664                 value = ((EOKeyValueCoding)anObject).storedValueForKey( key );
665             }
666             else
667             {
668                 value = EOKeyValueCodingSupport.storedValueForKey( anObject, key );
669             }
670             
671             // convert to-one relationship to oid
672             if ( oneKeys.contains( key ) )
673             {
674                 id = context.globalIDForObject( value );
675                 
676                 // if this id hasn't been persisted, save it first
677                 // NOTE: this won't work for self-referential graphs of objects!
678                 if ( id.isTemporary() )
679                 {
680                     processInsert( aContext, value );   
681                     id = context.globalIDForObject( value );
682                 }
683                 
684                 value = id;
685             }
686             else
687             // convert to-many relationship list to oid list
688             if ( manyKeys.contains( key ) )
689             {
690                 //NOTE: we can assume that array faults that 
691                 // are marked as changed have been fired.
692                 if ( value instanceof Collection )
693                 {
694                     Object object;
695                     Collection newValue = new LinkedList();
696                     Iterator jiterator = ((Collection)value).iterator();
697                     while ( jiterator.hasNext() )
698                     {
699                         object = jiterator.next();
700                         id = context.globalIDForObject( object );
701 
702                         // if this id hasn't been persisted, save it first
703                         // NOTE: this won't work for self-referential graphs of objects!
704                         if ( id.isTemporary() )
705                         {
706                             processInsert( aContext, object );   
707                             id = context.globalIDForObject( object );
708                         }
709                         newValue.add( id );
710                     }
711                     value = newValue;
712                 }
713                 else
714                 {
715                     // should never happen
716                     new RuntimeException( 
717                         "Can't update to-many relationship because it's not a Collection." )
718                         .printStackTrace();
719                 }
720             }
721             
722             // place value in map 
723             result.put( key, value );
724         }
725         
726 System.out.println( result );
727         return result;
728     }
729 
730 /*
731  * $Log$
732  * Revision 1.2  2006/02/16 16:47:14  cgruber
733  * Move some classes in to "internal" packages and re-work imports, etc.
734  *
735  * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions.
736  *
737  * Revision 1.1  2006/02/16 13:19:57  cgruber
738  * Check in all sources in eclipse-friendly maven-enabled packages.
739  *
740  * Revision 1.5  2003/12/18 15:37:38  mpowers
741  * Changes to retain ability to work with objects that don't necessarily
742  * implement EOEnterpriseObject.  I would still like to preserve this case
743  * for general usage, however the access package is free to assume that
744  * those objects will be EOs and cast appropriately.
745  *
746  * Revision 1.4  2003/08/19 01:53:12  chochos
747  * 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.
748  *
749  * Revision 1.3  2002/10/24 18:18:12  mpowers
750  * NSArray's are now considered read-only, so we can return our internal
751  * representation to reduce unnecessary object allocation.
752  *
753  * Revision 1.2  2002/01/19 17:27:49  mpowers
754  * Implemented most of it.
755  *
756  * Revision 1.1  2001/11/25 22:44:02  mpowers
757  * Contributing draft of AbstractObjectStore.
758  *
759  * 
760  */
761 }
762