Skip to content

Commit d2b2406

Browse files
authored
chore: prevent NPE in solution annotation processing
Also addresses an unrelated improvement to fail-fast exception messages.
1 parent d4ca33a commit d2b2406

File tree

1 file changed

+35
-24
lines changed

1 file changed

+35
-24
lines changed

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

Lines changed: 35 additions & 24 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 = extractMostRelevantPlanningSolutionAnnotation();
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 extractMostRelevantPlanningSolutionAnnotation() {
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,
@@ -579,7 +592,7 @@ private void processProblemFactPropertyAnnotation(DescriptorPolicy descriptorPol
579592
var type = memberAccessor.getType();
580593
if (!(Collection.class.isAssignableFrom(type) || type.isArray())) {
581594
throw new IllegalStateException(
582-
"The solutionClass (%s) has a @%s annotated member (%s) that does not return a %s or an array."
595+
"The solutionClass (%s) has a @%s-annotated member (%s) that does not return a %s or an array."
583596
.formatted(solutionClass, ProblemFactCollectionProperty.class.getSimpleName(), member,
584597
Collection.class.getSimpleName()));
585598
}
@@ -596,16 +609,14 @@ private void processProblemFactPropertyAnnotation(DescriptorPolicy descriptorPol
596609
throw new IllegalStateException("Impossible situation with annotationClass (" + annotationClass + ").");
597610
}
598611
if (problemFactType.isAnnotationPresent(PlanningEntity.class)) {
599-
throw new IllegalStateException(
600-
"The solutionClass (%s) has a @%s annotated member (%s) that returns a @%s. Maybe use @%s instead?"
601-
.formatted(
602-
solutionClass,
603-
annotationClass,
604-
memberAccessor.getName(),
605-
PlanningEntity.class.getSimpleName(),
606-
((annotationClass == ProblemFactProperty.class)
607-
? PlanningEntityProperty.class.getSimpleName()
608-
: PlanningEntityCollectionProperty.class.getSimpleName())));
612+
throw new IllegalStateException("""
613+
The solutionClass (%s) has a @%s-annotated member (%s) that returns a @%s.
614+
Maybe use @%s instead?"""
615+
.formatted(solutionClass, annotationClass.getSimpleName(), memberAccessor.getName(),
616+
PlanningEntity.class.getSimpleName(),
617+
((annotationClass == ProblemFactProperty.class)
618+
? PlanningEntityProperty.class.getSimpleName()
619+
: PlanningEntityCollectionProperty.class.getSimpleName())));
609620
}
610621
}
611622

0 commit comments

Comments
 (0)