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.io.ByteArrayInputStream;
22 import java.io.ByteArrayOutputStream;
23 import java.io.IOException;
24 import java.io.InputStream;
25 import java.io.ObjectInputStream;
26 import java.io.ObjectOutputStream;
27 import java.io.ObjectStreamClass;
28 import java.io.OutputStream;
29 import java.io.Serializable;
30 import java.util.Iterator;
31 import java.util.LinkedList;
32 import java.util.List;
33 import java.util.Map;
34
35 import net.wotonomy.foundation.NSDictionary;
36 import net.wotonomy.foundation.NSMutableDictionary;
37 import net.wotonomy.foundation.internal.Duplicator;
38 import net.wotonomy.foundation.internal.WotonomyException;
39
40 /***
41 * KeyValueCodingUtilities implements what
42 * EOKeyValueCodingSupport leaves out. Importantly,
43 * this class implements the deep clone and deep copy
44 * operations that are essential to the functioning of
45 * nested editing contexts.
46 *
47 * @author michael@mpowers.net
48 * @author $Author: cgruber $
49 * @version $Revision: 900 $
50 */
51 public class KeyValueCodingUtilities
52 {
53 /***
54 * Returns a Map of the specified keys to their values,
55 * each of which is obtained by calling valueForKey
56 * on the specified object if it implements EOKeyValueCoding,
57 * and otherwise falling back on EOKeyValueCodingSupport.
58 * Null values must be represented by NSNull.nullValue().
59 */
60 static public NSDictionary valuesForKeys(
61 Object anObject, List aKeyList )
62 {
63 return valuesForKeys( anObject, aKeyList, false );
64 }
65
66 /***
67 * Returns a Map of the specified keys to their values,
68 * each of which is obtained by calling storedValueForKey
69 * on the specified object if it implements EOKeyValueCoding,
70 * and otherwise falling back on EOKeyValueCodingSupport.
71 * Null values must be represented by NSNull.nullValue().
72 */
73 static public NSDictionary storedValuesForKeys(
74 Object anObject, List aKeyList )
75 {
76 return valuesForKeys( anObject, aKeyList, true );
77 }
78
79 /***
80 * Called by valuesForKeys and storedValuesForKeys.
81 * This uses storedValueForKey if isStored is true,
82 * otherwise uses valueForKey.
83 */
84 static private NSDictionary valuesForKeys(
85 Object anObject, List aKeyList, boolean isStored )
86 {
87 EOKeyValueCoding coding;
88 if ( anObject instanceof EOKeyValueCoding )
89 {
90 coding = (EOKeyValueCoding) anObject;
91 }
92 else
93 {
94 coding = null;
95 }
96
97 String key;
98 Object value;
99 NSMutableDictionary result = new NSMutableDictionary();
100 Iterator it = aKeyList.iterator();
101 while ( it.hasNext() )
102 {
103
104 try
105 {
106 key = it.next().toString();
107 if ( coding != null )
108 {
109 if ( isStored )
110 value = coding.storedValueForKey( key );
111 else
112 value = coding.valueForKey( key );
113 }
114 else
115 {
116 if ( isStored )
117 value = EOKeyValueCodingSupport.storedValueForKey( anObject, key );
118 else
119 value = EOKeyValueCodingSupport.valueForKey( anObject, key );
120 }
121 if ( value == null )
122 {
123 value = EONullValue.nullValue();
124 }
125 result.setObjectForKey( value, key );
126 }
127 catch ( RuntimeException exc )
128 {
129 System.out.println(
130 "KeyValueCodingUtilities.valuesForKeys: "
131 + isStored + " : " + exc );
132 }
133 }
134 return result;
135 }
136
137 /***
138 * Takes the keys from the specified Map as properties
139 * and applies the corresponding values, each of which
140 * might be set by calling takeValueForKey on the
141 * specified object if it implements EOKeyValueCoding,
142 * and otherwise falling back on EOKeyValueCodingSupport.
143 * Null values must be represented by NSNull.nullValue().
144 */
145 static public void takeValuesFromDictionary(
146 Object anObject, Map aMap )
147 {
148 takeStoredValuesFromDictionary( anObject, aMap, false );
149 }
150
151 /***
152 * Takes the keys from the specified Map as properties
153 * and applies the corresponding values, each of which
154 * might be set by calling takeStoredValueForKey on the
155 * specified object if it implements EOKeyValueCoding,
156 * and otherwise falling back on EOKeyValueCodingSupport.
157 * Null values must be represented by NSNull.nullValue().
158 */
159 static public void takeStoredValuesFromDictionary(
160 Object anObject, Map aMap )
161 {
162 takeStoredValuesFromDictionary( anObject, aMap, true );
163 }
164
165 /***
166 * Called by takeValuesFromDictionary and takeStoredValuesFromDictionary.
167 * This uses takeStoredValueForKey if isStored is true,
168 * otherwise uses takeValueForKey.
169 */
170 static private void takeStoredValuesFromDictionary(
171 Object anObject, Map aMap, boolean isStored )
172 {
173 EOKeyValueCoding coding;
174 if ( anObject instanceof EOKeyValueCoding )
175 {
176 coding = (EOKeyValueCoding) anObject;
177 }
178 else
179 {
180 coding = null;
181 }
182
183 String key;
184 Object value;
185 NSMutableDictionary result = new NSMutableDictionary();
186 Iterator it = aMap.keySet().iterator();
187 while ( it.hasNext() )
188 {
189
190 try
191 {
192 key = it.next().toString();
193 value = aMap.get( key );
194 if ( value instanceof EONullValue )
195
196 {
197 value = null;
198 }
199 if ( coding != null )
200 {
201 if ( isStored )
202 coding.takeStoredValueForKey( value, key );
203 else
204 coding.takeValueForKey( value, key );
205 }
206 else
207 {
208 if ( isStored )
209 EOKeyValueCodingSupport.takeStoredValueForKey(
210 anObject, value, key );
211 else
212 EOKeyValueCodingSupport.takeValueForKey(
213 anObject, value, key );
214 }
215 }
216 catch ( WotonomyException exc )
217 {
218 System.out.println(
219 "KeyValueCodingUtilities.takeStoredValuesFromDictionary: "
220 + isStored + " : " + exc );
221 }
222 }
223 }
224
225 /***
226 * Creates a deep clone of the specified object.
227 * (Object.clone() only creates a shallow clone.)
228 * Returns null if operation fails.
229 */
230 static public Object clone( Object aSource )
231 {
232 return Duplicator.deepClone( aSource );
233 }
234
235 /***
236 * Creates a deep clone of the specified object,
237 * registered in the specified source editing context,
238 * transposing it into the specified destination
239 * editing context.
240 * Returns null if operation fails.
241 */
242 static public Object clone(
243 EOEditingContext aSourceContext, Object aSource,
244 EOEditingContext aDestinationContext )
245 {
246 return clone( aSourceContext, aSource, aDestinationContext, aSource );
247 }
248
249 /***
250 * Called by clone and copy.
251 * The specified root object will not be replaced
252 * by an object in the destination editing context:
253 * this should be the same as the source object for
254 * cloning, but should be null for copying.
255 * Returns null if operation fails.
256 */
257 static private Object clone(
258 EOEditingContext aSourceContext, Object aSource,
259 EOEditingContext aDestinationContext,
260 Object aRootObject )
261 {
262
263
264
265
266
267
268
269
270
271
272 return thaw(
273 freeze( aSource, aSourceContext, aRootObject, true ),
274 aDestinationContext, true );
275 }
276
277 /***
278 * Serializes an object to a byte array containing
279 * GlobalIDMarkers in place of references to other objects
280 * registered in the specified context.
281 * The specified root object will be serialized,
282 * even if it is registered in the specified context:
283 * this is typically the root object you're trying to
284 * serialize.
285 * Package access, as this method is used by editing
286 * context for snapshots.
287 */
288 static public byte[] freeze(
289 Object anObject, EOEditingContext aContext, Object aRootObject, boolean transpose )
290 {
291 try
292 {
293
294 ByteArrayOutputStream byteOutput =
295 new ByteArrayOutputStream();
296 ObjectOutputStream objectOutput;
297 if ( transpose )
298 {
299 objectOutput =
300 new TransposingContextObjectOutputStream(
301 byteOutput, aContext, aRootObject );
302 }
303 else
304 {
305 objectOutput =
306 new ContextObjectOutputStream(
307 byteOutput, aContext );
308 }
309
310 objectOutput.writeObject( anObject );
311 objectOutput.flush();
312 objectOutput.close();
313
314 return byteOutput.toByteArray();
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334 }
335 catch ( Exception exc )
336 {
337 throw new WotonomyException( exc );
338 }
339 }
340
341
342
343
344 /***
345 * De-serializes an object from the specified byte
346 * array, replacing GlobalIDMarkers with reference
347 * to objects registered in the specified editing
348 * context.
349 * Package access, as this method is used by editing
350 * context for snapshots.
351 */
352 static public Object thaw(
353 byte[] aByteArray, EOEditingContext aContext, boolean transpose )
354 {
355 return thaw( aByteArray, aContext, null, transpose );
356 }
357
358 /***
359 * De-serializes an object from the specified byte
360 * array, replacing GlobalIDMarkers with reference
361 * to objects registered in the specified editing
362 * context.
363 * Package access, as this method is used by editing
364 * context for snapshots.
365 */
366 static public Object thaw(
367 byte[] aByteArray, EOEditingContext aContext, ClassLoader aLoader, boolean transpose )
368 {
369 try
370 {
371
372 ByteArrayInputStream byteInput =
373 new ByteArrayInputStream( aByteArray );
374 ObjectInputStream objectInput;
375 if ( transpose )
376 {
377 objectInput =
378 new TransposingContextObjectInputStream(
379 byteInput, aContext, aLoader );
380 }
381 else
382 {
383 objectInput =
384 new ContextObjectInputStream(
385 byteInput, aContext, aLoader );
386 }
387
388 return objectInput.readObject();
389
390
391
392
393
394
395
396
397
398
399
400
401 }
402 catch ( Exception exc )
403 {
404 throw new WotonomyException( exc );
405 }
406 }
407
408 /***
409 * Copies values from one object registered in the
410 * specified origin context to the specified destination
411 * object
412 * The values themselves are cloned, so this is a deep copy.
413 * Returns the destination object, or throws exception
414 * if operation fails.
415 */
416 static public Object copy( Object aSource, Object aDestination )
417 {
418 NSDictionary values = (NSDictionary)
419 clone( valuesForKeys( aSource,
420 EOClassDescription.classDescriptionForClass(
421 aSource.getClass() ).attributeKeys() ) );
422
423 takeStoredValuesFromDictionary( aDestination, values );
424 return aDestination;
425 }
426
427 /***
428 * Copies values from one object registered in the
429 * specified origin context to the specified destination
430 * object
431 * The values themselves are cloned, so this is a deep copy.
432 * Returns the destination object, or throws exception
433 * if operation fails.
434 */
435 static public Object copy(
436 EOEditingContext aSourceContext, Object aSource,
437 EOEditingContext aDestinationContext, Object aDestination )
438 {
439
440 EOClassDescription classDesc =
441 EOClassDescription.classDescriptionForClass( aSource.getClass() );
442 List keys = new LinkedList();
443 keys.addAll( classDesc.attributeKeys() );
444 keys.addAll( classDesc.toOneRelationshipKeys() );
445 keys.addAll( classDesc.toManyRelationshipKeys() );
446
447
448 NSDictionary values = storedValuesForKeys( aSource, keys );
449 values = (NSDictionary)
450 clone( aSourceContext, values, aDestinationContext, null );
451
452
453 takeStoredValuesFromDictionary( aDestination, values );
454 return aDestination;
455 }
456
457
458
459 /***
460 * An ObjectOutputStream that serializes objects with references
461 * to an editing context. The specified context will not be
462 * serialized but referenced, so that a ContextObjectInputStream
463 * can replace the reference with another editing context.
464 */
465 static private class ContextObjectOutputStream extends ObjectOutputStream
466 {
467 private EditingContextMarker marker = new EditingContextMarker();
468 protected EOEditingContext editingContext;
469
470 /***
471 * Specifies the output stream to wrap,
472 * and the source context that should be
473 * referenced but not serialized.
474 */
475 public ContextObjectOutputStream(
476 OutputStream anOutputStream,
477 EOEditingContext aContext )
478 throws IOException
479 {
480 super( anOutputStream );
481 editingContext = aContext;
482 try
483 {
484 enableReplaceObject(true);
485 }
486 catch ( Exception exc )
487 {
488 exc.printStackTrace();
489 }
490 }
491
492 protected Object replaceObject(Object anObject) throws IOException
493 {
494
495
496 if ( anObject instanceof EOEditingContext ) return marker;
497 return anObject;
498 }
499
500 }
501
502 /***
503 * A ContextObjectOutputStream that replaces any objects registered
504 * in the source editing context with markers to be used in
505 * ContextObjectInputStream.
506 */
507 static private class TransposingContextObjectOutputStream
508 extends ContextObjectOutputStream
509 {
510 protected Object rootObject;
511
512 /***
513 * Specifies the output stream to wrap,
514 * the source context containing objects that
515 * should be replaced if found,
516 * and the object which should not be re-registered,
517 * which is typically the object being cloned, but
518 * may be null.
519 */
520 public TransposingContextObjectOutputStream(
521 OutputStream anOutputStream,
522 EOEditingContext aContext,
523 Object anObject )
524 throws IOException
525 {
526 super( anOutputStream, aContext );
527 rootObject = anObject;
528 }
529
530 protected Object replaceObject(Object anObject) throws IOException
531 {
532 if ( anObject == rootObject ) return anObject;
533 if ( editingContext != null )
534 {
535 EOGlobalID id = editingContext.globalIDForObject( anObject );
536 if ( id != null )
537 {
538 Object result = new GlobalIDMarker( id );
539
540 return result;
541 }
542 }
543 return super.replaceObject( anObject );
544 }
545
546 }
547
548 /***
549 * A marker class so references to objects registered in editing
550 * contexts get transposed rather than cloned.
551 */
552 static private class GlobalIDMarker implements Serializable
553 {
554 private EOGlobalID id;
555
556 public GlobalIDMarker( EOGlobalID anID )
557 {
558 id = anID;
559 }
560
561 public EOGlobalID getID()
562 {
563 return id;
564 }
565
566 public String toString()
567 {
568 return "[GlobalIDMarker:"+id+"]";
569 }
570 }
571
572 /***
573 * A marker class so references an object's editing context
574 * gets transposed rather than cloned.
575 */
576 static private class EditingContextMarker implements Serializable
577 {
578
579 }
580
581 /***
582 * An ObjectInputStream that replaces any markers from
583 * ContextObjectOutputStream with objects registered
584 * in the destination editing context.
585 */
586 static private class ContextObjectInputStream extends ObjectInputStream
587 {
588 protected EOEditingContext editingContext;
589 protected ClassLoader classLoader;
590
591 /***
592 * Specifies the output stream to wrap,
593 * the source context containing objects that
594 * should be to replace any markers.
595 * The class loader may be null.
596 */
597 public ContextObjectInputStream(
598 InputStream anInputStream,
599 EOEditingContext aContext,
600 ClassLoader aClassLoader )
601 throws IOException
602 {
603 super( anInputStream );
604 editingContext = aContext;
605 classLoader = aClassLoader;
606 if ( classLoader == null )
607 {
608 classLoader =
609 KeyValueCodingUtilities.class.getClassLoader();
610 }
611 try
612 {
613 enableResolveObject(true);
614 }
615 catch ( Exception exc )
616 {
617 exc.printStackTrace();
618 }
619 }
620
621 protected Object resolveObject(Object anObject) throws IOException
622 {
623 if ( anObject instanceof EditingContextMarker )
624 {
625 return editingContext;
626 }
627 return anObject;
628 }
629
630 protected Class resolveClass(ObjectStreamClass v)
631 throws IOException, ClassNotFoundException
632 {
633 return classLoader.loadClass( v.getName() );
634 }
635 }
636
637 /***
638 * A ContextObjectInputStream that replaces any markers from
639 * TransposingContextObjectOutputStream with objects registered
640 * in the destination editing context.
641 */
642 static private class TransposingContextObjectInputStream
643 extends ContextObjectInputStream
644 {
645 /***
646 * Specifies the output stream to wrap,
647 * the source context containing objects that
648 * should be to replace any markers.
649 */
650 public TransposingContextObjectInputStream(
651 InputStream anInputStream,
652 EOEditingContext aContext,
653 ClassLoader aClassLoader )
654 throws IOException
655 {
656 super( anInputStream, aContext, aClassLoader );
657 }
658
659 protected Object resolveObject(Object anObject) throws IOException
660 {
661 if ( anObject instanceof GlobalIDMarker )
662 {
663 return editingContext.faultForGlobalID(
664 ((GlobalIDMarker)anObject).getID(), editingContext );
665 }
666 return super.resolveObject( anObject );
667 }
668 }
669
670 }
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740