View Javadoc

1   /*
2   Wotonomy: OpenStep design patterns for pure Java applications.
3   Copyright (C) 2001 Intersect Software Corporation
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.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             //TODO: get rid of this try/catch - exceptions should be fatal (?) 
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             //TODO: get rid of this try/catch - exceptions should be fatal (?) 
190             try
191             {
192                 key = it.next().toString();
193                 value = aMap.get( key );
194                 if ( value instanceof EONullValue ) 
195                 // can't use == nullValue() because of cloning/serialization
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 //System.out.println();
264 //System.out.println( "clone: " + aSourceContext );
265 //System.out.println( "     : " + aSource );
266 //System.out.println( "     : " + aDestinationContext );
267 //System.out.println();
268 
269         // the only known way to deep copy in
270         // java without native code is serialization
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 //long t = System.currentTimeMillis();            
294             ByteArrayOutputStream byteOutput =
295                 new ByteArrayOutputStream();// CloneBufferSize );
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 // profiling
317 /*
318 byte[] result = byteOutput.toByteArray();
319 long size = result.length;
320 long time = ( System.currentTimeMillis() - t );
321 maxSize = Math.max( size, maxSize );
322 minSize = Math.min( size, minSize );
323 totSize += size;
324 maxTime = Math.max( time, maxTime );
325 minTime = Math.min( time, minTime );
326 totTime += time;
327 nTime++;
328 System.out.println( "freeze: size = [ " + size + " : " + minSize + " : " + ( (float)totSize / (float)nTime ) + " : " + maxSize 
329 + " ]  time = [ " + time + " : " + minTime + " : " + ( (float)totTime / (float)nTime ) + " : " + maxTime + " ]" );
330 return result;            
331 */
332 // end profiling
333 
334         } 
335         catch ( Exception exc )
336         {
337             throw new WotonomyException( exc );
338         }
339     }
340     
341 //static long maxTime, minTime, totTime, nTime, maxSize, minSize, totSize;    
342 //static long maxTimeThaw, minTimeThaw, totTimeThaw, nTimeThaw;
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 //long t = System.currentTimeMillis();            
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 // profiling
390 /*
391 Object result = objectInput.readObject();
392 long timeThaw = ( System.currentTimeMillis() - t );
393 maxTimeThaw = Math.max( timeThaw, maxTimeThaw );
394 minTimeThaw = Math.min( timeThaw, minTimeThaw );
395 totTimeThaw += timeThaw;
396 nTimeThaw++;
397 System.out.println( "thaw: size = " + aByteArray.length + ", time = [ " + timeThaw + " : " + minTimeThaw + " : " + ( (float)totTimeThaw / (float)nTimeThaw ) + " : " + maxTimeThaw + " ]" );
398 return result;
399 */
400 // end profiling
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         // get all keys for this object
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         // transpose all objects registered in source context
448         NSDictionary values = storedValuesForKeys( aSource, keys );
449         values = (NSDictionary)
450             clone( aSourceContext, values, aDestinationContext, null );
451             
452         // apply to destination object
453         takeStoredValuesFromDictionary( aDestination, values );        
454         return aDestination;
455     }
456     
457     // inner classes
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 //            if ( anObject == editingContext ) return marker;
495 //FIXME: this should be more strict as above
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     //System.out.println( "KeyValueCodingUtilities.replaceObject: returning: " + result );
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         // just a marker class - no implementation necessary
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  * $Log$
674  * Revision 1.3  2006/02/18 22:46:44  cgruber
675  * Add Surrogate map from .util into control's internal package, and fix imports.
676  *
677  * Revision 1.2  2006/02/16 16:47:14  cgruber
678  * Move some classes in to "internal" packages and re-work imports, etc.
679  *
680  * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions.
681  *
682  * Revision 1.1  2006/02/16 13:19:57  cgruber
683  * Check in all sources in eclipse-friendly maven-enabled packages.
684  *
685  * Revision 1.15  2003/01/21 22:30:10  mpowers
686  * thaw() now allows you to pass in a class loader.
687  *
688  * Revision 1.14  2002/05/15 13:46:35  mpowers
689  * Exposed freeze and thaw as public.
690  *
691  * Revision 1.13  2001/08/22 19:25:13  mpowers
692  * Added (and commented out) profiling code for freeze.
693  *
694  * Revision 1.12  2001/05/06 18:27:10  mpowers
695  * More broadly catching editing contexts for now.
696  *
697  * Revision 1.11  2001/05/05 13:18:49  mpowers
698  * Fixed: transposing output stream was not returning the object to replace.
699  *
700  * Revision 1.10  2001/05/04 16:57:56  mpowers
701  * Now correctly transposing references to editing contexts when
702  * cloning/copying between editing contexts.
703  *
704  * Revision 1.9  2001/05/04 14:42:58  mpowers
705  * Now getting stored values in KeyValueCoding.
706  * MasterDetail now marks dirty based on whether it's an attribute
707  * or relation.
708  * Implemented editing context marker.
709  *
710  * Revision 1.8  2001/05/02 15:47:40  mpowers
711  * Fixed the pernicious problem with reverts: recordObject was recording
712  * a snapshot of the clone before the transposition-copy happened,
713  * so the revert object would lose all of its transposed relationships.
714  *
715  * Revision 1.7  2001/04/30 12:33:17  mpowers
716  * Fixed problem with use of EONullValue.nullValue(), which can't be used
717  * when we're serializably duplicating objects.
718  *
719  * Revision 1.6  2001/04/30 02:14:25  mpowers
720  * Copying should call takeStoredValueForKeys.
721  *
722  * Revision 1.5  2001/04/29 22:02:45  mpowers
723  * Work on id transposing between editing contexts.
724  *
725  * Revision 1.4  2001/04/29 02:29:31  mpowers
726  * Debugging relationship faulting.
727  *
728  * Revision 1.3  2001/04/28 16:18:44  mpowers
729  * Implementing relationships.
730  *
731  * Revision 1.2  2001/04/28 14:12:23  mpowers
732  * Refactored cloning/copying into KeyValueCodingUtilities.
733  *
734  * Revision 1.1  2001/04/27 23:41:12  mpowers
735  * Contributing file for KeyValueCodingUtilities.
736  *
737  *
738  */
739     
740