diff options
| author | Benjamin Culkin <scorpress@gmail.com> | 2024-05-19 17:56:33 -0400 |
|---|---|---|
| committer | Benjamin Culkin <scorpress@gmail.com> | 2024-05-19 17:56:33 -0400 |
| commit | aedc34d55462a75e329bbf342251ff6504cd117e (patch) | |
| tree | bcc8f1f2352582717b484df302aeea6696b8f000 /projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/KeyValueCodingUtilities.java | |
Initial import from SVN
Diffstat (limited to 'projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/KeyValueCodingUtilities.java')
| -rw-r--r-- | projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/KeyValueCodingUtilities.java | 740 |
1 files changed, 740 insertions, 0 deletions
diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/KeyValueCodingUtilities.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/KeyValueCodingUtilities.java new file mode 100644 index 0000000..1aa2147 --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/KeyValueCodingUtilities.java @@ -0,0 +1,740 @@ +/* +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. + * + * + */ + + |
