Skip to content

Commit 7b37414

Browse files
committed
feat: add PlanningSpecification programmatic API and LambdaBasedSolutionCloner
Introduces PlanningSpecification<S> as an intermediate representation (IR) that decouples domain model definition from annotation scanning. Both the annotation-based and new programmatic paths produce the same IR, which is compiled into a SolutionDescriptor by SpecificationCompiler. Adds LambdaBasedSolutionCloner, a queue-based non-recursive implementation using LambdaMetafactory for fast field access without bytecode generation.
1 parent fa1f273 commit 7b37414

File tree

87 files changed

+8000
-3195
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

87 files changed

+8000
-3195
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package ai.timefold.solver.core.api.domain.specification;
2+
3+
import java.util.function.Consumer;
4+
5+
/**
6+
* Builder for configuring a cascading update shadow variable.
7+
*
8+
* @param <S> the solution type
9+
* @param <E> the entity type
10+
*/
11+
public interface CascadingUpdateShadowBuilder<S, E> {
12+
13+
CascadingUpdateShadowBuilder<S, E> updateMethod(Consumer<E> updater);
14+
15+
CascadingUpdateShadowBuilder<S, E> sources(String... sourcePaths);
16+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package ai.timefold.solver.core.api.domain.specification;
2+
3+
import java.util.List;
4+
import java.util.Map;
5+
import java.util.Set;
6+
import java.util.function.BiConsumer;
7+
import java.util.function.Function;
8+
import java.util.function.Supplier;
9+
10+
import ai.timefold.solver.core.api.domain.solution.cloner.SolutionCloner;
11+
12+
/**
13+
* Describes how to clone a planning solution using lambdas.
14+
* <p>
15+
* The specification contains a complete cloning recipe: for every cloneable class (solution, entities,
16+
* {@code @DeepPlanningClone} facts), it lists all fields with their getter/setter lambdas and
17+
* a pre-classified {@link DeepCloneDecision} that determines how each field value is handled during cloning.
18+
*
19+
* @param solutionFactory creates a new empty solution instance
20+
* @param solutionProperties all fields on the solution class (including inherited), with clone decisions
21+
* @param cloneableClasses cloning recipes for entities and {@code @DeepPlanningClone} facts, keyed by exact class
22+
* @param entityClasses all entity classes (for runtime entity detection in cloneMap lookups)
23+
* @param deepCloneClasses all {@code @DeepPlanningClone}-annotated classes
24+
* @param customCloner optional custom cloner (null if using lambda-based cloning)
25+
* @param <S> the solution type
26+
*/
27+
public record CloningSpecification<S>(
28+
Supplier<S> solutionFactory,
29+
List<PropertyCopyDescriptor> solutionProperties,
30+
Map<Class<?>, CloneableClassDescriptor> cloneableClasses,
31+
Set<Class<?>> entityClasses,
32+
Set<Class<?>> deepCloneClasses,
33+
SolutionCloner<S> customCloner) {
34+
35+
/**
36+
* Describes how to copy a single field during cloning.
37+
*
38+
* @param name the field name (for debugging)
39+
* @param getter reads the field value from an object
40+
* @param setter writes the field value to an object
41+
* @param deepCloneDecision how this field's value should be handled during cloning
42+
* @param cloneTimeValidationMessage if non-null, an error message to throw at clone time
43+
* (used for deferred validation, e.g. {@code @DeepPlanningClone} on a {@code @PlanningVariable}
44+
* whose type is not deep-cloneable)
45+
*/
46+
public record PropertyCopyDescriptor(
47+
String name,
48+
Function<Object, Object> getter,
49+
BiConsumer<Object, Object> setter,
50+
DeepCloneDecision deepCloneDecision,
51+
String cloneTimeValidationMessage) {
52+
53+
public PropertyCopyDescriptor(
54+
String name,
55+
Function<Object, Object> getter,
56+
BiConsumer<Object, Object> setter,
57+
DeepCloneDecision deepCloneDecision) {
58+
this(name, getter, setter, deepCloneDecision, null);
59+
}
60+
}
61+
62+
/**
63+
* Pre-classified decision for how a field value is handled during cloning.
64+
* Determined at specification-build time so no runtime type inspection is needed.
65+
*/
66+
public enum DeepCloneDecision {
67+
/** Immutable type: direct copy (no cloning needed). */
68+
SHALLOW,
69+
/** May be an entity or deep-clone type: resolve from cloneMap. */
70+
RESOLVE_ENTITY_REFERENCE,
71+
/** {@code @DeepPlanningClone} type: always deep clone. */
72+
ALWAYS_DEEP,
73+
/** Collection needing element resolution/deep-cloning. */
74+
DEEP_COLLECTION,
75+
/** Map needing key/value resolution/deep-cloning. */
76+
DEEP_MAP,
77+
/** Array needing element resolution/deep-cloning. */
78+
DEEP_ARRAY,
79+
/**
80+
* Non-immutable type where the field's declared type is not known to be deep-cloneable,
81+
* but the runtime value's class might be (e.g. a subclass annotated with {@code @DeepPlanningClone}).
82+
* At clone time: check if the value's actual class is deep-cloneable, and deep-clone if so.
83+
* Otherwise, shallow copy.
84+
*/
85+
SHALLOW_OR_DEEP_BY_RUNTIME_TYPE
86+
}
87+
88+
/**
89+
* Cloning recipe for a single cloneable class (entity or {@code @DeepPlanningClone} fact).
90+
*
91+
* @param clazz the class
92+
* @param factory creates a new empty instance (no-arg constructor)
93+
* @param properties all fields (including inherited), with clone decisions
94+
*/
95+
public record CloneableClassDescriptor(
96+
Class<?> clazz,
97+
Supplier<Object> factory,
98+
List<PropertyCopyDescriptor> properties) {
99+
}
100+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package ai.timefold.solver.core.api.domain.specification;
2+
3+
import java.util.function.BiConsumer;
4+
import java.util.function.Consumer;
5+
import java.util.function.Function;
6+
import java.util.function.Supplier;
7+
8+
/**
9+
* Builder for configuring lambda-based solution cloning.
10+
* <p>
11+
* This builder allows users to define a complete cloning recipe: for the solution class,
12+
* every entity class, and any {@code @DeepPlanningClone} fact classes, all fields are declared
13+
* with their getter/setter lambdas and a {@link CloningSpecification.DeepCloneDecision}.
14+
*
15+
* @param <S> the solution type
16+
*/
17+
public interface CloningSpecificationBuilder<S> {
18+
19+
/**
20+
* Sets the factory that creates new empty solution instances.
21+
*/
22+
CloningSpecificationBuilder<S> solutionFactory(Supplier<S> factory);
23+
24+
/**
25+
* Declares a shallow-copy property on the solution class.
26+
*/
27+
<V> CloningSpecificationBuilder<S> solutionProperty(String name, Function<S, V> getter, BiConsumer<S, V> setter);
28+
29+
/**
30+
* Declares a property on the solution class with an explicit deep clone decision.
31+
*/
32+
<V> CloningSpecificationBuilder<S> solutionProperty(String name, Function<S, V> getter, BiConsumer<S, V> setter,
33+
CloningSpecification.DeepCloneDecision decision);
34+
35+
/**
36+
* Registers an entity class with its no-arg constructor and property definitions.
37+
*/
38+
<E> CloningSpecificationBuilder<S> entityClass(Class<E> entityClass, Supplier<E> factory,
39+
Consumer<CloneableClassBuilder<E>> config);
40+
41+
/**
42+
* Registers a {@code @DeepPlanningClone} fact class with its no-arg constructor and property definitions.
43+
*/
44+
<E> CloningSpecificationBuilder<S> deepCloneFact(Class<E> factClass, Supplier<E> factory,
45+
Consumer<CloneableClassBuilder<E>> config);
46+
47+
/**
48+
* Builder for defining properties on a cloneable class (entity or deep-clone fact).
49+
*
50+
* @param <E> the class type
51+
*/
52+
interface CloneableClassBuilder<E> {
53+
54+
/**
55+
* Declares a shallow-copy property.
56+
*/
57+
<V> CloneableClassBuilder<E> shallowProperty(String name, Function<E, V> getter, BiConsumer<E, V> setter);
58+
59+
/**
60+
* Declares a property that may reference an entity (resolved from cloneMap).
61+
*/
62+
<V> CloneableClassBuilder<E> entityRefProperty(String name, Function<E, V> getter, BiConsumer<E, V> setter);
63+
64+
/**
65+
* Declares a property that should always be deep-cloned.
66+
*/
67+
<V> CloneableClassBuilder<E> deepProperty(String name, Function<E, V> getter, BiConsumer<E, V> setter);
68+
69+
/**
70+
* Declares a collection property that needs element resolution/deep-cloning.
71+
*/
72+
<V> CloneableClassBuilder<E> deepCollectionProperty(String name, Function<E, V> getter,
73+
BiConsumer<E, V> setter);
74+
75+
/**
76+
* Declares a map property that needs key/value resolution/deep-cloning.
77+
*/
78+
<V> CloneableClassBuilder<E> deepMapProperty(String name, Function<E, V> getter, BiConsumer<E, V> setter);
79+
80+
/**
81+
* Declares an array property that needs element resolution/deep-cloning.
82+
*/
83+
<V> CloneableClassBuilder<E> deepArrayProperty(String name, Function<E, V> getter, BiConsumer<E, V> setter);
84+
85+
/**
86+
* Declares a property with an explicit deep clone decision.
87+
*/
88+
<V> CloneableClassBuilder<E> property(String name, Function<E, V> getter, BiConsumer<E, V> setter,
89+
CloningSpecification.DeepCloneDecision decision);
90+
}
91+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package ai.timefold.solver.core.api.domain.specification;
2+
3+
import java.util.function.Function;
4+
5+
import ai.timefold.solver.core.api.domain.solution.ConstraintWeightOverrides;
6+
7+
/**
8+
* Describes constraint weight overrides on a planning solution.
9+
*
10+
* @param getter reads the constraint weight overrides from the solution
11+
* @param <S> the solution type
12+
*/
13+
public record ConstraintWeightSpecification<S>(
14+
Function<S, ConstraintWeightOverrides<?>> getter) {
15+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package ai.timefold.solver.core.api.domain.specification;
2+
3+
import java.util.function.Function;
4+
5+
/**
6+
* Builder for configuring a declarative shadow variable.
7+
*
8+
* @param <S> the solution type
9+
* @param <E> the entity type
10+
* @param <V> the shadow variable value type
11+
*/
12+
public interface DeclarativeShadowBuilder<S, E, V> {
13+
14+
DeclarativeShadowBuilder<S, E, V> supplier(Function<E, V> supplier);
15+
16+
DeclarativeShadowBuilder<S, E, V> sources(String... sourcePaths);
17+
18+
DeclarativeShadowBuilder<S, E, V> alignmentKey(String key);
19+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package ai.timefold.solver.core.api.domain.specification;
2+
3+
import java.util.Collection;
4+
import java.util.function.BiConsumer;
5+
import java.util.function.Function;
6+
7+
import org.jspecify.annotations.Nullable;
8+
9+
/**
10+
* Describes an entity collection property on a planning solution.
11+
*
12+
* @param name the property name
13+
* @param getter reads the entity collection from the solution
14+
* @param setter writes the entity collection to the solution (may be null)
15+
* @param isSingular true if this is a singular {@code @PlanningEntityProperty} (not a collection)
16+
* @param <S> the solution type
17+
*/
18+
public record EntityCollectionSpecification<S>(
19+
String name,
20+
Function<S, ? extends Collection<?>> getter,
21+
@Nullable BiConsumer<S, Object> setter,
22+
boolean isSingular) {
23+
24+
/**
25+
* Constructor without setter (programmatic API, always treated as collection).
26+
*/
27+
public EntityCollectionSpecification(String name, Function<S, ? extends Collection<?>> getter) {
28+
this(name, getter, null, false);
29+
}
30+
31+
/**
32+
* Constructor without setter (annotation path).
33+
*/
34+
public EntityCollectionSpecification(String name, Function<S, ? extends Collection<?>> getter, boolean isSingular) {
35+
this(name, getter, null, isSingular);
36+
}
37+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package ai.timefold.solver.core.api.domain.specification;
2+
3+
import java.util.Comparator;
4+
import java.util.List;
5+
import java.util.function.Function;
6+
import java.util.function.Predicate;
7+
import java.util.function.ToIntFunction;
8+
9+
/**
10+
* Describes a planning entity.
11+
*
12+
* @param entityClass the entity class
13+
* @param planningIdGetter optional getter for the planning ID
14+
* @param difficultyComparator optional comparator for entity difficulty sorting
15+
* @param difficultyComparatorFactoryClass optional factory class for solution-aware entity difficulty sorting
16+
* @param pinnedPredicate optional predicate to determine if an entity is pinned
17+
* @param pinToIndexFunction optional function to determine the pin-to-index
18+
* @param variables the genuine planning variables
19+
* @param shadows the shadow variables
20+
* @param entityScopedValueRanges value ranges scoped to this entity
21+
* @param <S> the solution type
22+
*/
23+
public record EntitySpecification<S>(
24+
Class<?> entityClass,
25+
Function<?, ?> planningIdGetter,
26+
java.util.function.BiConsumer<?, Object> planningIdSetter,
27+
Comparator<?> difficultyComparator,
28+
Class<?> difficultyComparatorFactoryClass,
29+
Predicate<?> pinnedPredicate,
30+
ToIntFunction<?> pinToIndexFunction,
31+
List<VariableSpecification<S>> variables,
32+
List<ShadowSpecification<S>> shadows,
33+
List<ValueRangeSpecification<S>> entityScopedValueRanges) {
34+
35+
/**
36+
* Backward-compatible constructor without planningIdSetter and difficultyComparatorFactoryClass.
37+
*/
38+
public EntitySpecification(
39+
Class<?> entityClass,
40+
Function<?, ?> planningIdGetter,
41+
Comparator<?> difficultyComparator,
42+
Predicate<?> pinnedPredicate,
43+
ToIntFunction<?> pinToIndexFunction,
44+
List<VariableSpecification<S>> variables,
45+
List<ShadowSpecification<S>> shadows,
46+
List<ValueRangeSpecification<S>> entityScopedValueRanges) {
47+
this(entityClass, planningIdGetter, null, difficultyComparator, null,
48+
pinnedPredicate, pinToIndexFunction, variables, shadows, entityScopedValueRanges);
49+
}
50+
51+
/**
52+
* Backward-compatible constructor without planningIdSetter.
53+
*/
54+
public EntitySpecification(
55+
Class<?> entityClass,
56+
Function<?, ?> planningIdGetter,
57+
Comparator<?> difficultyComparator,
58+
Class<?> difficultyComparatorFactoryClass,
59+
Predicate<?> pinnedPredicate,
60+
ToIntFunction<?> pinToIndexFunction,
61+
List<VariableSpecification<S>> variables,
62+
List<ShadowSpecification<S>> shadows,
63+
List<ValueRangeSpecification<S>> entityScopedValueRanges) {
64+
this(entityClass, planningIdGetter, null, difficultyComparator, difficultyComparatorFactoryClass,
65+
pinnedPredicate, pinToIndexFunction, variables, shadows, entityScopedValueRanges);
66+
}
67+
}

0 commit comments

Comments
 (0)