/* 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. * * */