/*
*
* * Copyright 2010, Unitils.org
* *
* * Licensed under the Apache License, Version 2.0 (the "License");
* * you may not use this file except in compliance with the License.
* * You may obtain a copy of the License at
* *
* * http://www.apache.org/licenses/LICENSE-2.0
* *
* * Unless required by applicable law or agreed to in writing, software
* * distributed under the License is distributed on an "AS IS" BASIS,
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* * See the License for the specific language governing permissions and
* * limitations under the License.
*
*/
package org.unitils.mock.core.proxy;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.objenesis.Objenesis;
import org.objenesis.ObjenesisStd;
import org.unitils.core.UnitilsException;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.IdentityHashMap;
import java.util.Map;
import static java.lang.reflect.Modifier.isStatic;
import static java.lang.reflect.Modifier.isTransient;
import static org.unitils.mock.core.proxy.ProxyUtils.isProxy;
/**
* Utility class for deep cloning objects.
* In a deep clone, not only the object itself is cloned, but also all the inner objects.
*
* @author Tim Ducheyne
* @author Filip Neven
* @author Kenny Claes
*/
public class CloneUtil {
/* The logger instance for this class */
private static Log logger = LogFactory.getLog(CloneUtil.class);
/* Objenesis instance for creating new instances of types */
private static Objenesis objenesis = new ObjenesisStd(true);
/**
* Creates a deep clone of the given object. If for some reason, the clone cannot be made, a warning is logged
* and the object itself will be returned. This is also true for all inner objects. If an inner object
* cannot be cloned, the object itself is used instead.
*
* @param object The object to clone
* @return The cloned instance
*/
@SuppressWarnings({"unchecked"})
public static <T> T createDeepClone(T object) {
try {
return (T) cloneObject(object, new IdentityHashMap<Object, Object>());
} catch (Throwable e) {
throw new UnitilsException("Unexpected exception during cloning of " + object, e);
}
}
/**
* Actual implementation of the cloning.
*
* It will try several ways to clone the object. First it will look for the simple cases: null, primitives,
* immutables... If not it will check whether it's an array and clone it using the {@link #cloneArray} method.
* Finally it will see whether the object is cloneable and the clone method can be used. If not, Objenisis is
* used to create the instance. The last step is to recursively do the same operation for the inner fields.
*
* An object is cloned once. All created clones are put in a cache and if an object is to be cloned a second time,
* the cached instance is used. This way the object graph is preserved.
*
* @param instanceToClone The instance, not null
* @param cloneCache The cached clones, not null
* @return The clone, the instance to clone if the clone could not be made
*/
protected static Object cloneObject(Object instanceToClone, Map<Object, Object> cloneCache) throws Throwable {
if (instanceToClone == null) {
return null;
}
// check whether the instance was already cloned, this will preserve the object graph
if (cloneCache.containsKey(instanceToClone)) {
return cloneCache.get(instanceToClone);
}
// if the value is immutable, return the instance itself
if (isImmutable(instanceToClone)) {
return instanceToClone;
}
// check for arrays
if (instanceToClone.getClass().isArray()) {
return cloneArray(instanceToClone, cloneCache);
}
// if the instance is cloneable, try to clone it
if (instanceToClone instanceof Cloneable) {
return createInstanceUsingClone(instanceToClone);
}
// don't clone java classes (unless they are cloneable)
if (isJdkClass(instanceToClone)) {
return instanceToClone;
}
// don't clone proxies
if (isProxy(instanceToClone)) {
return instanceToClone;
}
// try to clone it ourselves
Object clonedInstance = createInstanceUsingObjenesis(instanceToClone);
// Unable to create an instance
if (clonedInstance == null) {
logger.warn("Could not create an instance of class " + instanceToClone.getClass() + " using objenesis");
return instanceToClone;
}
// cache the clone
cloneCache.put(instanceToClone, clonedInstance);
// recursively do the same for all inner fields
cloneFields(instanceToClone.getClass(), instanceToClone, clonedInstance, cloneCache);
return clonedInstance;
}
/**
* @param instanceToClone The instance, not null
* @return True if the instance is immutable, e.g. a primitive
*/
protected static boolean isImmutable(Object instanceToClone) {
Class<?> clazz = instanceToClone.getClass();
if (clazz.isPrimitive() || clazz.isEnum() || clazz.isAnnotation()) {
return true;
}
if (instanceToClone instanceof Number || instanceToClone instanceof String || instanceToClone instanceof Character || instanceToClone instanceof Boolean) {
return true;
}
if (clazz.getName().startsWith("com.google.common.collect") && clazz.getName().contains("Immutable")) {
return true;
}
return false;
}
/**
* @param instanceToClone The instance, not null
* @return True if the instance is should not be cloned, e.g. a java lang class or a data source
*/
protected static boolean isJdkClass(Object instanceToClone) {
if (instanceToClone instanceof Collection || instanceToClone instanceof Map) {
// make sure to clone collections
return false;
}
String className = instanceToClone.getClass().getName();
if (className.startsWith("java.")) {
return true;
}
return false;
}
/**
* If the given value is cloneable and the cloning succeeds, the clone is returned, else null is returned.
*
* @param instanceToClone The instance, not null
* @return The clone if it could be cloned, else null
*/
protected static Object createInstanceUsingClone(Object instanceToClone) {
try {
Method cloneMethod = Object.class.getDeclaredMethod("clone");
cloneMethod.setAccessible(true);
return cloneMethod.invoke(instanceToClone);
} catch (Throwable t) {
return null;
}
}
/**
* Tries to create an instance of the same type as the given value using Objenesis.
*
* @param instanceToClone The instance, not null
* @return The new instance if it could be created, else null
*/
protected static Object createInstanceUsingObjenesis(Object instanceToClone) {
try {
return objenesis.newInstance(instanceToClone.getClass());
} catch (Throwable t) {
return null;
}
}
/**
* Clones all values in all fields of the given class and superclasses.
*
* @param clazz The current class
* @param instanceToClone The instance, not null
* @param clonedInstance The clone, not null
* @param cloneCache The cached clones, not null
*/
protected static void cloneFields(Class<?> clazz, Object instanceToClone, Object clonedInstance, Map<Object, Object> cloneCache) throws Throwable {
if (clazz == null || Object.class.equals(clazz)) {
return;
}
Field[] fields = clazz.getDeclaredFields();
AccessibleObject.setAccessible(fields, true);
for (Field field : fields) {
// skip static fields
if (isStatic(field.getModifiers())) {
continue;
}
if (field.getDeclaringClass().getName().startsWith("org.hibernate")
&& isTransient(field.getModifiers())) {
continue;
}
Object fieldValue = field.get(instanceToClone);
Object clonedFieldValue = cloneObject(fieldValue, cloneCache);
field.set(clonedInstance, clonedFieldValue);
}
cloneFields(clazz.getSuperclass(), instanceToClone, clonedInstance, cloneCache);
}
/**
* Clones the given array and all it's elements.
*
* @param arrayToClone The array, not null
* @param cloneCache The cached clones, not null
* @return The cloned array, not null
*/
protected static Object cloneArray(Object arrayToClone, Map<Object, Object> cloneCache) throws Throwable {
int lenght = Array.getLength(arrayToClone);
Object clonedArray = Array.newInstance(arrayToClone.getClass().getComponentType(), lenght);
// Make sure we put the array in the cache before we start cloning the elements, since the array itself may also
// be one of the elements, and in this case we want to reuse the same element, to avoid infinite recursion.
cloneCache.put(arrayToClone, clonedArray);
for (int i = 0; i < lenght; i++) {
Object elementValue = Array.get(arrayToClone, i);
Object clonedElementValue = cloneObject(elementValue, cloneCache);
Array.set(clonedArray, i, clonedElementValue);
}
return clonedArray;
}
}