Skip to content

Commit

Permalink
chore: Add internal interface for planning cloning atypical types (Ti…
Browse files Browse the repository at this point in the history
…mefoldAI#727)

Sometimes, the domain model has atypical types for fields (for instance,
UserList instead of List or ArrayList). The default planning cloner
originally failed fast when the field cannot be assigned a typical
value. Now, it checks if the collection implements the internal
interface. If it does, it uses it to create an empty collection/map.
This allows the default planning cloner to be used when the user has
control over UserList.
  • Loading branch information
Christopher-Chianelli authored Mar 20, 2024
1 parent 2773757 commit 019a13e
Show file tree
Hide file tree
Showing 9 changed files with 288 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,14 @@ private <C> C clone(C original, Map<Object, Object> originalToCloneMap, Queue<Un
if (existingClone != null) {
return existingClone;
}

Class<C> declaringClass = (Class<C>) original.getClass();
C clone = constructClone(declaringClass);
C clone;
if (original instanceof PlanningCloneable<?> planningCloneable) {
clone = (C) planningCloneable.createNewInstance();
} else {
clone = constructClone(declaringClass);
}
originalToCloneMap.put(original, clone);
copyFields(declaringClass, original, clone, unprocessedQueue, declaringClassMetadata);
return clone;
Expand Down Expand Up @@ -189,8 +195,12 @@ private <E> Collection<E> cloneCollection(Class<?> expectedType, Collection<E> o
return cloneCollection;
}

@SuppressWarnings("unchecked")
private static <E> Collection<E> constructCloneCollection(Collection<E> originalCollection) {
// TODO Don't hardcode all standard collections
if (originalCollection instanceof PlanningCloneable<?> planningCloneable) {
return (Collection<E>) planningCloneable.createNewInstance();
}
if (originalCollection instanceof LinkedList) {
return new LinkedList<>();
}
Expand Down Expand Up @@ -228,8 +238,12 @@ private <K, V> Map<K, V> cloneMap(Class<?> expectedType, Map<K, V> originalMap,
return cloneMap;
}

@SuppressWarnings("unchecked")
private static <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 PlanningCloneable<?> planningCloneable) {
return (Map<K, V>) planningCloneable.createNewInstance();
}
if (originalMap instanceof SortedMap<K, V> map) {
var setComparator = map.comparator();
return new TreeMap<>(setComparator);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package ai.timefold.solver.core.impl.domain.solution.cloner;

/**
* An internal interface used to construct new instances of an object
* when there is no suitable constructor.
* Used during planning cloning to create an "empty"/"blank" instance
* whose fields/items will be set to the planning clone of the original's
* fields/items.
*
* @param <T> The type of object being cloned.
*/
public interface PlanningCloneable<T> {
/**
* Creates a new "empty"/"blank" instance.
* If the {@link PlanningCloneable} is a {@link java.util.Collection}
* or {@link java.util.Map}, the returned instance should be
* empty and modifiable.
*
* @return never null, a new instance with the same type as the object being cloned.
*/
T createNewInstance();
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import java.util.stream.Stream;

import ai.timefold.solver.core.impl.domain.solution.cloner.DeepCloningUtils;
import ai.timefold.solver.core.impl.domain.solution.cloner.PlanningCloneable;
import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor;

public final class GizmoCloningUtils {
Expand All @@ -27,7 +28,8 @@ public static Set<Class<?>> getDeepClonedClasses(SolutionDescriptor<?> solutionD
deepClonedClassSet.add(clazz);
for (Field field : getAllFields(clazz)) {
deepClonedClassSet.addAll(getDeepClonedTypeArguments(solutionDescriptor, field.getGenericType()));
if (DeepCloningUtils.isFieldDeepCloned(solutionDescriptor, field, clazz)) {
if (DeepCloningUtils.isFieldDeepCloned(solutionDescriptor, field, clazz)
&& !PlanningCloneable.class.isAssignableFrom(field.getType())) {
deepClonedClassSet.add(field.getType());
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import ai.timefold.solver.core.impl.domain.common.accessor.gizmo.GizmoMemberDescriptor;
import ai.timefold.solver.core.impl.domain.solution.cloner.DeepCloningUtils;
import ai.timefold.solver.core.impl.domain.solution.cloner.FieldAccessingSolutionCloner;
import ai.timefold.solver.core.impl.domain.solution.cloner.PlanningCloneable;
import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor;
import ai.timefold.solver.core.impl.util.MutableReference;

Expand Down Expand Up @@ -323,7 +324,16 @@ private void createCloneSolutionRun(ClassCreator classCreator, SolutionDescripto
GizmoSolutionOrEntityDescriptor solutionSubclassDescriptor =
memoizedSolutionOrEntityDescriptorMap.computeIfAbsent(solutionSubclass,
(key) -> new GizmoSolutionOrEntityDescriptor(solutionDescriptor, solutionSubclass));
ResultHandle clone = isSubclassBranch.newInstance(MethodDescriptor.ofConstructor(solutionSubclass));

ResultHandle clone;
if (PlanningCloneable.class.isAssignableFrom(solutionSubclass)) {
clone = isSubclassBranch.invokeInterfaceMethod(
MethodDescriptor.ofMethod(PlanningCloneable.class, "createNewInstance", Object.class),
thisObj);
clone = isSubclassBranch.checkCast(clone, solutionSubclass);
} else {
clone = isSubclassBranch.newInstance(MethodDescriptor.ofConstructor(solutionSubclass));
}

isSubclassBranch.invokeInterfaceMethod(
MethodDescriptor.ofMethod(Map.class, "put", Object.class, Object.class, Object.class),
Expand Down Expand Up @@ -599,7 +609,14 @@ private void writeDeepCloneCollectionInstructions(BytecodeCreator bytecodeCreato
ResultHandle size = bytecodeCreator
.invokeInterfaceMethod(MethodDescriptor.ofMethod(Collection.class, "size", int.class), toClone);

if (List.class.isAssignableFrom(deeplyClonedFieldClass)) {
if (PlanningCloneable.class.isAssignableFrom(deeplyClonedFieldClass)) {
var emptyInstance = bytecodeCreator
.invokeInterfaceMethod(MethodDescriptor.ofMethod(PlanningCloneable.class, "createNewInstance",
Object.class), bytecodeCreator.checkCast(toClone, PlanningCloneable.class));
bytecodeCreator.assign(cloneCollection,
bytecodeCreator.checkCast(emptyInstance,
Collection.class));
} else if (List.class.isAssignableFrom(deeplyClonedFieldClass)) {
bytecodeCreator.assign(cloneCollection,
bytecodeCreator.newInstance(MethodDescriptor.ofConstructor(ArrayList.class, int.class), size));
} else if (Set.class.isAssignableFrom(deeplyClonedFieldClass)) {
Expand Down Expand Up @@ -696,33 +713,41 @@ private void writeDeepCloneMapInstructions(BytecodeCreator bytecodeCreator,
Class<?> deeplyClonedFieldClass, java.lang.reflect.Type type, ResultHandle toClone,
AssignableResultHandle cloneResultHolder, ResultHandle createdCloneMap,
SortedSet<Class<?>> deepClonedClassesSortedSet) {
Class<?> holderClass = deeplyClonedFieldClass;
try {
holderClass.getConstructor();
} catch (NoSuchMethodException e) {
if (LinkedHashMap.class.isAssignableFrom(holderClass)) {
holderClass = LinkedHashMap.class;
} else if (ConcurrentHashMap.class.isAssignableFrom(holderClass)) {
holderClass = ConcurrentHashMap.class;
} else {
// Default to LinkedHashMap
holderClass = LinkedHashMap.class;
ResultHandle cloneMap;
if (PlanningCloneable.class.isAssignableFrom(deeplyClonedFieldClass)) {
var emptyInstance = bytecodeCreator
.invokeInterfaceMethod(MethodDescriptor.ofMethod(PlanningCloneable.class, "createNewInstance",
Object.class), bytecodeCreator.checkCast(toClone, PlanningCloneable.class));
cloneMap = bytecodeCreator.checkCast(emptyInstance, Map.class);
} else {
Class<?> holderClass = deeplyClonedFieldClass;
try {
holderClass.getConstructor();
} catch (NoSuchMethodException e) {
if (LinkedHashMap.class.isAssignableFrom(holderClass)) {
holderClass = LinkedHashMap.class;
} else if (ConcurrentHashMap.class.isAssignableFrom(holderClass)) {
holderClass = ConcurrentHashMap.class;
} else {
// Default to LinkedHashMap
holderClass = LinkedHashMap.class;
}
}

ResultHandle size =
bytecodeCreator.invokeInterfaceMethod(MethodDescriptor.ofMethod(Map.class, "size", int.class), toClone);
try {
holderClass.getConstructor(int.class);
cloneMap = bytecodeCreator.newInstance(MethodDescriptor.ofConstructor(holderClass, int.class), size);
} catch (NoSuchMethodException e) {
cloneMap = bytecodeCreator.newInstance(MethodDescriptor.ofConstructor(holderClass));
}
}

ResultHandle cloneMap;
ResultHandle size =
bytecodeCreator.invokeInterfaceMethod(MethodDescriptor.ofMethod(Map.class, "size", int.class), toClone);
ResultHandle entrySet = bytecodeCreator
.invokeInterfaceMethod(MethodDescriptor.ofMethod(Map.class, "entrySet", Set.class), toClone);
ResultHandle iterator = bytecodeCreator
.invokeInterfaceMethod(MethodDescriptor.ofMethod(Iterable.class, "iterator", Iterator.class), entrySet);
try {
holderClass.getConstructor(int.class);
cloneMap = bytecodeCreator.newInstance(MethodDescriptor.ofConstructor(holderClass, int.class), size);
} catch (NoSuchMethodException e) {
cloneMap = bytecodeCreator.newInstance(MethodDescriptor.ofConstructor(holderClass));
}

BytecodeCreator whileLoopBlock = bytecodeCreator.whileLoop(conditionBytecode -> {
ResultHandle hasNext = conditionBytecode
Expand Down Expand Up @@ -960,7 +985,15 @@ private void createDeepCloneHelperMethod(ClassCreator classCreator,
toClone,
cloneMap);

ResultHandle cloneObj = noCloneBranch.newInstance(MethodDescriptor.ofConstructor(entityClass));
ResultHandle cloneObj;
if (PlanningCloneable.class.isAssignableFrom(entityClass)) {
cloneObj = noCloneBranch.invokeInterfaceMethod(
MethodDescriptor.ofMethod(PlanningCloneable.class, "createNewInstance", Object.class),
toClone);
cloneObj = noCloneBranch.checkCast(cloneObj, entityClass);
} else {
cloneObj = noCloneBranch.newInstance(MethodDescriptor.ofConstructor(entityClass));
}
noCloneBranch.invokeInterfaceMethod(
MethodDescriptor.ofMethod(Map.class, "put", Object.class, Object.class, Object.class),
cloneMap, toClone, cloneObj);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
import ai.timefold.solver.core.impl.testdata.domain.clone.deepcloning.TestdataVariousTypes;
import ai.timefold.solver.core.impl.testdata.domain.clone.deepcloning.field.TestdataFieldAnnotatedDeepCloningEntity;
import ai.timefold.solver.core.impl.testdata.domain.clone.deepcloning.field.TestdataFieldAnnotatedDeepCloningSolution;
import ai.timefold.solver.core.impl.testdata.domain.clone.planning_cloneable.PlanningCloneableEntity;
import ai.timefold.solver.core.impl.testdata.domain.clone.planning_cloneable.PlanningCloneableSolution;
import ai.timefold.solver.core.impl.testdata.domain.collection.TestdataArrayBasedEntity;
import ai.timefold.solver.core.impl.testdata.domain.collection.TestdataArrayBasedSolution;
import ai.timefold.solver.core.impl.testdata.domain.collection.TestdataEntityCollectionPropertyEntity;
Expand Down Expand Up @@ -1119,4 +1121,48 @@ void cloneExtendedShadowEntities() {
.isNotSameAs(original.shadowEntityList.get(0));
}

@Test
void clonePlanningCloneableItems() {
var solutionDescriptor = PlanningCloneableSolution.buildSolutionDescriptor();
var cloner = createSolutionCloner(solutionDescriptor);

var entityA = new PlanningCloneableEntity("A");
var entityB = new PlanningCloneableEntity("B");
var entityC = new PlanningCloneableEntity("C");

var original = new PlanningCloneableSolution(List.of(entityA, entityB, entityC));
var clone = cloner.cloneSolution(original);

assertThat(clone.entityList)
.hasSize(3)
.isNotSameAs(original.entityList)
.first()
.isNotNull();

assertThat(clone.entityList.get(0))
.isNotSameAs(original.entityList.get(0))
.hasFieldOrPropertyWithValue("code", "A");

assertThat(clone.entityList.get(1))
.isNotSameAs(original.entityList.get(1))
.hasFieldOrPropertyWithValue("code", "B");

assertThat(clone.entityList.get(2))
.isNotSameAs(original.entityList.get(2))
.hasFieldOrPropertyWithValue("code", "C");

assertThat(clone.codeToEntity)
.hasSize(3)
.isNotSameAs(original.codeToEntity);

assertThat(clone.codeToEntity.get("A"))
.isSameAs(clone.entityList.get(0));

assertThat(clone.codeToEntity.get("B"))
.isSameAs(clone.entityList.get(1));

assertThat(clone.codeToEntity.get("C"))
.isSameAs(clone.entityList.get(2));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package ai.timefold.solver.core.impl.testdata.domain.clone.planning_cloneable;

import ai.timefold.solver.core.api.domain.entity.PlanningEntity;
import ai.timefold.solver.core.api.domain.variable.PlanningVariable;
import ai.timefold.solver.core.impl.domain.solution.cloner.PlanningCloneable;
import ai.timefold.solver.core.impl.testdata.domain.TestdataValue;

@PlanningEntity
public class PlanningCloneableEntity implements PlanningCloneable<PlanningCloneableEntity> {
public String code;
@PlanningVariable
public TestdataValue value;

public PlanningCloneableEntity(String code) {
this.code = code;
}

public String getCode() {
return code;
}

@Override
public PlanningCloneableEntity createNewInstance() {
return new PlanningCloneableEntity(code);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package ai.timefold.solver.core.impl.testdata.domain.clone.planning_cloneable;

import java.util.AbstractList;
import java.util.ArrayList;
import java.util.List;

import ai.timefold.solver.core.impl.domain.solution.cloner.PlanningCloneable;

public class PlanningCloneableList<T> extends AbstractList<T> implements PlanningCloneable<PlanningCloneableList<T>> {
private final List<T> backingList;

public PlanningCloneableList() {
this.backingList = new ArrayList<>();
}

@Override
public PlanningCloneableList<T> createNewInstance() {
return new PlanningCloneableList<>();
}

@Override
public T get(int i) {
return backingList.get(i);
}

@Override
public T set(int i, T item) {
return backingList.set(i, item);
}

@Override
public void add(int i, T item) {
backingList.add(i, item);
}

@Override
public T remove(int i) {
return backingList.remove(i);
}

@Override
public int size() {
return backingList.size();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package ai.timefold.solver.core.impl.testdata.domain.clone.planning_cloneable;

import java.util.AbstractMap;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import ai.timefold.solver.core.impl.domain.solution.cloner.PlanningCloneable;

public class PlanningCloneableMap<K, V> extends AbstractMap<K, V> implements PlanningCloneable<PlanningCloneableMap<K, V>> {
private final Map<K, V> backingMap;

public PlanningCloneableMap() {
this.backingMap = new HashMap<>();
}

@Override
public PlanningCloneableMap<K, V> createNewInstance() {
return new PlanningCloneableMap<>();
}

@Override
public Set<Entry<K, V>> entrySet() {
return backingMap.entrySet();
}

@Override
public V put(K key, V value) {
return backingMap.put(key, value);
}
}
Loading

0 comments on commit 019a13e

Please sign in to comment.