Skip to content

Commit 36f4b78

Browse files
committed
chore: prevent NPE in solution annotation processing
1 parent 6c3c668 commit 36f4b78

1 file changed

Lines changed: 26 additions & 13 deletions

File tree

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

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningSolutionMetaModel;
8484
import ai.timefold.solver.core.preview.api.domain.solution.diff.PlanningSolutionDiff;
8585

86+
import org.jspecify.annotations.NonNull;
8687
import org.slf4j.Logger;
8788
import org.slf4j.LoggerFactory;
8889

@@ -395,20 +396,9 @@ Maybe add a getScore() method with a @%s annotation."""
395396
}
396397

397398
private void processSolutionAnnotations(DescriptorPolicy descriptorPolicy) {
398-
var solutionAnnotation = solutionClass.getAnnotation(PlanningSolution.class);
399-
var parentSolutionAnnotation =
400-
solutionClass.getSuperclass() != null ? solutionClass.getSuperclass().getAnnotation(PlanningSolution.class)
401-
: null;
402-
if (solutionAnnotation == null && parentSolutionAnnotation == null) {
403-
throw new IllegalStateException(
404-
"The solutionClass (%s) has been specified as a solution in the configuration, but does not have a @%s annotation."
405-
.formatted(solutionClass, PlanningSolution.class.getSimpleName()));
406-
}
407-
var annotation = solutionAnnotation != null ? solutionAnnotation : parentSolutionAnnotation;
399+
var annotation = extractTopMostPlanningSolutionAnnotation();
408400
autoDiscoverMemberType = annotation.autoDiscoverMemberType();
409-
// We accept only the child class cloner
410-
var solutionClonerClass =
411-
solutionAnnotation != null ? solutionAnnotation.solutionCloner() : PlanningSolution.NullSolutionCloner.class;
401+
var solutionClonerClass = annotation.solutionCloner();
412402
if (solutionClonerClass != PlanningSolution.NullSolutionCloner.class) {
413403
solutionCloner = ConfigUtils.newInstance(this::toString, "solutionClonerClass", solutionClonerClass);
414404
}
@@ -417,6 +407,29 @@ private void processSolutionAnnotations(DescriptorPolicy descriptorPolicy) {
417407
new LookUpStrategyResolver(descriptorPolicy, lookUpStrategyType);
418408
}
419409

410+
private @NonNull PlanningSolution extractTopMostPlanningSolutionAnnotation() {
411+
var solutionAnnotation = solutionClass.getAnnotation(PlanningSolution.class);
412+
if (solutionAnnotation != null) {
413+
return solutionAnnotation;
414+
}
415+
var solutionSuperclass = solutionClass.getSuperclass(); // Null if interface.
416+
if (solutionSuperclass == null) {
417+
throw new IllegalStateException("""
418+
The solutionClass (%s) has been specified as a solution in the configuration, \
419+
but does not have a @%s annotation."""
420+
.formatted(solutionClass.getCanonicalName(), PlanningSolution.class.getSimpleName()));
421+
}
422+
var parentSolutionAnnotation = solutionSuperclass.getAnnotation(PlanningSolution.class);
423+
if (parentSolutionAnnotation == null) {
424+
throw new IllegalStateException("""
425+
The solutionClass (%s) has been specified as a solution in the configuration, \
426+
but neither it nor its superclass (%s) have a @%s annotation."""
427+
.formatted(solutionClass.getCanonicalName(), solutionSuperclass.getCanonicalName(),
428+
PlanningSolution.class.getSimpleName()));
429+
}
430+
return parentSolutionAnnotation;
431+
}
432+
420433
private void processValueRangeProviderAnnotation(DescriptorPolicy descriptorPolicy, Member member) {
421434
if (((AnnotatedElement) member).isAnnotationPresent(ValueRangeProvider.class)) {
422435
var memberAccessor = descriptorPolicy.getMemberAccessorFactory().buildAndCacheMemberAccessor(member,

0 commit comments

Comments
 (0)