/* * Copyright 2012 Red Hat, Inc. and/or its affiliates. * * 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.optaplanner.core.impl.domain.solution.cloner; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.Deque; import java.util.HashMap; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.Set; import java.util.SortedMap; import java.util.SortedSet; import java.util.TreeMap; import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import org.apache.commons.lang3.tuple.Pair; import org.optaplanner.core.api.domain.solution.PlanningSolution; import org.optaplanner.core.api.domain.solution.cloner.DeepPlanningClone; import org.optaplanner.core.api.domain.solution.cloner.SolutionCloner; import org.optaplanner.core.impl.domain.common.ReflectionHelper; import org.optaplanner.core.impl.domain.common.accessor.MemberAccessor; import org.optaplanner.core.impl.domain.solution.descriptor.SolutionDescriptor; /** * @param <Solution_> the solution type, the class with the {@link PlanningSolution} annotation */ public class FieldAccessingSolutionCloner<Solution_> implements SolutionCloner<Solution_> { protected final SolutionDescriptor<Solution_> solutionDescriptor; protected final ConcurrentMap<Class<?>, Constructor<?>> constructorCache = new ConcurrentHashMap<>(); protected final ConcurrentMap<Class<?>, List<Field>> fieldListCache = new ConcurrentHashMap<>(); protected final ConcurrentMap<Pair<Field, Class<?>>, Boolean> deepCloneDecisionFieldCache = new ConcurrentHashMap<>(); protected final ConcurrentMap<Class<?>, Boolean> deepCloneDecisionActualValueClassCache = new ConcurrentHashMap<>(); public FieldAccessingSolutionCloner(SolutionDescriptor<Solution_> solutionDescriptor) { this.solutionDescriptor = solutionDescriptor; } // ************************************************************************ // Worker methods // ************************************************************************ @Override public Solution_ cloneSolution(Solution_ originalSolution) { return new FieldAccessingSolutionClonerRun().cloneSolution(originalSolution); } /** * This method is thread-safe. * @param clazz never null * @param <C> type * @return never null */ @SuppressWarnings("unchecked") protected <C> Constructor<C> retrieveCachedConstructor(Class<C> clazz) { return (Constructor<C>) constructorCache.computeIfAbsent(clazz, key -> { Constructor<C> constructor; try { constructor = clazz.getDeclaredConstructor(); } catch (ReflectiveOperationException e) { throw new IllegalStateException("The class (" + clazz + ") should have a no-arg constructor to create a planning clone.", e); } constructor.setAccessible(true); return constructor; }); } /** * This method is thread-safe. * @param clazz never null * @param <C> type * @return never null */ protected <C> List<Field> retrieveCachedFields(Class<C> clazz) { return fieldListCache.computeIfAbsent(clazz, key -> { Field[] fields = clazz.getDeclaredFields(); List<Field> fieldList = new ArrayList<>(fields.length); for (Field field : fields) { if (!Modifier.isStatic(field.getModifiers())) { field.setAccessible(true); fieldList.add(field); } } return fieldList; }); } /** * This method is thread-safe. * @param field never null * @param fieldInstanceClass never null * @param actualValueClass never null * @return never null */ protected boolean retrieveDeepCloneDecision(Field field, Class<?> fieldInstanceClass, Class<?> actualValueClass) { Pair<Field, Class<?>> pair = Pair.of(field, fieldInstanceClass); Boolean deepCloneDecision = deepCloneDecisionFieldCache.computeIfAbsent(pair, key -> isFieldDeepCloned(field, fieldInstanceClass)); return deepCloneDecision || retrieveDeepCloneDecisionForActualValueClass(actualValueClass); } private boolean isFieldDeepCloned(Field field, Class<?> fieldInstanceClass) { return isFieldAnEntityPropertyOnSolution(field, fieldInstanceClass) || isFieldAnEntityOrSolution(field, fieldInstanceClass) || isFieldADeepCloneProperty(field, fieldInstanceClass); } protected boolean isFieldAnEntityPropertyOnSolution(Field field, Class<?> fieldInstanceClass) { // field.getDeclaringClass() is a superclass of or equal to the fieldInstanceClass if (solutionDescriptor.getSolutionClass().isAssignableFrom(fieldInstanceClass)) { String fieldName = field.getName(); // This assumes we're dealing with a simple getter/setter. // If that assumption is false, validateCloneSolution(...) fails-fast. if (solutionDescriptor.getEntityMemberAccessorMap().get(fieldName) != null) { return true; } // This assumes we're dealing with a simple getter/setter. // If that assumption is false, validateCloneSolution(...) fails-fast. if (solutionDescriptor.getEntityCollectionMemberAccessorMap().get(fieldName) != null) { return true; } } return false; } protected boolean isFieldAnEntityOrSolution(Field field, Class<?> fieldInstanceClass) { Class<?> type = field.getType(); if (isClassDeepCloned(type)) { return true; } if (Collection.class.isAssignableFrom(type) || Map.class.isAssignableFrom(type)) { if (isTypeArgumentDeepCloned(field.getGenericType())) { return true; } } return false; } private boolean isTypeArgumentDeepCloned(Type genericType) { // Check the generic type arguments of the field. // Yes, it is possible for fields and methods, but not instances! if (genericType instanceof ParameterizedType) { ParameterizedType parameterizedType = (ParameterizedType) genericType; for (Type actualTypeArgument : parameterizedType.getActualTypeArguments()) { if (actualTypeArgument instanceof Class && isClassDeepCloned((Class) actualTypeArgument)) { return true; } if (isTypeArgumentDeepCloned(actualTypeArgument)) { return true; } } } return false; } private boolean isFieldADeepCloneProperty(Field field, Class<?> fieldInstanceClass) { if (field.isAnnotationPresent(DeepPlanningClone.class)) { return true; } Method getterMethod = ReflectionHelper.getGetterMethod(fieldInstanceClass, field.getName()); if (getterMethod != null && getterMethod.isAnnotationPresent(DeepPlanningClone.class)) { return true; } return false; } /** * This method is thread-safe. * @param actualValueClass never null * @return never null */ protected boolean retrieveDeepCloneDecisionForActualValueClass(Class<?> actualValueClass) { return deepCloneDecisionActualValueClassCache.computeIfAbsent(actualValueClass, key -> isClassDeepCloned(actualValueClass)); } protected boolean isClassDeepCloned(Class<?> type) { return solutionDescriptor.hasEntityDescriptor(type) || solutionDescriptor.getSolutionClass().isAssignableFrom(type) || type.isAnnotationPresent(DeepPlanningClone.class); } protected class FieldAccessingSolutionClonerRun { protected Map<Object, Object> originalToCloneMap; protected Queue<Unprocessed> unprocessedQueue; protected Solution_ cloneSolution(Solution_ originalSolution) { int entityCount = solutionDescriptor.getEntityCount(originalSolution); unprocessedQueue = new ArrayDeque<>(entityCount + 1); originalToCloneMap = new IdentityHashMap<>( entityCount + 1); Solution_ cloneSolution = clone(originalSolution); processQueue(); validateCloneSolution(originalSolution, cloneSolution); return cloneSolution; } protected <C> C clone(C original) { if (original == null) { return null; } C existingClone = (C) originalToCloneMap.get(original); if (existingClone != null) { return existingClone; } Class<C> instanceClass = (Class<C>) original.getClass(); C clone = constructClone(instanceClass); originalToCloneMap.put(original, clone); copyFields(instanceClass, instanceClass, original, clone); return clone; } protected <C> C constructClone(Class<C> clazz) { try { Constructor<C> constructor = retrieveCachedConstructor(clazz); return constructor.newInstance(); } catch (ReflectiveOperationException e) { throw new IllegalStateException("The class (" + clazz + ") should have a no-arg constructor to create a planning clone.", e); } } protected <C> void copyFields(Class<C> clazz, Class<? extends C> instanceClass, C original, C clone) { for (Field field : retrieveCachedFields(clazz)) { Object originalValue = getFieldValue(original, field); if (isDeepCloneField(field, instanceClass, originalValue)) { // Postpone filling in the fields unprocessedQueue.add(new Unprocessed(clone, field, originalValue)); } else { // Shallow copy setFieldValue(clone, field, originalValue); } } Class<? super C> superclass = clazz.getSuperclass(); if (superclass != null) { copyFields(superclass, instanceClass, original, clone); } } protected boolean isDeepCloneField(Field field, Class<?> fieldInstanceClass, Object originalValue) { if (originalValue == null) { return false; } return retrieveDeepCloneDecision(field, fieldInstanceClass, originalValue.getClass()); } protected void processQueue() { while (!unprocessedQueue.isEmpty()) { Unprocessed unprocessed = unprocessedQueue.remove(); process(unprocessed); } } protected void process(Unprocessed unprocessed) { Object cloneValue; if (unprocessed.originalValue instanceof Collection) { cloneValue = cloneCollection(unprocessed.field.getType(), (Collection<?>) unprocessed.originalValue); } else if (unprocessed.originalValue instanceof Map) { cloneValue = cloneMap(unprocessed.field.getType(), (Map<?, ?>) unprocessed.originalValue); } else { cloneValue = clone(unprocessed.originalValue); } setFieldValue(unprocessed.bean, unprocessed.field, cloneValue); } protected <E> Collection<E> cloneCollection(Class<?> expectedType, Collection<E> originalCollection) { Collection<E> cloneCollection = constructCloneCollection(originalCollection); if (!expectedType.isInstance(cloneCollection)) { throw new IllegalStateException("The cloneCollectionClass (" + cloneCollection.getClass() + ") created for originalCollectionClass (" + originalCollection.getClass() + ") is not assignable to the field's type (" + expectedType + ").\n" + "Maybe consider replacing the default " + SolutionCloner.class.getSimpleName() + "."); } for (E originalElement : originalCollection) { E cloneElement = cloneCollectionsElementIfNeeded(originalElement); cloneCollection.add(cloneElement); } return cloneCollection; } protected <E> Collection<E> constructCloneCollection(Collection<E> originalCollection) { // TODO Don't hardcode all standard collections if (originalCollection instanceof List) { if (originalCollection instanceof ArrayList) { return new ArrayList<>(originalCollection.size()); } else if (originalCollection instanceof LinkedList) { return new LinkedList<>(); } else { // Default List return new ArrayList<>(originalCollection.size()); } } else if (originalCollection instanceof Set) { if (originalCollection instanceof SortedSet) { Comparator<E> setComparator = ((SortedSet) originalCollection).comparator(); return new TreeSet<>(setComparator); } else if (originalCollection instanceof LinkedHashSet) { return new LinkedHashSet<>(originalCollection.size()); } else if (originalCollection instanceof HashSet) { return new HashSet<>(originalCollection.size()); } else { // Default Set // Default to a LinkedHashSet to respect order return new LinkedHashSet<>(originalCollection.size()); } } else if (originalCollection instanceof Deque) { return new ArrayDeque<>(originalCollection.size()); } else { // Default collection return new ArrayList<>(originalCollection.size()); } } protected <K, V> Map<K, V> cloneMap(Class<?> expectedType, Map<K, V> originalMap) { Map<K, V> cloneMap = constructCloneMap(originalMap); if (!expectedType.isInstance(cloneMap)) { throw new IllegalStateException("The cloneMapClass (" + cloneMap.getClass() + ") created for originalMapClass (" + originalMap.getClass() + ") is not assignable to the field's type (" + expectedType + ").\n" + "Maybe consider replacing the default " + SolutionCloner.class.getSimpleName() + "."); } for (Map.Entry<K, V> originalEntry : originalMap.entrySet()) { K cloneKey = cloneCollectionsElementIfNeeded(originalEntry.getKey()); V cloneValue = cloneCollectionsElementIfNeeded(originalEntry.getValue()); cloneMap.put(cloneKey, cloneValue); } return cloneMap; } protected <K, V> Map<K, V> constructCloneMap(Map<K, V> originalMap) { // Normally a Map will never be selected for cloning, but extending implementations might anyway if (originalMap instanceof SortedMap) { Comparator<K> setComparator = ((SortedMap) originalMap).comparator(); return new TreeMap<>(setComparator); } else if (originalMap instanceof LinkedHashMap) { return new LinkedHashMap<>(originalMap.size()); } else if (originalMap instanceof HashMap) { return new HashMap<>(originalMap.size()); } else { // Default Map // Default to a LinkedHashMap to respect order return new LinkedHashMap<>(originalMap.size()); } } protected <C> C cloneCollectionsElementIfNeeded(C original) { // Because an element which is itself a Collection or Map might hold an entity, we clone it too // Also, the List<Long> in Map<String, List<Long>> needs to be cloned // if the List<Long> is a shadow, despite that Long never needs to be cloned (because it's immutable). if (original instanceof Collection) { return (C) cloneCollection(Collection.class, (Collection) original); } else if (original instanceof Map) { return (C) cloneMap(Map.class, (Map) original); } if (retrieveDeepCloneDecisionForActualValueClass(original.getClass())) { return clone(original); } else { return original; } } /** * Fails fast if {@link #isFieldAnEntityPropertyOnSolution} assumptions were wrong. * @param originalSolution never null * @param cloneSolution never null */ protected void validateCloneSolution(Solution_ originalSolution, Solution_ cloneSolution) { for (MemberAccessor memberAccessor : solutionDescriptor.getEntityMemberAccessorMap().values()) { Object originalProperty = memberAccessor.executeGetter(originalSolution); if (originalProperty != null) { Object cloneProperty = memberAccessor.executeGetter(cloneSolution); if (originalProperty == cloneProperty) { throw new IllegalStateException( "The solutionProperty (" + memberAccessor.getName() + ") was not cloned as expected." + " The " + FieldAccessingSolutionCloner.class.getSimpleName() + " failed to recognize" + " that property's field, probably because its field name is different."); } } } for (MemberAccessor memberAccessor : solutionDescriptor.getEntityCollectionMemberAccessorMap().values()) { Object originalProperty = memberAccessor.executeGetter(originalSolution); if (originalProperty != null) { Object cloneProperty = memberAccessor.executeGetter(cloneSolution); if (originalProperty == cloneProperty) { throw new IllegalStateException( "The solutionProperty (" + memberAccessor.getName() + ") was not cloned as expected." + " The " + FieldAccessingSolutionCloner.class.getSimpleName() + " failed to recognize" + " that property's field, probably because its field name is different."); } } } } protected Object getFieldValue(Object bean, Field field) { try { return field.get(bean); } catch (IllegalAccessException e) { throw new IllegalStateException("The class (" + bean.getClass() + ") has a field (" + field + ") which can not be read to create a planning clone.", e); } } protected void setFieldValue(Object bean, Field field, Object value) { try { field.set(bean, value); } catch (IllegalAccessException e) { throw new IllegalStateException("The class (" + bean.getClass() + ") has a field (" + field + ") which can not be written with the value (" + value + ") to create a planning clone.", e); } } } protected static class Unprocessed { protected Object bean; protected Field field; protected Object originalValue; public Unprocessed(Object bean, Field field, Object originalValue) { this.bean = bean; this.field = field; this.originalValue = originalValue; } } }