1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
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 {
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
92 EOEditingContext aContext )
93 {
94 return
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
104 EOEditingContext aContext )
105 {
106
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 {
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
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
148
149
150
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
175 if ( snapshot == null )
176 {
177 snapshot = new HashMap();
178 snapshots.put( aGlobalID, snapshot );
179 }
180
181
182 if ( ( keys == null ) || ( ! snapshot.keySet().containsAll( keys ) ) )
183 {
184
185 try
186 {
187 Map data = readObject( aGlobalID, keys );
188
189
190 Comparable localTimestamp = (Comparable) timestampForData( snapshot );
191
192 if ( localTimestamp != null )
193 {
194 Comparable incomingTimestamp = (Comparable) timestampForData( data );
195 if ( incomingTimestamp == null )
196 {
197
198 new RuntimeException( "Server returned data without an timestamp" ).printStackTrace();
199
200 }
201
202
203 if ( ( incomingTimestamp == null ) || ( ! incomingTimestamp.equals( localTimestamp ) ) )
204 {
205
206 snapshot.clear();
207
208
209 }
210 }
211
212
213 snapshot.putAll( data );
214 }
215 catch ( Exception exc )
216 {
217 exc.printStackTrace();
218 }
219 }
220
221
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
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
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
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 {
417
418 Map snapshot = readFromCache( aGlobalID, new NSArray( aRelationship ) );
419 Object value = snapshot.get( aRelationship );
420 if ( value == null ) value = new NSArray();
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
430 EOGlobalID id;
431 Object fault;
432 Iterator iterator = ((Collection)value).iterator();
433 while ( iterator.hasNext() )
434 {
435 id = (EOGlobalID) iterator.next();
436
437
438 fault = aContext.faultForGlobalID( id, aContext );
439
440
441 if ( fault == null )
442 {
443
444 throw new RuntimeException(
445 "Could not find fault for ID: " + id );
446 }
447
448 result.add( fault );
449 }
450
451 fireObjectsChangedInStore();
452
453
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
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
482 if ( insertedIDsBuffer.size() + updatedIDsBuffer.size() +
483 deletedIDsBuffer.size() + invalidatedIDsBuffer.size() == 0 )
484 {
485 return;
486 }
487
488
489
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
507
508 insertedIDsBuffer.removeAllObjects();
509 updatedIDsBuffer.removeAllObjects();
510 deletedIDsBuffer.removeAllObjects();
511 invalidatedIDsBuffer.removeAllObjects();
512
513
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
530
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;
544 Map updateMap;
545 Object object;
546 EOGlobalID id;
547 Iterator iterator;
548
549
550
551
552
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
570 iterator = aContext.insertedObjects().iterator();
571 while ( iterator.hasNext() )
572 {
573 object = iterator.next();
574 processInsert( aContext, object );
575 }
576
577
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
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 );
608
609
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
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
672 if ( oneKeys.contains( key ) )
673 {
674 id = context.globalIDForObject( value );
675
676
677
678 if ( id.isTemporary() )
679 {
680 processInsert( aContext, value );
681 id = context.globalIDForObject( value );
682 }
683
684 value = id;
685 }
686 else
687
688 if ( manyKeys.contains( key ) )
689 {
690
691
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
703
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
716 new RuntimeException(
717 "Can't update to-many relationship because it's not a Collection." )
718 .printStackTrace();
719 }
720 }
721
722
723 result.put( key, value );
724 }
725
726 System.out.println( result );
727 return result;
728 }
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761 }
762