Added 'finer' logging for the clone() method to provide visibility for
debugging. Added a setClonedValue() method to force uncloned or precloned references in some specific use-cases. Added an isCloned() method to tell if an object has already been cloned in this cloner's 'session'.
This commit is contained in:
parent
c6aac78f42
commit
2028f3b3f8
@ -37,6 +37,8 @@ import java.lang.reflect.InvocationTargetException;
|
|||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.IdentityHashMap;
|
import java.util.IdentityHashMap;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
import java.util.logging.Level;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
@ -49,7 +51,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||||||
* <p>By default, objects that do not implement JmeCloneable will
|
* <p>By default, objects that do not implement JmeCloneable will
|
||||||
* be treated like normal Java Cloneable objects. If the object does
|
* be treated like normal Java Cloneable objects. If the object does
|
||||||
* not implement the JmeCloneable or the regular JDK Cloneable interfaces
|
* not implement the JmeCloneable or the regular JDK Cloneable interfaces
|
||||||
* AND has no special handling defined then an IllegalArgumentException
|
* AND has no special handling defined then an IllegalArgumentException
|
||||||
* will be thrown.</p>
|
* will be thrown.</p>
|
||||||
*
|
*
|
||||||
* <p>Enhanced object cloning is done in a two step process. First,
|
* <p>Enhanced object cloning is done in a two step process. First,
|
||||||
@ -60,7 +62,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||||||
* can easily have a regular shallow clone implementation just like any
|
* can easily have a regular shallow clone implementation just like any
|
||||||
* normal Java objects. Second, the deep cloning of fields happens after
|
* normal Java objects. Second, the deep cloning of fields happens after
|
||||||
* creation wich means that the clone is available to future field cloning
|
* creation wich means that the clone is available to future field cloning
|
||||||
* to resolve circular references.</p>
|
* to resolve circular references.</p>
|
||||||
*
|
*
|
||||||
* <p>Similar to Java serialization, the handling of specific object
|
* <p>Similar to Java serialization, the handling of specific object
|
||||||
* types can be customized. This allows certain objects to be cloned gracefully
|
* types can be customized. This allows certain objects to be cloned gracefully
|
||||||
@ -87,31 +89,33 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||||||
* Foo fooClone = cloner.clone(foo);
|
* Foo fooClone = cloner.clone(foo);
|
||||||
* cloner.clearIndex(); // prepare it for reuse
|
* cloner.clearIndex(); // prepare it for reuse
|
||||||
* Foo fooClone2 = cloner.clone(foo);
|
* Foo fooClone2 = cloner.clone(foo);
|
||||||
*
|
*
|
||||||
* // Example 2: using the utility method that self-instantiates a temporary cloner.
|
* // Example 2: using the utility method that self-instantiates a temporary cloner.
|
||||||
* Foo fooClone = Cloner.deepClone(foo);
|
* Foo fooClone = Cloner.deepClone(foo);
|
||||||
*
|
*
|
||||||
* </pre>
|
* </pre>
|
||||||
*
|
*
|
||||||
* @author Paul Speed
|
* @author Paul Speed
|
||||||
*/
|
*/
|
||||||
public class Cloner {
|
public class Cloner {
|
||||||
|
|
||||||
|
static Logger log = Logger.getLogger(Cloner.class.getName());
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Keeps track of the objects that have been cloned so far.
|
* Keeps track of the objects that have been cloned so far.
|
||||||
*/
|
*/
|
||||||
private IdentityHashMap<Object, Object> index = new IdentityHashMap<Object, Object>();
|
private IdentityHashMap<Object, Object> index = new IdentityHashMap<Object, Object>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom functions for cloning objects.
|
* Custom functions for cloning objects.
|
||||||
*/
|
*/
|
||||||
private Map<Class, CloneFunction> functions = new HashMap<Class, CloneFunction>();
|
private Map<Class, CloneFunction> functions = new HashMap<Class, CloneFunction>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cache the clone methods once for all cloners.
|
* Cache the clone methods once for all cloners.
|
||||||
*/
|
*/
|
||||||
private static final Map<Class, Method> methodCache = new ConcurrentHashMap<>();
|
private static final Map<Class, Method> methodCache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new cloner with only default clone functions and an empty
|
* Creates a new cloner with only default clone functions and an empty
|
||||||
* object index.
|
* object index.
|
||||||
@ -121,41 +125,41 @@ public class Cloner {
|
|||||||
ListCloneFunction listFunction = new ListCloneFunction();
|
ListCloneFunction listFunction = new ListCloneFunction();
|
||||||
functions.put(java.util.ArrayList.class, listFunction);
|
functions.put(java.util.ArrayList.class, listFunction);
|
||||||
functions.put(java.util.LinkedList.class, listFunction);
|
functions.put(java.util.LinkedList.class, listFunction);
|
||||||
functions.put(java.util.concurrent.CopyOnWriteArrayList.class, listFunction);
|
functions.put(java.util.concurrent.CopyOnWriteArrayList.class, listFunction);
|
||||||
functions.put(java.util.Vector.class, listFunction);
|
functions.put(java.util.Vector.class, listFunction);
|
||||||
functions.put(java.util.Stack.class, listFunction);
|
functions.put(java.util.Stack.class, listFunction);
|
||||||
functions.put(com.jme3.util.SafeArrayList.class, listFunction);
|
functions.put(com.jme3.util.SafeArrayList.class, listFunction);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convenience utility function that creates a new Cloner, uses it to
|
* Convenience utility function that creates a new Cloner, uses it to
|
||||||
* deep clone the object, and then returns the result.
|
* deep clone the object, and then returns the result.
|
||||||
*/
|
*/
|
||||||
public static <T> T deepClone( T object ) {
|
public static <T> T deepClone( T object ) {
|
||||||
return new Cloner().clone(object);
|
return new Cloner().clone(object);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deeps clones the specified object, reusing previous clones when possible.
|
* Deeps clones the specified object, reusing previous clones when possible.
|
||||||
*
|
*
|
||||||
* <p>Object cloning priority works as follows:</p>
|
* <p>Object cloning priority works as follows:</p>
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>If the object has already been cloned then its clone is returned.
|
* <li>If the object has already been cloned then its clone is returned.
|
||||||
* <li>If there is a custom CloneFunction then it is called to clone the object.
|
* <li>If there is a custom CloneFunction then it is called to clone the object.
|
||||||
* <li>If the object implements Cloneable then its clone() method is called, arrays are
|
* <li>If the object implements Cloneable then its clone() method is called, arrays are
|
||||||
* deep cloned with entries passing through clone().
|
* deep cloned with entries passing through clone().
|
||||||
* <li>If the object implements JmeCloneable then its cloneFields() method is called on the
|
* <li>If the object implements JmeCloneable then its cloneFields() method is called on the
|
||||||
* clone.
|
* clone.
|
||||||
* <li>Else an IllegalArgumentException is thrown.
|
* <li>Else an IllegalArgumentException is thrown.
|
||||||
* </ul>
|
* </ul>
|
||||||
*
|
*
|
||||||
* Note: objects returned by this method may not have yet had their cloneField()
|
* Note: objects returned by this method may not have yet had their cloneField()
|
||||||
* method called.
|
* method called.
|
||||||
*/
|
*/
|
||||||
public <T> T clone( T object ) {
|
public <T> T clone( T object ) {
|
||||||
return clone(object, true);
|
return clone(object, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal method to work around a Java generics typing issue by
|
* Internal method to work around a Java generics typing issue by
|
||||||
* isolating the 'bad' case into a method with suppressed warnings.
|
* isolating the 'bad' case into a method with suppressed warnings.
|
||||||
@ -167,20 +171,20 @@ public class Cloner {
|
|||||||
// Wrapping it in a method at least isolates the warning suppression
|
// Wrapping it in a method at least isolates the warning suppression
|
||||||
return (Class<T>)object.getClass();
|
return (Class<T>)object.getClass();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deeps clones the specified object, reusing previous clones when possible.
|
* Deeps clones the specified object, reusing previous clones when possible.
|
||||||
*
|
*
|
||||||
* <p>Object cloning priority works as follows:</p>
|
* <p>Object cloning priority works as follows:</p>
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>If the object has already been cloned then its clone is returned.
|
* <li>If the object has already been cloned then its clone is returned.
|
||||||
* <li>If useFunctions is true and there is a custom CloneFunction then it is
|
* <li>If useFunctions is true and there is a custom CloneFunction then it is
|
||||||
* called to clone the object.
|
* called to clone the object.
|
||||||
* <li>If the object implements Cloneable then its clone() method is called, arrays are
|
* <li>If the object implements Cloneable then its clone() method is called, arrays are
|
||||||
* deep cloned with entries passing through clone().
|
* deep cloned with entries passing through clone().
|
||||||
* <li>If the object implements JmeCloneable then its cloneFields() method is called on the
|
* <li>If the object implements JmeCloneable then its cloneFields() method is called on the
|
||||||
* clone.
|
* clone.
|
||||||
* <li>Else an IllegalArgumentException is thrown.
|
* <li>Else an IllegalArgumentException is thrown.
|
||||||
* </ul>
|
* </ul>
|
||||||
*
|
*
|
||||||
* <p>The abililty to selectively use clone functions is useful when
|
* <p>The abililty to selectively use clone functions is useful when
|
||||||
@ -188,71 +192,94 @@ public class Cloner {
|
|||||||
*
|
*
|
||||||
* Note: objects returned by this method may not have yet had their cloneField()
|
* Note: objects returned by this method may not have yet had their cloneField()
|
||||||
* method called.
|
* method called.
|
||||||
*/
|
*/
|
||||||
public <T> T clone( T object, boolean useFunctions ) {
|
public <T> T clone( T object, boolean useFunctions ) {
|
||||||
|
|
||||||
if( object == null ) {
|
if( object == null ) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
Class<T> type = objectClass(object);
|
|
||||||
|
if( log.isLoggable(Level.FINER) ) {
|
||||||
|
log.finer("cloning:" + object.getClass() + "@" + System.identityHashCode(object));
|
||||||
|
}
|
||||||
|
|
||||||
|
Class<T> type = objectClass(object);
|
||||||
|
|
||||||
// Check the index to see if we already have it
|
// Check the index to see if we already have it
|
||||||
Object clone = index.get(object);
|
Object clone = index.get(object);
|
||||||
if( clone != null ) {
|
if( clone != null ) {
|
||||||
return type.cast(clone);
|
if( log.isLoggable(Level.FINER) ) {
|
||||||
|
log.finer("cloned:" + object.getClass() + "@" + System.identityHashCode(object)
|
||||||
|
+ " as cached:" + clone.getClass() + "@" + System.identityHashCode(clone));
|
||||||
|
}
|
||||||
|
return type.cast(clone);
|
||||||
}
|
}
|
||||||
|
|
||||||
// See if there is a custom function... that trumps everything.
|
// See if there is a custom function... that trumps everything.
|
||||||
CloneFunction<T> f = getCloneFunction(type);
|
CloneFunction<T> f = getCloneFunction(type);
|
||||||
if( f != null ) {
|
if( f != null ) {
|
||||||
T result = f.cloneObject(this, object);
|
T result = f.cloneObject(this, object);
|
||||||
|
|
||||||
// Store the object in the identity map so that any circular references
|
// Store the object in the identity map so that any circular references
|
||||||
// are resolvable.
|
// are resolvable.
|
||||||
index.put(object, result);
|
index.put(object, result);
|
||||||
|
|
||||||
// Now call the function again to deep clone the fields
|
// Now call the function again to deep clone the fields
|
||||||
f.cloneFields(this, result, object);
|
f.cloneFields(this, result, object);
|
||||||
|
|
||||||
return result;
|
if( log.isLoggable(Level.FINER) ) {
|
||||||
|
if( result == null ) {
|
||||||
|
log.finer("cloned:" + object.getClass() + "@" + System.identityHashCode(object)
|
||||||
|
+ " as transformed:null");
|
||||||
|
} else {
|
||||||
|
log.finer("clone:" + object.getClass() + "@" + System.identityHashCode(object)
|
||||||
|
+ " as transformed:" + result.getClass() + "@" + System.identityHashCode(result));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
if( object.getClass().isArray() ) {
|
if( object.getClass().isArray() ) {
|
||||||
// Perform an array clone
|
// Perform an array clone
|
||||||
clone = arrayClone(object);
|
clone = arrayClone(object);
|
||||||
|
|
||||||
// Array clone already indexes the clone
|
// Array clone already indexes the clone
|
||||||
} else if( object instanceof JmeCloneable ) {
|
} else if( object instanceof JmeCloneable ) {
|
||||||
// Use the two-step cloning semantics
|
// Use the two-step cloning semantics
|
||||||
clone = ((JmeCloneable)object).jmeClone();
|
clone = ((JmeCloneable)object).jmeClone();
|
||||||
|
|
||||||
// Store the object in the identity map so that any circular references
|
// Store the object in the identity map so that any circular references
|
||||||
// are resolvable
|
// are resolvable
|
||||||
index.put(object, clone);
|
index.put(object, clone);
|
||||||
|
|
||||||
((JmeCloneable)clone).cloneFields(this, object);
|
((JmeCloneable)clone).cloneFields(this, object);
|
||||||
} else if( object instanceof Cloneable ) {
|
} else if( object instanceof Cloneable ) {
|
||||||
|
|
||||||
// Perform a regular Java shallow clone
|
// Perform a regular Java shallow clone
|
||||||
try {
|
try {
|
||||||
clone = javaClone(object);
|
clone = javaClone(object);
|
||||||
} catch( CloneNotSupportedException e ) {
|
} catch( CloneNotSupportedException e ) {
|
||||||
throw new IllegalArgumentException("Object is not cloneable, type:" + type, e);
|
throw new IllegalArgumentException("Object is not cloneable, type:" + type, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the object in the identity map so that any circular references
|
// Store the object in the identity map so that any circular references
|
||||||
// are resolvable
|
// are resolvable
|
||||||
index.put(object, clone);
|
index.put(object, clone);
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalArgumentException("Object is not cloneable, type:" + type);
|
throw new IllegalArgumentException("Object is not cloneable, type:" + type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if( log.isLoggable(Level.FINER) ) {
|
||||||
|
log.finer("cloned:" + object.getClass() + "@" + System.identityHashCode(object)
|
||||||
|
+ " as " + clone.getClass() + "@" + System.identityHashCode(clone));
|
||||||
|
}
|
||||||
return type.cast(clone);
|
return type.cast(clone);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets a custom CloneFunction for the exact Java type. Note: no inheritence
|
* Sets a custom CloneFunction for the exact Java type. Note: no inheritence
|
||||||
* checks are performed so a function must be registered for each specific type
|
* checks are performed so a function must be registered for each specific type
|
||||||
* that it handles. By default ListCloneFunction is registered for
|
* that it handles. By default ListCloneFunction is registered for
|
||||||
* ArrayList, LinkedList, CopyOnWriteArrayList, Vector, Stack, and JME's SafeArrayList.
|
* ArrayList, LinkedList, CopyOnWriteArrayList, Vector, Stack, and JME's SafeArrayList.
|
||||||
*/
|
*/
|
||||||
public <T> void setCloneFunction( Class<T> type, CloneFunction<T> function ) {
|
public <T> void setCloneFunction( Class<T> type, CloneFunction<T> function ) {
|
||||||
@ -262,24 +289,45 @@ public class Cloner {
|
|||||||
functions.put(type, function);
|
functions.put(type, function);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a previously registered clone function for the specified type or null
|
* Returns a previously registered clone function for the specified type or null
|
||||||
* if there is no custom clone function for the type.
|
* if there is no custom clone function for the type.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public <T> CloneFunction<T> getCloneFunction( Class<T> type ) {
|
public <T> CloneFunction<T> getCloneFunction( Class<T> type ) {
|
||||||
return (CloneFunction<T>)functions.get(type);
|
return (CloneFunction<T>)functions.get(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forces an object to be added to the indexing cache such that attempts
|
||||||
|
* to clone the 'original' will always result in the 'clone' being returned.
|
||||||
|
* This can be used to stub out specific values from being cloned or to
|
||||||
|
* force global shared instances to be used even if the object is cloneable
|
||||||
|
* normally.
|
||||||
|
*/
|
||||||
|
public <T> void setClonedValue( T original, T clone ) {
|
||||||
|
index.put(original, clone);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the specified object has already been cloned
|
||||||
|
* by this cloner during this session. Cloned objects are cached
|
||||||
|
* for later use and it's sometimes convenient to know if some
|
||||||
|
* objects have already been cloned.
|
||||||
|
*/
|
||||||
|
public boolean isCloned( Object o ) {
|
||||||
|
return index.containsKey(o);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clears the object index allowing the cloner to be reused for a brand new
|
* Clears the object index allowing the cloner to be reused for a brand new
|
||||||
* cloning operation.
|
* cloning operation.
|
||||||
*/
|
*/
|
||||||
public void clearIndex() {
|
public void clearIndex() {
|
||||||
index.clear();
|
index.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Performs a raw shallow Java clone using reflection. This call does NOT
|
* Performs a raw shallow Java clone using reflection. This call does NOT
|
||||||
* check against the clone index and so will return new objects every time
|
* check against the clone index and so will return new objects every time
|
||||||
@ -287,51 +335,51 @@ public class Cloner {
|
|||||||
* not ever, depending on the caller) get resolved.
|
* not ever, depending on the caller) get resolved.
|
||||||
*
|
*
|
||||||
* <p>This method is provided as a convenient way for CloneFunctions to call
|
* <p>This method is provided as a convenient way for CloneFunctions to call
|
||||||
* clone() and objects without necessarily knowing their real type.</p>
|
* clone() and objects without necessarily knowing their real type.</p>
|
||||||
*/
|
*/
|
||||||
public <T> T javaClone( T object ) throws CloneNotSupportedException {
|
public <T> T javaClone( T object ) throws CloneNotSupportedException {
|
||||||
Method m = methodCache.get(object.getClass());
|
Method m = methodCache.get(object.getClass());
|
||||||
if( m == null ) {
|
if( m == null ) {
|
||||||
try {
|
try {
|
||||||
// Lookup the method and cache it
|
// Lookup the method and cache it
|
||||||
m = object.getClass().getMethod("clone");
|
m = object.getClass().getMethod("clone");
|
||||||
} catch( NoSuchMethodException e ) {
|
} catch( NoSuchMethodException e ) {
|
||||||
throw new CloneNotSupportedException("No public clone method found for:" + object.getClass());
|
throw new CloneNotSupportedException("No public clone method found for:" + object.getClass());
|
||||||
}
|
}
|
||||||
methodCache.put(object.getClass(), m);
|
methodCache.put(object.getClass(), m);
|
||||||
|
|
||||||
// Note: yes we might cache the method twice... but so what?
|
// Note: yes we might cache the method twice... but so what?
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Class<? extends T> type = objectClass(object);
|
Class<? extends T> type = objectClass(object);
|
||||||
return type.cast(m.invoke(object));
|
return type.cast(m.invoke(object));
|
||||||
} catch( IllegalAccessException | InvocationTargetException e ) {
|
} catch( IllegalAccessException | InvocationTargetException e ) {
|
||||||
throw new RuntimeException("Error cloning object of type:" + object.getClass(), e);
|
throw new RuntimeException("Error cloning object of type:" + object.getClass(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clones a primitive array by coping it and clones an object
|
* Clones a primitive array by coping it and clones an object
|
||||||
* array by coping it and then running each of its values through
|
* array by coping it and then running each of its values through
|
||||||
* Cloner.clone().
|
* Cloner.clone().
|
||||||
*/
|
*/
|
||||||
protected <T> T arrayClone( T object ) {
|
protected <T> T arrayClone( T object ) {
|
||||||
|
|
||||||
// Java doesn't support the cloning of arrays through reflection unless
|
// Java doesn't support the cloning of arrays through reflection unless
|
||||||
// you open access to Object's protected clone array... which requires
|
// you open access to Object's protected clone array... which requires
|
||||||
// elevated privileges. So we will do a work-around that is slightly less
|
// elevated privileges. So we will do a work-around that is slightly less
|
||||||
// elegant.
|
// elegant.
|
||||||
// This should be 100% allowed without a case but Java generics
|
// This should be 100% allowed without a case but Java generics
|
||||||
// is not that smart
|
// is not that smart
|
||||||
Class<T> type = objectClass(object);
|
Class<T> type = objectClass(object);
|
||||||
Class elementType = type.getComponentType();
|
Class elementType = type.getComponentType();
|
||||||
int size = Array.getLength(object);
|
int size = Array.getLength(object);
|
||||||
Object clone = Array.newInstance(elementType, size);
|
Object clone = Array.newInstance(elementType, size);
|
||||||
|
|
||||||
// Store the clone for later lookups
|
// Store the clone for later lookups
|
||||||
index.put(object, clone);
|
index.put(object, clone);
|
||||||
|
|
||||||
if( elementType.isPrimitive() ) {
|
if( elementType.isPrimitive() ) {
|
||||||
// Then our job is a bit easier
|
// Then our job is a bit easier
|
||||||
System.arraycopy(object, 0, clone, 0, size);
|
System.arraycopy(object, 0, clone, 0, size);
|
||||||
@ -341,8 +389,8 @@ public class Cloner {
|
|||||||
Object element = clone(Array.get(object, i));
|
Object element = clone(Array.get(object, i));
|
||||||
Array.set(clone, i, element);
|
Array.set(clone, i, element);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return type.cast(clone);
|
return type.cast(clone);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user