/* * Copyright 2009 the original author or authors. * * 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.powermock.classloading; import org.powermock.api.support.ClassLoaderUtil; import org.powermock.api.support.SafeExceptionRethrower; import org.powermock.classloading.spi.DeepClonerSPI; import org.powermock.classloading.spi.DoNotClone; import org.powermock.core.ListMap; import org.powermock.reflect.Whitebox; import sun.misc.Unsafe; import java.io.*; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Map; /** * The purpose of the deep cloner is to create a deep clone of an object. An * object can also be cloned to a different class-loader. * <p> */ public class DeepCloner implements DeepClonerSPI { private final ClassLoader targetCL; private final Map<Object, Object> referenceMap = new ListMap<Object, Object>(); private final Class<DoNotClone> doNotClone; /** * Clone using the supplied ClassLoader. */ public DeepCloner(ClassLoader classLoader) { this.targetCL = classLoader; doNotClone = getDoNotClone(targetCL); } /** * Clone using the current ContextClassLoader. */ public DeepCloner() { this(Thread.currentThread().getContextClassLoader()); } private Class<DoNotClone> getDoNotClone(ClassLoader targetCL) { return ClassLoaderUtil.loadClass(DoNotClone.class, targetCL); } /** * Clones an object. * * @return A deep clone of the object to clone. */ @Override public <T> T clone(T objectToClone) { return clone(objectToClone, true); } /** * * @param includeStandardJavaType * <code>true</code> also clones standard java types (using * simple serialization), <code>false</code> simply reference to * these objects (will be same instance). * @return A deep clone of the object to clone. */ public <T> T clone(T objectToClone, boolean includeStandardJavaType) { assertObjectNotNull(objectToClone); return performClone(ClassLoaderUtil.loadClass(getType(objectToClone), targetCL), objectToClone, includeStandardJavaType); } @SuppressWarnings("unchecked") private static <T> Class<T> getType(T object) { if (object == null) { return null; } return (Class<T>) (object instanceof Class ? object : object.getClass()); } private static boolean isClass(Object object) { if (object == null) { return false; } return object instanceof Class<?>; } private static void assertObjectNotNull(Object object) { if (object == null) { throw new IllegalArgumentException("Object to clone cannot be null"); } } @SuppressWarnings("unchecked") private <T> T performClone(Class<T> targetClass, Object source, boolean shouldCloneStandardJavaTypes) { Object target = null; if (targetClass.isArray() && !isClass(source)) { return (T) instantiateArray(targetCL, targetClass, source, shouldCloneStandardJavaTypes); } else if (isJavaReflectMethod(targetClass)) { return (T) cloneJavaReflectMethod(source); } else if (targetClass.isPrimitive() || isSunClass(targetClass) || isJavaReflectClass(targetClass)) { return (T) source; } else if (isSerializableCandidate(targetClass, source)) { return (T) serializationClone(source); } else if (targetClass.isEnum()) { return (T) cloneEnum(targetCL, source); } else if (isClass(source)) { return (T) ClassLoaderUtil.loadClass(getType(source), targetCL); } else { target = isClass(source) ? source : Whitebox.newInstance(targetClass); } if (target != null) { referenceMap.put(source, target); cloneFields(targetCL, targetClass, source, target, referenceMap, shouldCloneStandardJavaTypes); } return (T) target; } private Object cloneJavaReflectMethod(Object source) { Method sourceMethod = (Method) source; Class<?> declaringClass = sourceMethod.getDeclaringClass(); Class<?> targetClassLoadedWithTargetCL = ClassLoaderUtil.loadClass(declaringClass, targetCL); Method targetMethod = null; try { targetMethod = targetClassLoadedWithTargetCL.getDeclaredMethod(sourceMethod.getName(), sourceMethod.getParameterTypes()); } catch (Exception e) { SafeExceptionRethrower.safeRethrow(e); } if (sourceMethod.isAccessible()) { targetMethod.setAccessible(true); } return targetMethod; } private boolean isJavaReflectMethod(Class<?> cls) { return cls.getName().equals(Method.class.getName()); } private boolean isSunClass(Class<?> cls) { return cls.getName().startsWith("sun."); } private boolean isJavaReflectClass(Class<?> cls) { return cls.getName().startsWith("java.lang.reflect"); } private <T> boolean isSerializableCandidate(Class<T> targetClass, Object source) { return isStandardJavaType(targetClass) && (isSerializable(targetClass) || isImpliticlySerializable(targetClass)) && !Map.class.isAssignableFrom(source.getClass()) && !Iterable.class.isAssignableFrom(source.getClass()); } private static boolean isImpliticlySerializable(Class<?> cls) { return cls.isPrimitive(); } private static boolean isSerializable(Class<?> cls) { return Serializable.class.isAssignableFrom(cls); } /* * Perform simple serialization */ private Object serializationClone(Object source) { ObjectOutputStream oos = null; ObjectInputStream ois = null; try { ByteArrayOutputStream bos = new ByteArrayOutputStream(); oos = new ObjectOutputStream(bos); oos.writeObject(source); oos.flush(); ByteArrayInputStream bin = new ByteArrayInputStream(bos.toByteArray()); ois = new ObjectInputStream(bin); return ois.readObject(); } catch (Exception e) { throw new RuntimeException(e); } finally { close(oos); close(ois); } } private void close(Closeable closeable) { try { if (closeable != null) { closeable.close(); } } catch (IOException e) { } } @SuppressWarnings({ "unchecked", "rawtypes" }) private Object cloneEnum(ClassLoader targetCL, Object source) { Object target; final Class enumClassLoadedByTargetCL = ClassLoaderUtil.loadClass(getType(source), targetCL); target = getEnumValue(source, enumClassLoadedByTargetCL); return target; } @SuppressWarnings("unchecked") private <T> void cloneFields(ClassLoader targetCL, Class<T> targetClass, Object source, Object target, Map<Object, Object> referenceMap, boolean cloneStandardJavaTypes) { Class<?> currentTargetClass = targetClass; while (currentTargetClass != null) { for (Field field : currentTargetClass.getDeclaredFields()) { if (field.getAnnotation(doNotClone) != null) { continue; } field.setAccessible(true); try { final Field declaredField = Whitebox.getField(getType(source), field.getName()); declaredField.setAccessible(true); final Object object = declaredField.get(source); final Object instantiatedValue; if (object == source) { instantiatedValue = target; } else if (referenceMap.containsKey(object)) { instantiatedValue = referenceMap.get(object); } else { if (object == null && !isIterable(object)) { instantiatedValue = object; } else { Class<Object> type = getType(object); if (type.getName().equals("void")) { type = Class.class.cast(Class.class); } final Class<Object> typeLoadedByCL = ClassLoaderUtil.loadClass(type, targetCL ); if (type.isEnum()) { instantiatedValue = getEnumValue(object, typeLoadedByCL); } else { instantiatedValue = performClone(typeLoadedByCL, object, cloneStandardJavaTypes); } } } final boolean needsUnsafeWrite = field.isEnumConstant() || isStaticFinalModifier(field); if (needsUnsafeWrite) { UnsafeFieldWriter.write(field, target, instantiatedValue); } else { field.set(target, instantiatedValue); } } catch (RuntimeException e) { throw e; } catch (Exception e) { throw new RuntimeException(e); } } currentTargetClass = currentTargetClass.getSuperclass(); } } private static <T> boolean isStandardJavaType(Class<T> targetClass) { return targetClass.getName().startsWith("java."); } private static boolean isStaticFinalModifier(final Field field) { final int modifiers = field.getModifiers(); return Modifier.isFinal(modifiers) && Modifier.isStatic(modifiers) || field.getDeclaringClass().equals(Character.class) && field.getName().equals("MIN_RADIX"); } private static boolean isIterable(final Object object) { return object != null && isIterable(object.getClass()); } private static boolean isIterable(final Class<?> cls) { return Iterable.class.isAssignableFrom(cls); } @SuppressWarnings({ "unchecked", "rawtypes" }) private static Enum getEnumValue(final Object enumValueOfSourceClassloader, final Class<Object> enumTypeLoadedByTargetCL) { return Enum.valueOf((Class) enumTypeLoadedByTargetCL, enumValueOfSourceClassloader.toString()); } private Object instantiateArray(ClassLoader targetCL, Class<?> arrayClass, Object objectToClone, boolean cloneStandardJavaTypes) { final int arrayLength = Array.getLength(objectToClone); final Object array = Array.newInstance(arrayClass.getComponentType(), arrayLength); for (int i = 0; i < arrayLength; i++) { final Object object = Array.get(objectToClone, i); final Object performClone; if (object == null) { performClone = null; } else { performClone = performClone(ClassLoaderUtil.loadClass(getType(object), targetCL), object, cloneStandardJavaTypes); } Array.set(array, i, performClone); } return array; } /** * Most of this code has been copied from the Sun14ReflectionProvider in the * XStream project. Some changes has been made, namely if the field is * static final then the {@link Unsafe#staticFieldOffset(Field)} method is * used instead of {@link Unsafe#objectFieldOffset(Field)}. * * @author Joe Walnes * @author Brian Slesinsky * @author Johan Haleby */ private static class UnsafeFieldWriter { private final static Unsafe unsafe; private final static Exception exception; static { Unsafe u = null; Exception ex = null; try { Class<?> objectStreamClass = Class.forName("sun.misc.Unsafe"); Field unsafeField = objectStreamClass.getDeclaredField("theUnsafe"); unsafeField.setAccessible(true); u = (Unsafe) unsafeField.get(null); } catch (Exception e) { ex = e; } exception = ex; unsafe = u; } public static void write(Field field, Object object, Object value) { if (exception != null) { throw new RuntimeException("Could not set field " + object.getClass() + "." + field.getName(), exception); } try { final long offset; if (DeepCloner.isStaticFinalModifier(field)) { offset = unsafe.staticFieldOffset(field); } else { offset = unsafe.objectFieldOffset(field); } Class<?> type = field.getType(); if (type.isPrimitive()) { if (type.equals(Integer.TYPE)) { unsafe.putInt(object, offset, ((Integer) value).intValue()); } else if (type.equals(Long.TYPE)) { unsafe.putLong(object, offset, ((Long) value).longValue()); } else if (type.equals(Short.TYPE)) { unsafe.putShort(object, offset, ((Short) value).shortValue()); } else if (type.equals(Character.TYPE)) { unsafe.putChar(object, offset, ((Character) value).charValue()); } else if (type.equals(Byte.TYPE)) { unsafe.putByte(object, offset, ((Byte) value).byteValue()); } else if (type.equals(Float.TYPE)) { unsafe.putFloat(object, offset, ((Float) value).floatValue()); } else if (type.equals(Double.TYPE)) { unsafe.putDouble(object, offset, ((Double) value).doubleValue()); } else if (type.equals(Boolean.TYPE)) { unsafe.putBoolean(object, offset, ((Boolean) value).booleanValue()); } else { throw new RuntimeException("Could not set field " + object.getClass() + "." + field.getName() + ": Unknown type " + type); } } else { unsafe.putObject(object, offset, value); } } catch (IllegalArgumentException e) { throw new RuntimeException("Could not set field " + object.getClass() + "." + field.getName(), e); } } } }