/*
* This file is part of LibrePlan
*
* Copyright (C) 2009-2010 Fundación para o Fomento da Calidade Industrial e
* Desenvolvemento Tecnolóxico de Galicia
* Copyright (C) 2010-2011 Igalia, S.L.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.libreplan.business.util.deepcopy;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import org.apache.commons.lang3.Validate;
import org.hibernate.proxy.HibernateProxy;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.libreplan.business.workingday.EffortDuration;
/**
* @author Óscar González Fernández <ogonzalez@igalia.com>
*/
public class DeepCopy {
private static Set<Class<?>> inmmutableTypes = new HashSet<>(Arrays.<Class<?>>asList(
Boolean.class, String.class, BigDecimal.class,
Double.class, Float.class, Integer.class,
Short.class, Byte.class, Character.class,
LocalDate.class, DateTime.class, EffortDuration.class));
private static List<ICustomCopy> DEFAULT_CUSTOM_COPIERS =
Arrays.asList(new DateCopy(), new SetCopy(), new MapCopy(), new ListCopy());
private Map<ByIdentity, Object> alreadyCopiedObjects = new HashMap<>();
public static boolean isImmutableType(Class<?> klass) {
return klass.isPrimitive() || isEnum(klass) || inmmutableTypes.contains(klass);
}
private static boolean isEnum(Class<?> klass) {
Class<?> currentClass = klass;
do {
if (currentClass.isEnum()) {
return true;
}
currentClass = currentClass.getSuperclass();
} while (currentClass != null);
return false;
}
public interface ICustomCopy {
boolean canHandle(Object object);
Object instantiateCopy(Strategy strategy, Object originValue);
void copyDataToResult(DeepCopy deepCopy, Object origin, Strategy strategy, Object result);
}
private static class DateCopy implements ICustomCopy {
@Override
public boolean canHandle(Object object) {
return object instanceof Date;
}
@Override
public void copyDataToResult(DeepCopy deepCopy, Object origin, Strategy strategy, Object result) {
// Already completed
}
@Override
public Object instantiateCopy(Strategy strategy, Object originValue) {
Date date = (Date) originValue;
return new Date(date.getTime());
}
}
private abstract static class CollectionCopy implements ICustomCopy {
@Override
public Object instantiateCopy(Strategy strategy, Object originValue) {
return getResultData(originValue);
}
protected abstract Collection<Object> getResultData(Object originValue);
@Override
public void copyDataToResult(DeepCopy deepCopy, Object origin, Strategy strategy, Object result) {
copy(deepCopy, origin, strategy, (Collection<Object>) result);
}
private void copy(DeepCopy deepCopy, Object origin, Strategy strategy, Collection<Object> destination) {
Strategy childrenStrategy = getChildrenStrategy(strategy);
for (Object each : originDataAsIterable(origin)) {
destination.add(deepCopy.copy(each, childrenStrategy));
}
}
private Strategy getChildrenStrategy(Strategy strategy) {
return strategy == Strategy.SHARE_COLLECTION_ELEMENTS ? Strategy.SHARE : strategy;
}
private Iterable<Object> originDataAsIterable(Object originValue) {
return (Iterable<Object>) originValue;
}
}
private static class SetCopy extends CollectionCopy {
@Override
public boolean canHandle(Object object) {
return object instanceof Set;
}
@Override
protected Collection<Object> getResultData(Object object) {
return instantiate(object.getClass());
}
private Set<Object> instantiate(final Class<? extends Object> klass) {
return new ImplementationInstantiation() {
@Override
protected Set<?> createDefault() {
return SortedSet.class.isAssignableFrom(klass) ? new TreeSet<>() : new HashSet<>();
}
}.instantiate(klass);
}
}
private static class MapCopy implements ICustomCopy {
@Override
public boolean canHandle(Object object) {
return object instanceof Map;
}
@Override
public Object instantiateCopy(Strategy strategy, Object originValue) {
return instantiate(originValue.getClass());
}
private Map<Object, Object> instantiate(Class<? extends Object> klass) {
return new ImplementationInstantiation() {
@Override
protected Object createDefault() {
return new HashMap<>();
}
}.instantiate(klass);
}
@Override
public void copyDataToResult(DeepCopy deepCopy, Object origin, Strategy strategy, Object result) {
doCopy(deepCopy, (Map<?, ?>) origin, strategy, ((Map<Object, Object>) result));
}
private void doCopy(DeepCopy deepCopy, Map<?, ?> origin, Strategy strategy, Map<Object, Object> resultMap) {
Strategy keyStrategy = getKeysStrategy(strategy);
Strategy valueStrategy = getValuesStrategy(strategy);
for (Entry<?, ?> entry : origin.entrySet()) {
Object key = deepCopy.copy(entry.getKey(), keyStrategy);
Object value = deepCopy.copy(entry.getValue(), valueStrategy);
resultMap.put(key, value);
}
}
private Strategy getKeysStrategy(Strategy strategy) {
return Strategy.ONLY_SHARE_KEYS == strategy || Strategy.SHARE_COLLECTION_ELEMENTS == strategy
? Strategy.SHARE
: strategy;
}
private Strategy getValuesStrategy(Strategy strategy) {
return Strategy.ONLY_SHARE_VALUES == strategy || Strategy.SHARE_COLLECTION_ELEMENTS == strategy
? Strategy.SHARE
: strategy;
}
}
private static class ListCopy extends CollectionCopy {
@Override
public boolean canHandle(Object object) {
return object instanceof List;
}
@Override
protected Collection<Object> getResultData(Object originValue) {
return instantiate(originValue.getClass());
}
private List<Object> instantiate(Class<? extends Object> klass) {
return new ImplementationInstantiation() {
@Override
protected Object createDefault() {
return new ArrayList<>();
}
}.instantiate(klass);
}
}
private static abstract class ImplementationInstantiation {
private static final String[] VETOED_IMPLEMENTATIONS = {
"PersistentSet", "PersistentList", "PersistentMap", "PersistentSortedSet"
};
ImplementationInstantiation() {
}
<T> T instantiate(Class<?> type) {
if (!isVetoed(type)) {
try {
Constructor<? extends Object> constructor = type.getConstructor();
return (T) type.cast(constructor.newInstance());
} catch (Exception ignored) {
}
}
return (T) createDefault();
}
private static boolean isVetoed(Class<?> type) {
final String simpleName = type.getSimpleName();
for (String each : VETOED_IMPLEMENTATIONS) {
if (each.equalsIgnoreCase(simpleName)) {
return true;
}
}
return false;
}
protected abstract Object createDefault();
}
private static class ByIdentity {
private final Object wrapped;
ByIdentity(Object wrapped) {
Validate.notNull(wrapped);
this.wrapped = wrapped;
}
@Override
public int hashCode() {
return wrapped.hashCode();
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj instanceof ByIdentity) {
ByIdentity other = (ByIdentity) obj;
return this.wrapped == other.wrapped;
}
return false;
}
}
private ByIdentity byIdentity(Object value) {
return new ByIdentity(value);
}
public <T> T copy(T entity) {
return copy(entity, null);
}
private <T> T copy(T couldBeProxyValue, Strategy strategy) {
if (couldBeProxyValue == null) {
return null;
}
T value = desproxify(couldBeProxyValue);
if (alreadyCopiedObjects.containsKey(byIdentity(value))) {
return (T) alreadyCopiedObjects.get(byIdentity(value));
}
if (Strategy.SHARE == strategy || isImmutable(value)) {
return value;
}
ICustomCopy copier = findCopier(value);
if (copier != null) {
Object resultData = copier.instantiateCopy(strategy, value);
alreadyCopiedObjects.put(byIdentity(value), resultData);
copier.copyDataToResult(this, value, strategy, resultData);
return (T) resultData;
}
T result = instantiateUsingDefaultConstructor(getTypedClassFrom(value));
alreadyCopiedObjects.put(byIdentity(value), result);
copyProperties(value, result);
callAfterCopyHooks(result);
return result;
}
private <T> T desproxify(T value) {
if (value instanceof HibernateProxy) {
HibernateProxy proxy = (HibernateProxy) value;
return (T) proxy.getHibernateLazyInitializer().getImplementation();
}
return value;
}
private boolean isImmutable(Object value) {
return isImmutableType(value.getClass());
}
@SuppressWarnings("unchecked")
private <T> Class<T> getTypedClassFrom(T entity) {
return (Class<T>) entity.getClass();
}
private <T> T instantiateUsingDefaultConstructor(Class<T> klass) {
Constructor<T> constructor;
try {
constructor = klass.getConstructor();
} catch (SecurityException e) {
throw new RuntimeException(e);
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException("could not invoke default no-args constructor for " + klass, e);
}
try {
return constructor.newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private void copyProperties(Object source, Object target) {
List<Field> fields = getAllFieldsFor(source);
for (Field each : fields) {
each.setAccessible(true);
if (!isIgnored(each)) {
Object sourceValue = readFieldValue(source, each);
if (sourceValue != null) {
Strategy strategy = getStrategy(each);
try {
writeFieldValue(target, each, copy(sourceValue, strategy));
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
}
}
}
private List<Field> getAllFieldsFor(Object source) {
List<Field> result = new ArrayList<>();
Class<? extends Object> currentClass = source.getClass();
while (currentClass != null) {
result.addAll(Arrays.asList(currentClass.getDeclaredFields()));
currentClass = currentClass.getSuperclass();
}
return result;
}
private boolean isIgnored(Field field) {
return isStatic(field) || isMarkedWithIgnore(field);
}
private boolean isStatic(Field field) {
return Modifier.isStatic(field.getModifiers());
}
private boolean isMarkedWithIgnore(Field each) {
OnCopy onCopy = each.getAnnotation(OnCopy.class);
return onCopy != null && onCopy.value() == Strategy.IGNORE;
}
private void writeFieldValue(Object target, Field field, Object value) {
try {
field.set(target, value);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private Object readFieldValue(Object source, Field field) {
try {
return field.get(source);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private Strategy getStrategy(Field field) {
OnCopy onCopy = field.getAnnotation(OnCopy.class);
return onCopy != null ? onCopy.value() : null;
}
private ICustomCopy findCopier(Object sourceValue) {
for (ICustomCopy each : DEFAULT_CUSTOM_COPIERS) {
if (each.canHandle(sourceValue)) {
return each;
}
}
return null;
}
private void callAfterCopyHooks(Object value) {
assert value != null;
for (Method each : getAfterCopyHooks(value.getClass())) {
each.setAccessible(true);
try {
each.invoke(value);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
private List<Method> getAfterCopyHooks(Class<?> klass) {
Class<?> current = klass;
List<Method> result = new ArrayList<>();
while (current != null) {
result.addAll(getAfterCopyDeclaredAt(current));
current = current.getSuperclass();
}
return result;
}
private List<Method> getAfterCopyDeclaredAt(Class<?> klass) {
List<Method> result = new ArrayList<>();
for (Method each : klass.getDeclaredMethods()) {
if (isAfterCopyHook(each)) {
result.add(each);
}
}
return result;
}
private boolean isAfterCopyHook(Method each) {
AfterCopy annotation = each.getAnnotation(AfterCopy.class);
return annotation != null;
}
public <T> DeepCopy replace(T toBeReplaced, T substitution) {
alreadyCopiedObjects.put(byIdentity(toBeReplaced), substitution);
return this;
}
}