All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.jme3.util.clone.Cloner Maven / Gradle / Ivy

There is a newer version: 3.7.0-stable
Show newest version
/*
 * Copyright (c) 2016-2021 jMonkeyEngine
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
 *   may be used to endorse or promote products derived from this software
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.jme3.util.clone;

import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.logging.Logger;
import java.util.logging.Level;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 *  A deep clone utility that provides similar object-graph-preserving
 *  qualities to typical serialization schemes.  An internal registry
 *  of cloned objects is kept to be used by other objects in the deep
 *  clone process that implement JmeCloneable.
 *
 *  

By default, objects that do not implement JmeCloneable will * be treated like normal Java Cloneable objects. If the object does * not implement the JmeCloneable or the regular JDK Cloneable interfaces * AND has no special handling defined then an IllegalArgumentException * will be thrown.

* *

Enhanced object cloning is done in a two step process. First, * the object is cloned using the normal Java clone() method and stored * in the clone registry. After that, if it implements JmeCloneable then * its cloneFields() method is called to deep clone any of the fields. * This two step process has a few benefits. First, it means that objects * can easily have a regular shallow clone implementation just like any * normal Java objects. Second, the deep cloning of fields happens after * creation which means that the clone is available to future field cloning * to resolve circular references.

* *

Similar to Java serialization, the handling of specific object * types can be customized. This allows certain objects to be cloned gracefully * even if they aren't normally Cloneable. This can also be used as a * sort of filter to keep certain types of objects from being cloned. * (For example, adding the IdentityCloneFunction for Mesh.class would cause * all mesh instances to be shared with the original object graph.)

* *

By default, the Cloner registers several default clone functions * as follows:

*
    *
  • java.util.ArrayList: ListCloneFunction *
  • java.util.LinkedList: ListCloneFunction *
  • java.util.concurrent.CopyOnWriteArrayList: ListCloneFunction *
  • java.util.Vector: ListCloneFunction *
  • java.util.Stack: ListCloneFunction *
  • com.jme3.util.SafeArrayList: ListCloneFunction *
* *

Usage:

*
 *  // Example 1: using an instantiated, reusable cloner.
 *  Cloner cloner = new Cloner();
 *  Foo fooClone = cloner.clone(foo);
 *  cloner.clearIndex(); // prepare it for reuse
 *  Foo fooClone2 = cloner.clone(foo);
 *
 *  // Example 2: using the utility method that self-instantiates a temporary cloner.
 *  Foo fooClone = Cloner.deepClone(foo);
 *
 *  
* * @author Paul Speed */ public class Cloner { private static final Logger log = Logger.getLogger(Cloner.class.getName()); /** * Keeps track of the objects that have been cloned so far. */ final private IdentityHashMap index = new IdentityHashMap<>(); /** * Custom functions for cloning objects. */ final private Map functions = new HashMap<>(); /** * Cache the clone methods once for all cloners. */ private static final Map methodCache = new ConcurrentHashMap<>(); /** * Creates a new cloner with only default clone functions and an empty * object index. */ public Cloner() { // Register some standard types ListCloneFunction listFunction = new ListCloneFunction(); functions.put(java.util.ArrayList.class, listFunction); functions.put(java.util.LinkedList.class, listFunction); functions.put(java.util.concurrent.CopyOnWriteArrayList.class, listFunction); functions.put(java.util.Vector.class, listFunction); functions.put(java.util.Stack.class, listFunction); functions.put(com.jme3.util.SafeArrayList.class, listFunction); } /** * Convenience utility function that creates a new Cloner, uses it to * deep clone the object, and then returns the result. * * @param the type of object to be cloned * @param object the object to be cloned (may be null) * @return a new instance, or a cached value, or null */ public static T deepClone(T object) { return new Cloner().clone(object); } /** * Deeps clones the specified object, reusing previous clones when possible. * *

Object cloning priority works as follows:

*
    *
  • If the object has already been cloned then its clone is returned. *
  • If there is a custom CloneFunction then it is called to clone the object. *
  • If the object implements Cloneable then its clone() method is called, arrays are * deep cloned with entries passing through clone(). *
  • If the object implements JmeCloneable then its cloneFields() method is called on the * clone. *
  • Else an IllegalArgumentException is thrown. *
* * Note: objects returned by this method may not have yet had their cloneField() * method called. * * @param the type of object to be cloned * @param object the object to be cloned (may be null) * @return a new instance, or a cached value, or null */ public T clone(T object) { return clone(object, true); } /** * Internal method to work around a Java generics typing issue by * isolating the 'bad' case into a method with suppressed warnings. */ @SuppressWarnings("unchecked") private Class objectClass(T object) { // This should be 100% allowed without a cast but Java generics // is not that smart sometimes. // Wrapping it in a method at least isolates the warning suppression return (Class)object.getClass(); } /** * Deeps clones the specified object, reusing previous clones when possible. * *

Object cloning priority works as follows:

*
    *
  • If the object has already been cloned then its clone is returned. *
  • If useFunctions is true and there is a custom CloneFunction then it is * called to clone the object. *
  • If the object implements Cloneable then its clone() method is called, arrays are * deep cloned with entries passing through clone(). *
  • If the object implements JmeCloneable then its cloneFields() method is called on the * clone. *
  • Else an IllegalArgumentException is thrown. *
* *

The ability to selectively use clone functions is useful when * being called from a clone function.

* * Note: objects returned by this method may not have yet had their cloneField() * method called. * * @param the type of object to be cloned * @param object the object to be cloned (may be null) * @param useFunctions true→use custom clone functions, * false→don't use * @return a new instance, or a cached value, or null */ public T clone(T object, boolean useFunctions) { if (object == null) { return null; } if (log.isLoggable(Level.FINER)) { log.finer("cloning:" + object.getClass() + "@" + System.identityHashCode(object)); } Class type = objectClass(object); // Check the index to see if we already have it Object clone = index.get(object); if (clone != null || index.containsKey(object)) { if (log.isLoggable(Level.FINER)) { log.finer("cloned:" + object.getClass() + "@" + System.identityHashCode(object) + " as cached:" + (clone == null ? "null" : (clone.getClass() + "@" + System.identityHashCode(clone)))); } return type.cast(clone); } // See if there is a custom function... that trumps everything. CloneFunction f = getCloneFunction(type); if (f != null) { T result = f.cloneObject(this, object); // Store the object in the identity map so that any circular references // are resolvable. index.put(object, result); // Now call the function again to deep clone the fields f.cloneFields(this, result, object); 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()) { // Perform an array clone clone = arrayClone(object); // Array clone already indexes the clone } else if (object instanceof JmeCloneable) { // Use the two-step cloning semantics clone = ((JmeCloneable)object).jmeClone(); // Store the object in the identity map so that any circular references // are resolvable index.put(object, clone); ((JmeCloneable) clone).cloneFields(this, object); } else if (object instanceof Cloneable) { // Perform a regular Java shallow clone try { clone = javaClone(object); } catch (CloneNotSupportedException e) { throw new IllegalArgumentException("Object is not cloneable, type:" + type, e); } // Store the object in the identity map so that any circular references // are resolvable index.put(object, clone); } else { 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); } /** * Sets a custom CloneFunction for implementations of the specified Java type. Some * inheritance checks are made but no disambiguation is performed. *

Note: in the general case, it is better to register against specific classes and * not super-classes or super-interfaces unless you know specifically that they are cloneable.

*

By default ListCloneFunction is registered for ArrayList, LinkedList, CopyOnWriteArrayList, * Vector, Stack, and JME's SafeArrayList.

* * @param the type of object to be cloned * @param type the type of object to be cloned * @param function the function to set, or null to cancel any previous * setting */ public void setCloneFunction(Class type, CloneFunction function) { if (function == null) { functions.remove(type); } else { functions.put(type, function); } } /** * Returns a previously registered clone function for the specified type or null * if there is no custom clone function for the type. * * @param the type of object to be cloned * @param type the type of object to be cloned * @return the registered function, or null if none */ @SuppressWarnings("unchecked") public CloneFunction getCloneFunction(Class type) { CloneFunction result = functions.get(type); if (result == null) { // Do a more exhaustive search for (Map.Entry e : functions.entrySet()) { if (e.getKey().isAssignableFrom(type)) { result = e.getValue(); break; } } if (result != null) { // Cache it for later functions.put(type, result); } } return result; } /** * 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. * * @param the type of object to be detected and returned * @param original the instance to be detected (alias created) * @param clone the instance to be returned (alias created) */ public 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. * * @param o the object to be tested * @return true if the object has been cloned, otherwise false */ public boolean isCloned(Object o) { return index.containsKey(o); } /** * Clears the object index allowing the cloner to be reused for a brand-new * cloning operation. */ public void clearIndex() { index.clear(); } /** * 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 * it is called. That's because these are shallow clones and have not (and may * not ever, depending on the caller) get resolved. * *

This method is provided as a convenient way for CloneFunctions to call * clone() and objects without necessarily knowing their real type.

* * @param the type of object to be cloned * @param object the object to be cloned (may be null) * @return a new instance or null * @throws CloneNotSupportedException if the object has no public clone method */ public T javaClone(T object) throws CloneNotSupportedException { if (object == null) { return null; } Method m = methodCache.get(object.getClass()); if (m == null) { try { // Lookup the method and cache it m = object.getClass().getMethod("clone"); } catch (NoSuchMethodException e) { throw new CloneNotSupportedException("No public clone method found for:" + object.getClass()); } methodCache.put(object.getClass(), m); // Note: yes we might cache the method twice... but so what? } try { Class type = objectClass(object); return type.cast(m.invoke(object)); } catch (IllegalAccessException | InvocationTargetException e) { throw new RuntimeException("Error cloning object of type:" + object.getClass(), e); } } /** * Clones a primitive array by coping it and clones an object * array by coping it and then running each of its values through * Cloner.clone(). * * @param the type of array to be cloned * @param object the array to be cloned * @return a new array */ protected T arrayClone(T object) { // Java doesn't support the cloning of arrays through reflection unless // you open access to Object's protected clone array... which requires // elevated privileges. So we will do a work-around that is slightly less // elegant. // This should be 100% allowed without a case but Java generics // is not that smart Class type = objectClass(object); Class elementType = type.getComponentType(); int size = Array.getLength(object); Object clone = Array.newInstance(elementType, size); // Store the clone for later lookups index.put(object, clone); if (elementType.isPrimitive()) { // Then our job is a bit easier System.arraycopy(object, 0, clone, 0, size); } else { // Else it's an object array, so we'll clone it and its children. for (int i = 0; i < size; i++) { Object element = clone(Array.get(object, i)); Array.set(clone, i, element); } } return type.cast(clone); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy