8383import ai .timefold .solver .core .preview .api .domain .metamodel .PlanningSolutionMetaModel ;
8484import ai .timefold .solver .core .preview .api .domain .solution .diff .PlanningSolutionDiff ;
8585
86+ import org .jspecify .annotations .NonNull ;
8687import org .slf4j .Logger ;
8788import 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