/*
* Copyright 2013 Chris Pheby
*
* 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.jadira.reflection.cloning.implementor;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
import java.util.IdentityHashMap;
import org.jadira.reflection.access.model.ClassModel;
import org.jadira.reflection.access.model.FieldModel;
import org.jadira.reflection.access.model.FieldType;
import org.jadira.reflection.cloning.MinimalCloner;
import org.jadira.reflection.cloning.api.CloneDriver;
import org.jadira.reflection.cloning.api.CloneImplementor;
import org.jadira.reflection.cloning.api.CloneStrategy;
import org.jadira.reflection.cloning.api.NoCloneImplementor;
import org.jadira.reflection.core.misc.ClassUtils;
/**
* A Base {@link CloneStrategy} implementation providing functionality which is
* common across class and field access mechanisms.
*/
public abstract class AbstractCloneStrategy implements CloneStrategy {
private static final int REFERENCE_STACK_LIMIT = 150;
@Override
public abstract <T> T newInstance(Class<T> c);
@Override
public boolean canClone(Class<?> clazz) {
return true;
}
@Override
public <T> T clone(T obj, CloneDriver context, IdentityHashMap<Object, Object> referencesToReuse, long stackDepth) {
/**
* To avoid unnecessary recursion and potential stackoverflow errors, we use an internal
* stack
*/
stackDepth++;
final Deque<WorkItem> stack;
if (REFERENCE_STACK_LIMIT <= stackDepth) {
stack = new ArrayDeque<WorkItem>();
} else {
stack = null;
}
Object objectInput;
// T parentOutput = null;
WorkItem nextWork = null;
while (true) {
Object objectResult;
if (nextWork == null) {
objectInput = obj;
} else {
objectInput = getFieldValue(nextWork.getSource(), nextWork.getFieldModel());
}
if (objectInput == null) {
objectResult = null;
}
else if (context.isImmutableInstance(objectInput)) {
objectResult = objectInput;
}
else {
objectResult = doCloneStep(objectInput, context, referencesToReuse, stack, stackDepth);
}
if (nextWork == null) {
nextWork = (stack == null ? null : stack.pollFirst());
if (nextWork == null) {
@SuppressWarnings("unchecked")
final T convertedResult = (T) objectResult;
return convertedResult;
}
} else {
putFieldValue(nextWork.getTarget(), nextWork.getFieldModel(), objectResult);
nextWork = (stack == null ? null : stack.pollFirst());
}
}
}
private Object doCloneStep(Object objectInput, CloneDriver context, IdentityHashMap<Object, Object> referencesToReuse, final Deque<WorkItem> stack, long stackDepth) {
Object objectResult;
@SuppressWarnings("unchecked")
final Class<Object> clazz = (Class<Object>) objectInput.getClass();
if (clazz.isPrimitive() || clazz.isEnum()) {
objectResult = objectInput;
} else if (clazz.isArray()) {
final Object copy = handleArray(objectInput, context, referencesToReuse, stackDepth);
if (referencesToReuse != null) {
referencesToReuse.put(objectInput, copy);
}
objectResult = copy;
} else if (ClassUtils.isJdkImmutable(clazz) || ClassUtils.isWrapper(clazz) || context.getImmutableClasses().contains(clazz) || context.getNonCloneableClasses().contains(clazz)) {
objectResult = objectInput;
} else {
final ClassModel<Object> model = getClassModel(clazz);
if (model.isFlat()) {
referencesToReuse = null;
}
final Object result = referencesToReuse == null ? null : referencesToReuse.get(objectInput);
if (result != null) {
objectResult = result;
} else {
final CloneImplementor cloneImplementor;
if (context.isUseCloneImplementors()) {
cloneImplementor = context.getImplementor(clazz);
} else {
cloneImplementor = context.getBuiltInImplementor(clazz);
}
if (cloneImplementor != null) {
Object copy = cloneImplementor.clone(objectInput, context, referencesToReuse, stackDepth);
if (referencesToReuse != null) {
referencesToReuse.put(objectInput, copy);
}
objectResult = copy;
} else {
if (model.isDetectedAsImmutable() || model.isNonCloneable()) {
objectResult = objectInput;
} else {
final org.jadira.reflection.cloning.annotation.Cloneable cloneableAnnotation = clazz.getAnnotation(org.jadira.reflection.cloning.annotation.Cloneable.class);
if (cloneableAnnotation != null && !NoCloneImplementor.class.equals(cloneableAnnotation.implementor())) {
final Object copy = handleCloneImplementor(objectInput, context, referencesToReuse, clazz, cloneableAnnotation, stackDepth);
if (referencesToReuse != null) {
referencesToReuse.put(objectInput, copy);
}
objectResult = copy;
}
else if (model.getCloneImplementor() != null) {
final Object copy = model.getCloneImplementor().clone(objectInput, context, referencesToReuse, stackDepth);
if (referencesToReuse != null) {
referencesToReuse.put(objectInput, copy);
}
objectResult = copy;
} else if (context.isUseCloneable() && Cloneable.class.isAssignableFrom(clazz)) {
final Object copy = handleCloneableCloneMethod(objectInput, context, referencesToReuse, clazz, cloneableAnnotation);
if (referencesToReuse != null) {
referencesToReuse.put(objectInput, copy);
}
objectResult = copy;
} else {
objectResult = newInstance(clazz);
if (referencesToReuse != null) {
referencesToReuse.put(objectInput, objectResult);
}
ClassModel<Object> classModelInHierarchy = model;
while (classModelInHierarchy != null) {
for (FieldModel<Object> f : classModelInHierarchy.getModelFields()) {
if (!context.isCloneTransientFields() && f.isTransientField()) {
handleTransientField(objectResult, f);
} else if (!context.isCloneTransientAnnotatedFields() && f.isTransientAnnotatedField()) {
handleTransientField(objectResult, f);
} else {
if (stack == null) {
handleCloneField(objectInput, objectResult, context, f, referencesToReuse, stackDepth);
} else {
if (f.getFieldType() == FieldType.PRIMITIVE) {
handleClonePrimitiveField(objectInput, objectResult, context, f, referencesToReuse);
} else {
if (!context.isCloneSyntheticFields() && f.isSynthetic()) {
Object fieldObject = getFieldValue(objectInput, f);
if (referencesToReuse != null) {
referencesToReuse.put(fieldObject, fieldObject);
}
} else {
stack.addFirst(new WorkItem(objectInput, objectResult, f));
}
}
}
}
}
classModelInHierarchy = classModelInHierarchy.getSuperClassModel();
}
}
}
}
}
}
return objectResult;
}
private <T> T handleCloneImplementor(T obj, CloneDriver context, IdentityHashMap<Object, Object> referencesToReuse, final Class<T> clazz,
org.jadira.reflection.cloning.annotation.Cloneable cloneableAnnotation, long stackDepth) {
CloneImplementor cloneImplementor = context.getAnnotationImplementor(clazz);
if (cloneImplementor == null) {
cloneImplementor = (CloneImplementor) newInstance(cloneableAnnotation.implementor());
context.putAnnotationImplementor(clazz, cloneImplementor);
}
if (MinimalCloner.class.equals(cloneImplementor.getClass())) {
T copy = cloneImplementor.clone(obj, (MinimalCloner) cloneImplementor, referencesToReuse, stackDepth);
referencesToReuse.put(obj, copy);
return copy;
} else {
T copy = cloneImplementor.clone(obj, context, referencesToReuse, stackDepth);
referencesToReuse.put(obj, copy);
return copy;
}
}
private <T> T handleCloneableCloneMethod(T obj, CloneDriver context, IdentityHashMap<Object, Object> referencesToReuse, final Class<T> clazz,
org.jadira.reflection.cloning.annotation.Cloneable cloneableAnnotation) {
MethodHandle handle = context.getCloneMethod(clazz);
if (handle == null) {
try {
Method cloneMethod = clazz.getMethod("clone");
handle = MethodHandles.lookup().unreflect(cloneMethod);
} catch (IllegalAccessException e) {
throw new IllegalStateException("Cannot access clone() method for: " + clazz.getName(), e);
} catch (NoSuchMethodException e) {
throw new IllegalStateException("Cannot find clone() method for: " + clazz.getName(), e);
} catch (SecurityException e) {
throw new IllegalStateException("Cannot invoke clone() method for: " + clazz.getName(), e);
}
context.putCloneMethod(clazz, handle);
}
T copy = performCloneForCloneableMethod(obj, context);
referencesToReuse.put(obj, copy);
return copy;
}
/**
* Helper method for performing cloning for objects of classes implementing java.lang.Cloneable
* @param object The object to be cloned.
* @param context The CloneDriver to be used
* @param <T> The type being copied
* @return The cloned object
*/
protected <T> T performCloneForCloneableMethod(T object, CloneDriver context) {
Class<?> clazz = object.getClass();
final T result;
try {
MethodHandle handle = context.getCloneMethod(clazz);
result = (T) handle.invoke(object);
} catch (Throwable e) {
throw new IllegalStateException("Could not invoke clone() for instance of: " + clazz.getName(), e);
}
return result;
}
/**
* Obtain a ClassModel instance for the given class
* @param clazz Class to model
* @param <W> The type of class
* @return The ClassModel
*/
protected abstract <W> ClassModel<W> getClassModel(Class<W> clazz);
/**
* Clone an array
* @param origFieldValue The original value
* @param context The CloneDriver
* @param visited Used for tracking objects that have already been seen
* @param stackDepth The current depth of the stack - used to switch from recursion to iteration if the stack grows too deep.
* @param <T> The type being copied
* @return A clone of the array
*/
protected <T> T handleArray(T origFieldValue, CloneDriver context, IdentityHashMap<Object, Object> visited, long stackDepth) {
if (visited != null) {
@SuppressWarnings("unchecked")
final T castResult = (T) visited.get(origFieldValue);
if (castResult != null) {
return castResult;
}
}
final Class<?> componentType = origFieldValue.getClass().getComponentType();
Object result = null;
if (componentType.isPrimitive()) {
if (java.lang.Boolean.TYPE == componentType) {
result = Arrays.copyOf((boolean[]) origFieldValue, ((boolean[]) origFieldValue).length);
} else if (java.lang.Byte.TYPE == componentType) {
result = Arrays.copyOf((byte[]) origFieldValue, ((byte[]) origFieldValue).length);
} else if (java.lang.Character.TYPE == componentType) {
result = Arrays.copyOf((char[]) origFieldValue, ((char[]) origFieldValue).length);
} else if (java.lang.Short.TYPE == componentType) {
result = Arrays.copyOf((short[]) origFieldValue, ((short[]) origFieldValue).length);
} else if (java.lang.Integer.TYPE == componentType) {
result = Arrays.copyOf((int[]) origFieldValue, ((int[]) origFieldValue).length);
} else if (java.lang.Long.TYPE == componentType) {
result = Arrays.copyOf((long[]) origFieldValue, ((long[]) origFieldValue).length);
} else if (java.lang.Float.TYPE == componentType) {
result = Arrays.copyOf((float[]) origFieldValue, ((float[]) origFieldValue).length);
} else if (java.lang.Double.TYPE == componentType) {
result = Arrays.copyOf((double[]) origFieldValue, ((double[]) origFieldValue).length);
}
}
if (result == null) {
Object[] array = Arrays.copyOf((Object[]) origFieldValue, ((Object[]) origFieldValue).length);
if (array.length > 0) {
if (componentType.isArray()) {
for (int i = 0; i < array.length; i++) {
stackDepth++;
array[i] = handleArray(array[i], context, visited, stackDepth);
}
} else {
for (int i = 0; i < array.length; i++) {
array[i] = clone(array[i], context, visited, stackDepth);
}
}
}
result = array;
}
if (visited != null) {
visited.put(origFieldValue, result);
}
@SuppressWarnings("unchecked")
final T castResult = (T) result;
return castResult;
}
/**
* Clone a Field
* @param obj The original object
* @param copy The destination object
* @param driver The CloneDriver
* @param f The FieldModel for the target field
* @param referencesToReuse Used for tracking objects that have already been seen
* @param stackDepth The current depth of the stack - used to switch from recursion to iteration if the stack grows too deep.
* @param <T> The type containing the field being cloned
*/
protected <T> void handleCloneField(T obj, T copy, CloneDriver driver, FieldModel<T> f, IdentityHashMap<Object, Object> referencesToReuse, long stackDepth) {
final Class<?> clazz = f.getFieldClass();
if (clazz.isPrimitive()) {
handleClonePrimitiveField(obj, copy, driver, f, referencesToReuse);
} else if (!driver.isCloneSyntheticFields() && f.isSynthetic()) {
putFieldValue(copy, f, getFieldValue(obj, f));
} else {
final Object fieldObject = getFieldValue(obj, f);
final Object fieldObjectClone = clone(fieldObject, driver, referencesToReuse, stackDepth);
putFieldValue(copy, f, fieldObjectClone);
}
}
/**
* Implementations should ensure that transient fields are left with the correct default (unset) value
* @param copy The target object
* @param f The FieldModel for the Field that should stay as a default
* @param <T> The type containing the field
*/
protected abstract <T> void handleTransientField(T copy, FieldModel<T> f);
/**
* Method should clone the given primitive field
* @param obj Source object
* @param copy The target object
* @param driver The CloneDriver to use
* @param f The FieldModel for the Field that should stay as a default
* @param referencesToReuse Used for tracking objects that have already been seen
* @param <T> The type being copied
*/
protected abstract <T> void handleClonePrimitiveField(T obj, T copy, CloneDriver driver, FieldModel<T> f, IdentityHashMap<Object, Object> referencesToReuse);
/**
* Method to retrieve the value of a particular field
* @param obj Source object
* @param f The FieldModel for the Field that should stay as a default
* @param <T> The type containing the field
* @return The value in the given field
*/
protected abstract <T> Object getFieldValue(T obj, FieldModel<T> f);
/**
* Put the given value into the target field
* @param obj Source object
* @param f The FieldModel for the Field that should stay as a default
* @param <T> The type containing the field
* @param value The value to put
*/
protected abstract <T> void putFieldValue(T obj, FieldModel<T> f, Object value);
@Override
public void initialiseFor(Class<?>... classes) {
IdentityHashMap<Class<?>, Boolean> seenClasses = new IdentityHashMap<Class<?>, Boolean>(classes.length * 10);
for (Class<?> clazz : classes) {
doInitialiseFor(clazz, seenClasses);
}
}
private void doInitialiseFor(Class<?> clazz, IdentityHashMap<Class<?>, Boolean> seenClasses) {
getClassModel(clazz);
seenClasses.put(clazz, Boolean.TRUE);
Field[] fields = ClassUtils.collectInstanceFields(clazz);
for (Field f : fields) {
Class<?> type = f.getType();
if (seenClasses.containsKey(type)) {
continue;
}
if (type.isPrimitive()) {
continue;
} else if (type.isArray() && !(type.getComponentType().isPrimitive())) {
doInitialiseFor(type.getComponentType(), seenClasses);
seenClasses.put(type.getComponentType(), Boolean.TRUE);
} else if (!type.isArray() && !type.isPrimitive() && !type.isEnum() && !type.isInterface() && !ClassUtils.isWrapper(type) && !ClassUtils.isJdkImmutable(type)) {
doInitialiseFor(type, seenClasses);
seenClasses.put(type, Boolean.TRUE);
}
}
}
private class WorkItem {
private final Object source;
private final Object target;
private final FieldModel<Object> fieldModel;
public WorkItem(Object source, Object target, FieldModel<Object> fieldModel) {
this.source = source;
this.target = target;
this.fieldModel = fieldModel;
}
public Object getSource() {
return source;
}
public Object getTarget() {
return target;
}
private FieldModel<Object> getFieldModel() {
return fieldModel;
}
}
}