/* Wotonomy: OpenStep design patterns for pure Java applications. Copyright (C) 2001 Intersect Software Corporation This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, see http://www.gnu.org */ package net.wotonomy.control; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.ObjectStreamClass; import java.io.OutputStream; import java.io.Serializable; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import net.wotonomy.foundation.NSDictionary; import net.wotonomy.foundation.NSMutableDictionary; import net.wotonomy.foundation.internal.Duplicator; import net.wotonomy.foundation.internal.WotonomyException; /** * KeyValueCodingUtilities implements what * EOKeyValueCodingSupport leaves out. Importantly, * this class implements the deep clone and deep copy * operations that are essential to the functioning of * nested editing contexts. * * @author michael@mpowers.net * @author $Author: cgruber $ * @version $Revision: 900 $ */ public class KeyValueCodingUtilities { /** * Returns a Map of the specified keys to their values, * each of which is obtained by calling valueForKey * on the specified object if it implements EOKeyValueCoding, * and otherwise falling back on EOKeyValueCodingSupport. * Null values must be represented by NSNull.nullValue(). */ static public NSDictionary valuesForKeys( Object anObject, List aKeyList ) { return valuesForKeys( anObject, aKeyList, false ); } /** * Returns a Map of the specified keys to their values, * each of which is obtained by calling storedValueForKey * on the specified object if it implements EOKeyValueCoding, * and otherwise falling back on EOKeyValueCodingSupport. * Null values must be represented by NSNull.nullValue(). */ static public NSDictionary storedValuesForKeys( Object anObject, List aKeyList ) { return valuesForKeys( anObject, aKeyList, true ); } /** * Called by valuesForKeys and storedValuesForKeys. * This uses storedValueForKey if isStored is true, * otherwise uses valueForKey. */ static private NSDictionary valuesForKeys( Object anObject, List aKeyList, boolean isStored ) { EOKeyValueCoding coding; if ( anObject instanceof EOKeyValueCoding ) { coding = (EOKeyValueCoding) anObject; } else { coding = null; } String key; Object value; NSMutableDictionary result = new NSMutableDictionary(); Iterator it = aKeyList.iterator(); while ( it.hasNext() ) { //TODO: get rid of this try/catch - exceptions should be fatal (?) try { key = it.next().toString(); if ( coding != null ) { if ( isStored ) value = coding.storedValueForKey( key ); else value = coding.valueForKey( key ); } else { if ( isStored ) value = EOKeyValueCodingSupport.storedValueForKey( anObject, key ); else value = EOKeyValueCodingSupport.valueForKey( anObject, key ); } if ( value == null ) { value = EONullValue.nullValue(); } result.setObjectForKey( value, key ); } catch ( RuntimeException exc ) { System.out.println( "KeyValueCodingUtilities.valuesForKeys: " + isStored + " : " + exc ); } } return result; } /** * Takes the keys from the specified Map as properties * and applies the corresponding values, each of which * might be set by calling takeValueForKey on the * specified object if it implements EOKeyValueCoding, * and otherwise falling back on EOKeyValueCodingSupport. * Null values must be represented by NSNull.nullValue(). */ static public void takeValuesFromDictionary( Object anObject, Map aMap ) { takeStoredValuesFromDictionary( anObject, aMap, false ); } /** * Takes the keys from the specified Map as properties * and applies the corresponding values, each of which * might be set by calling takeStoredValueForKey on the * specified object if it implements EOKeyValueCoding, * and otherwise falling back on EOKeyValueCodingSupport. * Null values must be represented by NSNull.nullValue(). */ static public void takeStoredValuesFromDictionary( Object anObject, Map aMap ) { takeStoredValuesFromDictionary( anObject, aMap, true ); } /** * Called by takeValuesFromDictionary and takeStoredValuesFromDictionary. * This uses takeStoredValueForKey if isStored is true, * otherwise uses takeValueForKey. */ static private void takeStoredValuesFromDictionary( Object anObject, Map aMap, boolean isStored ) { EOKeyValueCoding coding; if ( anObject instanceof EOKeyValueCoding ) { coding = (EOKeyValueCoding) anObject; } else { coding = null; } String key; Object value; NSMutableDictionary result = new NSMutableDictionary(); Iterator it = aMap.keySet().iterator(); while ( it.hasNext() ) { //TODO: get rid of this try/catch - exceptions should be fatal (?) try { key = it.next().toString(); value = aMap.get( key ); if ( value instanceof EONullValue ) // can't use == nullValue() because of cloning/serialization { value = null; } if ( coding != null ) { if ( isStored ) coding.takeStoredValueForKey( value, key ); else coding.takeValueForKey( value, key ); } else { if ( isStored ) EOKeyValueCodingSupport.takeStoredValueForKey( anObject, value, key ); else EOKeyValueCodingSupport.takeValueForKey( anObject, value, key ); } } catch ( WotonomyException exc ) { System.out.println( "KeyValueCodingUtilities.takeStoredValuesFromDictionary: " + isStored + " : " + exc ); } } } /** * Creates a deep clone of the specified object. * (Object.clone() only creates a shallow clone.) * Returns null if operation fails. */ static public Object clone( Object aSource ) { return Duplicator.deepClone( aSource ); } /** * Creates a deep clone of the specified object, * registered in the specified source editing context, * transposing it into the specified destination * editing context. * Returns null if operation fails. */ static public Object clone( EOEditingContext aSourceContext, Object aSource, EOEditingContext aDestinationContext ) { return clone( aSourceContext, aSource, aDestinationContext, aSource ); } /** * Called by clone and copy. * The specified root object will not be replaced * by an object in the destination editing context: * this should be the same as the source object for * cloning, but should be null for copying. * Returns null if operation fails. */ static private Object clone( EOEditingContext aSourceContext, Object aSource, EOEditingContext aDestinationContext, Object aRootObject ) { //System.out.println(); //System.out.println( "clone: " + aSourceContext ); //System.out.println( " : " + aSource ); //System.out.println( " : " + aDestinationContext ); //System.out.println(); // the only known way to deep copy in // java without native code is serialization return thaw( freeze( aSource, aSourceContext, aRootObject, true ), aDestinationContext, true ); } /** * Serializes an object to a byte array containing * GlobalIDMarkers in place of references to other objects * registered in the specified context. * The specified root object will be serialized, * even if it is registered in the specified context: * this is typically the root object you're trying to * serialize. * Package access, as this method is used by editing * context for snapshots. */ static public byte[] freeze( Object anObject, EOEditingContext aContext, Object aRootObject, boolean transpose ) { try { //long t = System.currentTimeMillis(); ByteArrayOutputStream byteOutput = new ByteArrayOutputStream();// CloneBufferSize ); ObjectOutputStream objectOutput; if ( transpose ) { objectOutput = new TransposingContextObjectOutputStream( byteOutput, aContext, aRootObject ); } else { objectOutput = new ContextObjectOutputStream( byteOutput, aContext ); } objectOutput.writeObject( anObject ); objectOutput.flush(); objectOutput.close(); return byteOutput.toByteArray(); // profiling /* byte[] result = byteOutput.toByteArray(); long size = result.length; long time = ( System.currentTimeMillis() - t ); maxSize = Math.max( size, maxSize ); minSize = Math.min( size, minSize ); totSize += size; maxTime = Math.max( time, maxTime ); minTime = Math.min( time, minTime ); totTime += time; nTime++; System.out.println( "freeze: size = [ " + size + " : " + minSize + " : " + ( (float)totSize / (float)nTime ) + " : " + maxSize + " ] time = [ " + time + " : " + minTime + " : " + ( (float)totTime / (float)nTime ) + " : " + maxTime + " ]" ); return result; */ // end profiling } catch ( Exception exc ) { throw new WotonomyException( exc ); } } //static long maxTime, minTime, totTime, nTime, maxSize, minSize, totSize; //static long maxTimeThaw, minTimeThaw, totTimeThaw, nTimeThaw; /** * De-serializes an object from the specified byte * array, replacing GlobalIDMarkers with reference * to objects registered in the specified editing * context. * Package access, as this method is used by editing * context for snapshots. */ static public Object thaw( byte[] aByteArray, EOEditingContext aContext, boolean transpose ) { return thaw( aByteArray, aContext, null, transpose ); } /** * De-serializes an object from the specified byte * array, replacing GlobalIDMarkers with reference * to objects registered in the specified editing * context. * Package access, as this method is used by editing * context for snapshots. */ static public Object thaw( byte[] aByteArray, EOEditingContext aContext, ClassLoader aLoader, boolean transpose ) { try { //long t = System.currentTimeMillis(); ByteArrayInputStream byteInput = new ByteArrayInputStream( aByteArray ); ObjectInputStream objectInput; if ( transpose ) { objectInput = new TransposingContextObjectInputStream( byteInput, aContext, aLoader ); } else { objectInput = new ContextObjectInputStream( byteInput, aContext, aLoader ); } return objectInput.readObject(); // profiling /* Object result = objectInput.readObject(); long timeThaw = ( System.currentTimeMillis() - t ); maxTimeThaw = Math.max( timeThaw, maxTimeThaw ); minTimeThaw = Math.min( timeThaw, minTimeThaw ); totTimeThaw += timeThaw; nTimeThaw++; System.out.println( "thaw: size = " + aByteArray.length + ", time = [ " + timeThaw + " : " + minTimeThaw + " : " + ( (float)totTimeThaw / (float)nTimeThaw ) + " : " + maxTimeThaw + " ]" ); return result; */ // end profiling } catch ( Exception exc ) { throw new WotonomyException( exc ); } } /** * Copies values from one object registered in the * specified origin context to the specified destination * object * The values themselves are cloned, so this is a deep copy. * Returns the destination object, or throws exception * if operation fails. */ static public Object copy( Object aSource, Object aDestination ) { NSDictionary values = (NSDictionary) clone( valuesForKeys( aSource, EOClassDescription.classDescriptionForClass( aSource.getClass() ).attributeKeys() ) ); takeStoredValuesFromDictionary( aDestination, values ); return aDestination; } /** * Copies values from one object registered in the * specified origin context to the specified destination * object * The values themselves are cloned, so this is a deep copy. * Returns the destination object, or throws exception * if operation fails. */ static public Object copy( EOEditingContext aSourceContext, Object aSource, EOEditingContext aDestinationContext, Object aDestination ) { // get all keys for this object EOClassDescription classDesc = EOClassDescription.classDescriptionForClass( aSource.getClass() ); List keys = new LinkedList(); keys.addAll( classDesc.attributeKeys() ); keys.addAll( classDesc.toOneRelationshipKeys() ); keys.addAll( classDesc.toManyRelationshipKeys() ); // transpose all objects registered in source context NSDictionary values = storedValuesForKeys( aSource, keys ); values = (NSDictionary) clone( aSourceContext, values, aDestinationContext, null ); // apply to destination object takeStoredValuesFromDictionary( aDestination, values ); return aDestination; } // inner classes /** * An ObjectOutputStream that serializes objects with references * to an editing context. The specified context will not be * serialized but referenced, so that a ContextObjectInputStream * can replace the reference with another editing context. */ static private class ContextObjectOutputStream extends ObjectOutputStream { private EditingContextMarker marker = new EditingContextMarker(); protected EOEditingContext editingContext; /** * Specifies the output stream to wrap, * and the source context that should be * referenced but not serialized. */ public ContextObjectOutputStream( OutputStream anOutputStream, EOEditingContext aContext ) throws IOException { super( anOutputStream ); editingContext = aContext; try { enableReplaceObject(true); } catch ( Exception exc ) { exc.printStackTrace(); } } protected Object replaceObject(Object anObject) throws IOException { // if ( anObject == editingContext ) return marker; //FIXME: this should be more strict as above if ( anObject instanceof EOEditingContext ) return marker; return anObject; } } /** * A ContextObjectOutputStream that replaces any objects registered * in the source editing context with markers to be used in * ContextObjectInputStream. */ static private class TransposingContextObjectOutputStream extends ContextObjectOutputStream { protected Object rootObject; /** * Specifies the output stream to wrap, * the source context containing objects that * should be replaced if found, * and the object which should not be re-registered, * which is typically the object being cloned, but * may be null. */ public TransposingContextObjectOutputStream( OutputStream anOutputStream, EOEditingContext aContext, Object anObject ) throws IOException { super( anOutputStream, aContext ); rootObject = anObject; } protected Object replaceObject(Object anObject) throws IOException { if ( anObject == rootObject ) return anObject; if ( editingContext != null ) { EOGlobalID id = editingContext.globalIDForObject( anObject ); if ( id != null ) { Object result = new GlobalIDMarker( id ); //System.out.println( "KeyValueCodingUtilities.replaceObject: returning: " + result ); return result; } } return super.replaceObject( anObject ); } } /** * A marker class so references to objects registered in editing * contexts get transposed rather than cloned. */ static private class GlobalIDMarker implements Serializable { private EOGlobalID id; public GlobalIDMarker( EOGlobalID anID ) { id = anID; } public EOGlobalID getID() { return id; } public String toString() { return "[GlobalIDMarker:"+id+"]"; } } /** * A marker class so references an object's editing context * gets transposed rather than cloned. */ static private class EditingContextMarker implements Serializable { // just a marker class - no implementation necessary } /** * An ObjectInputStream that replaces any markers from * ContextObjectOutputStream with objects registered * in the destination editing context. */ static private class ContextObjectInputStream extends ObjectInputStream { protected EOEditingContext editingContext; protected ClassLoader classLoader; /** * Specifies the output stream to wrap, * the source context containing objects that * should be to replace any markers. * The class loader may be null. */ public ContextObjectInputStream( InputStream anInputStream, EOEditingContext aContext, ClassLoader aClassLoader ) throws IOException { super( anInputStream ); editingContext = aContext; classLoader = aClassLoader; if ( classLoader == null ) { classLoader = KeyValueCodingUtilities.class.getClassLoader(); } try { enableResolveObject(true); } catch ( Exception exc ) { exc.printStackTrace(); } } protected Object resolveObject(Object anObject) throws IOException { if ( anObject instanceof EditingContextMarker ) { return editingContext; } return anObject; } protected Class resolveClass(ObjectStreamClass v) throws IOException, ClassNotFoundException { return classLoader.loadClass( v.getName() ); } } /** * A ContextObjectInputStream that replaces any markers from * TransposingContextObjectOutputStream with objects registered * in the destination editing context. */ static private class TransposingContextObjectInputStream extends ContextObjectInputStream { /** * Specifies the output stream to wrap, * the source context containing objects that * should be to replace any markers. */ public TransposingContextObjectInputStream( InputStream anInputStream, EOEditingContext aContext, ClassLoader aClassLoader ) throws IOException { super( anInputStream, aContext, aClassLoader ); } protected Object resolveObject(Object anObject) throws IOException { if ( anObject instanceof GlobalIDMarker ) { return editingContext.faultForGlobalID( ((GlobalIDMarker)anObject).getID(), editingContext ); } return super.resolveObject( anObject ); } } } /* * $Log$ * Revision 1.3 2006/02/18 22:46:44 cgruber * Add Surrogate map from .util into control's internal package, and fix imports. * * Revision 1.2 2006/02/16 16:47:14 cgruber * Move some classes in to "internal" packages and re-work imports, etc. * * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. * * Revision 1.1 2006/02/16 13:19:57 cgruber * Check in all sources in eclipse-friendly maven-enabled packages. * * Revision 1.15 2003/01/21 22:30:10 mpowers * thaw() now allows you to pass in a class loader. * * Revision 1.14 2002/05/15 13:46:35 mpowers * Exposed freeze and thaw as public. * * Revision 1.13 2001/08/22 19:25:13 mpowers * Added (and commented out) profiling code for freeze. * * Revision 1.12 2001/05/06 18:27:10 mpowers * More broadly catching editing contexts for now. * * Revision 1.11 2001/05/05 13:18:49 mpowers * Fixed: transposing output stream was not returning the object to replace. * * Revision 1.10 2001/05/04 16:57:56 mpowers * Now correctly transposing references to editing contexts when * cloning/copying between editing contexts. * * Revision 1.9 2001/05/04 14:42:58 mpowers * Now getting stored values in KeyValueCoding. * MasterDetail now marks dirty based on whether it's an attribute * or relation. * Implemented editing context marker. * * Revision 1.8 2001/05/02 15:47:40 mpowers * Fixed the pernicious problem with reverts: recordObject was recording * a snapshot of the clone before the transposition-copy happened, * so the revert object would lose all of its transposed relationships. * * Revision 1.7 2001/04/30 12:33:17 mpowers * Fixed problem with use of EONullValue.nullValue(), which can't be used * when we're serializably duplicating objects. * * Revision 1.6 2001/04/30 02:14:25 mpowers * Copying should call takeStoredValueForKeys. * * Revision 1.5 2001/04/29 22:02:45 mpowers * Work on id transposing between editing contexts. * * Revision 1.4 2001/04/29 02:29:31 mpowers * Debugging relationship faulting. * * Revision 1.3 2001/04/28 16:18:44 mpowers * Implementing relationships. * * Revision 1.2 2001/04/28 14:12:23 mpowers * Refactored cloning/copying into KeyValueCodingUtilities. * * Revision 1.1 2001/04/27 23:41:12 mpowers * Contributing file for KeyValueCodingUtilities. * * */