Skip to content

Commit 6c4e2e6

Browse files
authored
fix: properly detect value ranges with wildcard types (#838)
1 parent 610b1a8 commit 6c4e2e6

File tree

8 files changed

+107
-146
lines changed

8 files changed

+107
-146
lines changed

core/src/main/java/ai/timefold/solver/core/config/util/ConfigUtils.java

Lines changed: 69 additions & 97 deletions
Large diffs are not rendered by default.

core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/SolutionDescriptor.java

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -331,9 +331,7 @@ private Class<? extends Annotation> extractFactEntityOrScoreAnnotationClassOrAut
331331
+ "Maybe the member (" + memberName + ") should return a typed "
332332
+ Collection.class.getSimpleName() + ".");
333333
}
334-
elementType = ConfigUtils.extractCollectionGenericTypeParameterLeniently(
335-
"solutionClass", solutionClass,
336-
type, genericType,
334+
elementType = ConfigUtils.extractGenericTypeParameter("solutionClass", solutionClass, type, genericType,
337335
null, member.getName()).orElse(Object.class);
338336
} else {
339337
elementType = type.getComponentType();
@@ -588,10 +586,10 @@ private Set<Class<?>> collectEntityAndProblemFactClasses() {
588586
Stream<Class<?>> problemFactOrEntityClassStream = concat(entityClassStream, factClassStream);
589587
Stream<Class<?>> factCollectionClassStream = problemFactCollectionMemberAccessorMap.values()
590588
.stream()
591-
.map(accessor -> ConfigUtils.extractCollectionGenericTypeParameterLeniently(
592-
"solutionClass", getSolutionClass(),
593-
accessor.getType(), accessor.getGenericType(), ProblemFactCollectionProperty.class,
594-
accessor.getName()).orElse(Object.class));
589+
.map(accessor -> ConfigUtils
590+
.extractGenericTypeParameter("solutionClass", getSolutionClass(), accessor.getType(),
591+
accessor.getGenericType(), ProblemFactCollectionProperty.class, accessor.getName())
592+
.orElse(Object.class));
595593
problemFactOrEntityClassStream = concat(problemFactOrEntityClassStream, factCollectionClassStream);
596594
// Add constraint configuration, if configured.
597595
if (constraintConfigurationDescriptor != null) {
@@ -932,12 +930,9 @@ public void visitEntitiesByEntityClass(Solution_ solution, Class<?> entityClass,
932930
}
933931
}
934932
for (MemberAccessor entityCollectionMemberAccessor : entityCollectionMemberAccessorMap.values()) {
935-
Optional<Class<?>> optionalTypeParameter = ConfigUtils.extractCollectionGenericTypeParameterLeniently(
936-
"solutionClass", entityCollectionMemberAccessor.getDeclaringClass(),
937-
entityCollectionMemberAccessor.getType(),
938-
entityCollectionMemberAccessor.getGenericType(),
939-
null,
940-
entityCollectionMemberAccessor.getName());
933+
Optional<Class<?>> optionalTypeParameter = ConfigUtils.extractGenericTypeParameter("solutionClass",
934+
entityCollectionMemberAccessor.getDeclaringClass(), entityCollectionMemberAccessor.getType(),
935+
entityCollectionMemberAccessor.getGenericType(), null, entityCollectionMemberAccessor.getName());
941936
boolean collectionGuaranteedToContainOnlyGivenEntityType = optionalTypeParameter
942937
.map(entityClass::isAssignableFrom)
943938
.orElse(false);

core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/descriptor/AbstractFromPropertyValueRangeDescriptor.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,8 @@ private void processValueRangeProviderAnnotation(ValueRangeProvider valueRangePr
6363
+ ", an array or a " + ValueRange.class.getSimpleName() + ".");
6464
}
6565
if (collectionWrapping) {
66-
Class<?> collectionElementClass = ConfigUtils.extractCollectionGenericTypeParameterStrictly(
67-
"solutionClass or entityClass", memberAccessor.getDeclaringClass(),
68-
memberAccessor.getType(), memberAccessor.getGenericType(),
66+
Class<?> collectionElementClass = ConfigUtils.extractGenericTypeParameterOrFail("solutionClass or entityClass",
67+
memberAccessor.getDeclaringClass(), memberAccessor.getType(), memberAccessor.getGenericType(),
6968
ValueRangeProvider.class, memberAccessor.getName());
7069
if (!variableDescriptor.acceptsValueType(collectionElementClass)) {
7170
throw new IllegalArgumentException("The entityClass (" + entityDescriptor.getEntityClass()

core/src/main/java/ai/timefold/solver/core/impl/domain/variable/descriptor/GenuineVariableDescriptor.java

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
package ai.timefold.solver.core.impl.domain.variable.descriptor;
22

3+
import static ai.timefold.solver.core.config.util.ConfigUtils.newInstance;
4+
35
import java.lang.reflect.ParameterizedType;
4-
import java.lang.reflect.Type;
56
import java.util.ArrayList;
67
import java.util.Arrays;
7-
import java.util.Collection;
88
import java.util.Comparator;
99
import java.util.stream.Stream;
1010

1111
import ai.timefold.solver.core.api.domain.solution.PlanningSolution;
12-
import ai.timefold.solver.core.api.domain.valuerange.ValueRange;
1312
import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider;
1413
import ai.timefold.solver.core.api.domain.variable.PlanningListVariable;
1514
import ai.timefold.solver.core.api.domain.variable.PlanningVariable;
@@ -91,8 +90,8 @@ The entityClass (%s) has a @%s annotated property (%s) that has no valueRangePro
9190
}
9291

9392
private MemberAccessor[] findAnonymousValueRangeMemberAccessors(DescriptorPolicy descriptorPolicy) {
94-
boolean supportsValueRangeProviderFromEntity = !isListVariable();
95-
Stream<MemberAccessor> applicableValueRangeProviderAccessors =
93+
var supportsValueRangeProviderFromEntity = !isListVariable();
94+
var applicableValueRangeProviderAccessors =
9695
supportsValueRangeProviderFromEntity ? Stream.concat(
9796
descriptorPolicy.getAnonymousFromEntityValueRangeProviderSet().stream(),
9897
descriptorPolicy.getAnonymousFromSolutionValueRangeProviderSet().stream())
@@ -103,26 +102,23 @@ private MemberAccessor[] findAnonymousValueRangeMemberAccessors(DescriptorPolicy
103102
* For basic variable, the type is the type of the variable.
104103
* For list variable, the type is List<X>, and we need to know X.
105104
*/
106-
Class<?> variableType =
105+
var variableType =
107106
isListVariable() ? (Class<?>) ((ParameterizedType) variableMemberAccessor.getGenericType())
108107
.getActualTypeArguments()[0] : variableMemberAccessor.getType();
109108
// We expect either ValueRange, Collection or an array.
110-
Type valueRangeType = valueRangeProviderAccessor.getGenericType();
109+
var valueRangeType = valueRangeProviderAccessor.getGenericType();
111110
if (valueRangeType instanceof ParameterizedType parameterizedValueRangeType) {
112-
Class<?> rawType = (Class<?>) parameterizedValueRangeType.getRawType();
113-
if (!ValueRange.class.isAssignableFrom(rawType) && !Collection.class.isAssignableFrom(rawType)) {
114-
return false;
115-
}
116-
Type[] generics = parameterizedValueRangeType.getActualTypeArguments();
117-
if (generics.length != 1) {
118-
return false;
119-
}
120-
Class<?> valueRangeGenericType = (Class<?>) generics[0];
121-
return variableType.isAssignableFrom(valueRangeGenericType);
111+
return ConfigUtils
112+
.extractGenericTypeParameter("solutionClass",
113+
entityDescriptor.getSolutionDescriptor().getSolutionClass(),
114+
valueRangeProviderAccessor.getType(), parameterizedValueRangeType,
115+
ValueRangeProvider.class, valueRangeProviderAccessor.getName())
116+
.map(variableType::isAssignableFrom)
117+
.orElse(false);
122118
} else {
123-
Class<?> clz = (Class<?>) valueRangeType;
119+
var clz = (Class<?>) valueRangeType;
124120
if (clz.isArray()) {
125-
Class<?> componentType = clz.getComponentType();
121+
var componentType = clz.getComponentType();
126122
return variableType.isAssignableFrom(componentType);
127123
}
128124
return false;
@@ -137,7 +133,7 @@ private MemberAccessor findValueRangeMemberAccessor(DescriptorPolicy descriptorP
137133
} else if (descriptorPolicy.hasFromEntityValueRangeProvider(valueRangeProviderRef)) {
138134
return descriptorPolicy.getFromEntityValueRangeProvider(valueRangeProviderRef);
139135
} else {
140-
Collection<String> providerIds = descriptorPolicy.getValueRangeProviderIds();
136+
var providerIds = descriptorPolicy.getValueRangeProviderIds();
141137
throw new IllegalArgumentException("The entityClass (" + entityDescriptor.getEntityClass()
142138
+ ") has a @" + PlanningVariable.class.getSimpleName()
143139
+ " annotated property (" + variableMemberAccessor.getName()
@@ -183,15 +179,15 @@ protected void processStrength(Class<? extends Comparator> strengthComparatorCla
183179
+ ") at the same time.");
184180
}
185181
if (strengthComparatorClass != null) {
186-
Comparator<Object> strengthComparator = ConfigUtils.newInstance(this::toString,
182+
Comparator<Object> strengthComparator = newInstance(this::toString,
187183
"strengthComparatorClass", strengthComparatorClass);
188184
increasingStrengthSorter = new ComparatorSelectionSorter<>(strengthComparator,
189185
SelectionSorterOrder.ASCENDING);
190186
decreasingStrengthSorter = new ComparatorSelectionSorter<>(strengthComparator,
191187
SelectionSorterOrder.DESCENDING);
192188
}
193189
if (strengthWeightFactoryClass != null) {
194-
SelectionSorterWeightFactory<Solution_, Object> strengthWeightFactory = ConfigUtils.newInstance(this::toString,
190+
SelectionSorterWeightFactory<Solution_, Object> strengthWeightFactory = newInstance(this::toString,
195191
"strengthWeightFactoryClass", strengthWeightFactoryClass);
196192
increasingStrengthSorter = new WeightFactorySelectionSorter<>(strengthWeightFactory,
197193
SelectionSorterOrder.ASCENDING);
@@ -241,7 +237,7 @@ public boolean isValueRangeEntityIndependent() {
241237
* is reinitializable if its value is {@code null}.
242238
*/
243239
public boolean isReinitializable(Object entity) {
244-
Object value = getValue(entity);
240+
var value = getValue(entity);
245241
return value == null;
246242
}
247243

core/src/main/java/ai/timefold/solver/core/impl/domain/variable/descriptor/ListVariableDescriptor.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,9 @@ public boolean isInitialized(Object entity) {
9393
}
9494

9595
public Class<?> getElementType() {
96-
return ConfigUtils.extractCollectionGenericTypeParameterStrictly(
97-
"entityClass", entityDescriptor.getEntityClass(),
98-
variableMemberAccessor.getType(), variableMemberAccessor.getGenericType(),
99-
PlanningListVariable.class, variableMemberAccessor.getName());
96+
return ConfigUtils.extractGenericTypeParameterOrFail("entityClass", entityDescriptor.getEntityClass(),
97+
variableMemberAccessor.getType(), variableMemberAccessor.getGenericType(), PlanningListVariable.class,
98+
variableMemberAccessor.getName());
10099
}
101100

102101
public int countUnassigned(Solution_ solution) {

core/src/main/java/ai/timefold/solver/core/impl/domain/variable/inverserelation/InverseRelationShadowVariableDescriptor.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,10 @@ private void linkShadowSources(DescriptorPolicy descriptorPolicy) {
6060
Class<?> sourceClass;
6161
if (Collection.class.isAssignableFrom(variablePropertyType)) {
6262
Type genericType = variableMemberAccessor.getGenericType();
63-
sourceClass = ConfigUtils.extractCollectionGenericTypeParameterLeniently(
64-
"entityClass", entityDescriptor.getEntityClass(),
65-
variablePropertyType, genericType,
66-
InverseRelationShadowVariable.class, variableMemberAccessor.getName()).orElse(Object.class);
63+
sourceClass = ConfigUtils
64+
.extractGenericTypeParameter("entityClass", entityDescriptor.getEntityClass(), variablePropertyType,
65+
genericType, InverseRelationShadowVariable.class, variableMemberAccessor.getName())
66+
.orElse(Object.class);
6767
singleton = false;
6868
} else {
6969
sourceClass = variablePropertyType;

core/src/test/java/ai/timefold/solver/core/api/domain/valuerange/AnonymousValueRangeFactoryTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ void solveList() {
7777
assertThat(solution).isNotNull();
7878
}
7979

80-
private void assertEntity(SoftAssertions softly, TestdataAnonymousValueRangeEntity entity) {
80+
private static void assertEntity(SoftAssertions softly, TestdataAnonymousValueRangeEntity entity) {
8181
softly.assertThat(entity.getNumberValue()).isNotNull();
8282
softly.assertThat(entity.getIntegerValue()).isNotNull();
8383
softly.assertThat(entity.getLongValue()).isNotNull();

core/src/test/java/ai/timefold/solver/core/impl/testdata/domain/valuerange/anonymous/TestdataAnonymousListSolution.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public List<Long> createLongList() {
6464
}
6565

6666
@ValueRangeProvider
67-
public List<Number> createNumberList() {
67+
public List<? super Number> createNumberList() { // Test the wildcards too.
6868
return List.of(0, BigInteger.TEN);
6969
}
7070

0 commit comments

Comments
 (0)