diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/ReflectionHelper.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/ReflectionHelper.java index 766d4e2c37e..c6483795fc7 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/ReflectionHelper.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/ReflectionHelper.java @@ -136,7 +136,7 @@ public static Method getDeclaredGetterMethod(Class containingClass, String pr var baseClass = containingClass; while (baseClass != null) { try { - return containingClass.getDeclaredMethod(getterName); + return baseClass.getDeclaredMethod(getterName); } catch (NoSuchMethodException e) { baseClass = baseClass.getSuperclass(); } @@ -158,12 +158,12 @@ public static Method getDeclaredGetterMethod(Class containingClass, String pr * @param methodName never null * @return sometimes null */ - public static Method getDeclaredMethod(Class containingClass, String methodName) { + public static Method getDeclaredMethod(Class containingClass, String methodName, Class... parameterTypes) { var baseClass = containingClass; while (baseClass != null) { try { - return containingClass.getDeclaredMethod(methodName); + return baseClass.getDeclaredMethod(methodName, parameterTypes); } catch (NoSuchMethodException e) { baseClass = baseClass.getSuperclass(); } @@ -233,6 +233,17 @@ public static Method getSetterMethod(Class containingClass, Class property } } + /** + * @param containingClass never null + * @param propertyType never null + * @param propertyName never null + * @return null if it doesn't exist + */ + public static Method getDeclaredSetterMethod(Class containingClass, Class propertyType, String propertyName) { + String setterName = PROPERTY_MUTATOR_PREFIX + capitalizePropertyName(propertyName); + return getDeclaredMethod(containingClass, setterName, propertyType); + } + /** * @param containingClass never null * @param propertyName never null diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/ReflectionBeanPropertyMemberAccessor.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/ReflectionBeanPropertyMemberAccessor.java index e17878cc257..e5580c59a5d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/ReflectionBeanPropertyMemberAccessor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/ReflectionBeanPropertyMemberAccessor.java @@ -4,7 +4,9 @@ import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.lang.reflect.Type; +import java.util.function.IntPredicate; import ai.timefold.solver.core.impl.domain.common.ReflectionHelper; @@ -34,14 +36,13 @@ public ReflectionBeanPropertyMemberAccessor(Method getterMethod, boolean getterO } catch (IllegalAccessException e) { throw new IllegalStateException(""" Impossible state: method (%s) not accessible. - %s - """ - .strip() + %s""" .formatted(getterMethod, MemberAccessorFactory.CLASSLOADER_NUDGE_MESSAGE), e); } Class declaringClass = getterMethod.getDeclaringClass(); if (!ReflectionHelper.isGetterMethod(getterMethod)) { - throw new IllegalArgumentException("The getterMethod (" + getterMethod + ") is not a valid getter."); + throw new IllegalArgumentException("The getterMethod (%s) is not a valid getter." + .formatted(getterMethod)); } propertyType = getterMethod.getReturnType(); propertyName = ReflectionHelper.getGetterPropertyName(getterMethod); @@ -49,23 +50,59 @@ public ReflectionBeanPropertyMemberAccessor(Method getterMethod, boolean getterO setterMethod = null; setterMethodHandle = null; } else { - setterMethod = ReflectionHelper.getSetterMethod(declaringClass, getterMethod.getReturnType(), propertyName); - if (setterMethod != null) { - try { - setterMethod.setAccessible(true); // Performance hack by avoiding security checks - this.setterMethodHandle = lookup.unreflect(setterMethod) - .asFixedArity(); - } catch (IllegalAccessException e) { - throw new IllegalStateException(""" - Impossible state: method (%s) not accessible. - %s - """ - .strip() - .formatted(setterMethod, MemberAccessorFactory.CLASSLOADER_NUDGE_MESSAGE), e); + setterMethod = ReflectionHelper.getDeclaredSetterMethod(declaringClass, getterMethod.getReturnType(), propertyName); + if (setterMethod == null) { + throw new IllegalArgumentException("The getterMethod (%s) does not have a matching setterMethod on class (%s)." + .formatted(getterMethod.getName(), declaringClass.getCanonicalName())); + } + var getterAccess = AccessModifier.forMethod(getterMethod); + var setterAccess = AccessModifier.forMethod(setterMethod); + if (getterAccess != setterAccess) { + throw new IllegalArgumentException( + "The getterMethod (%s) has access modifier (%s) which does not match the setterMethod (%s) access modifier (%s) on class (%s)." + .formatted(getterMethod.getName(), getterAccess, setterMethod.getName(), setterAccess, + declaringClass.getCanonicalName())); + } + try { + setterMethod.setAccessible(true); // Performance hack by avoiding security checks + this.setterMethodHandle = lookup.unreflect(setterMethod) + .asFixedArity(); + } catch (IllegalAccessException e) { + throw new IllegalStateException(""" + Impossible state: method (%s) not accessible. + %s""" + .formatted(setterMethod, MemberAccessorFactory.CLASSLOADER_NUDGE_MESSAGE), e); + } + } + } + + private enum AccessModifier { + PUBLIC("public", Modifier::isPublic), + PROTECTED("protected", Modifier::isProtected), + PACKAGE_PRIVATE("package-private", modifier -> false), + PRIVATE("private", Modifier::isPrivate); + + final String name; + final IntPredicate predicate; + + AccessModifier(String name, IntPredicate predicate) { + this.name = name; + this.predicate = predicate; + } + + public static AccessModifier forMethod(Method method) { + var modifiers = method.getModifiers(); + for (var accessModifier : AccessModifier.values()) { + if (accessModifier.predicate.test(modifiers)) { + return accessModifier; } - } else { - setterMethodHandle = null; } + return PACKAGE_PRIVATE; + } + + @Override + public String toString() { + return name; } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/entity/descriptor/EntityDescriptor.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/entity/descriptor/EntityDescriptor.java index 62664e3535d..47ba0f663ee 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/entity/descriptor/EntityDescriptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/entity/descriptor/EntityDescriptor.java @@ -1,6 +1,5 @@ package ai.timefold.solver.core.impl.domain.entity.descriptor; -import static ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorFactory.MemberAccessorType.FIELD_OR_GETTER_METHOD; import static ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorFactory.MemberAccessorType.FIELD_OR_GETTER_METHOD_WITH_SETTER; import static ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorFactory.MemberAccessorType.FIELD_OR_READ_METHOD; import static ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptorValidator.assertNotMixedInheritance; @@ -46,7 +45,6 @@ import ai.timefold.solver.core.config.util.ConfigUtils; import ai.timefold.solver.core.impl.domain.common.ReflectionHelper; import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessor; -import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorFactory; import ai.timefold.solver.core.impl.domain.policy.DescriptorPolicy; import ai.timefold.solver.core.impl.domain.solution.descriptor.ProblemScaleTracker; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; @@ -323,44 +321,27 @@ private void processValueRangeProviderAnnotation(DescriptorPolicy descriptorPoli } } + @SuppressWarnings("unchecked") private void processPlanningVariableAnnotation(MutableInt variableDescriptorCounter, DescriptorPolicy descriptorPolicy, Member member) { - var variableAnnotationClass = ConfigUtils.extractAnnotationClass( - member, VARIABLE_ANNOTATION_CLASSES); + var variableAnnotationClass = ConfigUtils.extractAnnotationClass(member, VARIABLE_ANNOTATION_CLASSES); if (variableAnnotationClass != null) { - MemberAccessorFactory.MemberAccessorType memberAccessorType; - if (variableAnnotationClass.equals(ShadowVariable.class)) { - // Need to check only the single annotation version, - // since supplier variable can only be used with a single - // annotation. - ShadowVariable annotation; - if (member instanceof Field field) { - annotation = field.getAnnotation(ShadowVariable.class); - } else if (member instanceof Method method) { - annotation = method.getAnnotation(ShadowVariable.class); - } else { - throw new IllegalStateException( - "Member must be a field or a method, but was (%s).".formatted(member.getClass().getSimpleName())); - } - if (annotation == null) { - throw new IllegalStateException("Impossible state: cannot get %s annotation on a %s annotated member (%s)." - .formatted(ShadowVariable.class.getSimpleName(), ShadowVariable.class.getSimpleName(), member)); - } - if (!annotation.supplierName().isEmpty()) { - memberAccessorType = FIELD_OR_GETTER_METHOD_WITH_SETTER; - } else { - memberAccessorType = FIELD_OR_GETTER_METHOD; - } - } else if (variableAnnotationClass.equals(CustomShadowVariable.class) - || variableAnnotationClass.equals(ShadowVariable.List.class) - || variableAnnotationClass.equals(PiggybackShadowVariable.class) - || variableAnnotationClass.equals(CascadingUpdateShadowVariable.class)) { - memberAccessorType = FIELD_OR_GETTER_METHOD; + Object annotation; + if (member instanceof Field field) { + annotation = field.getAnnotation(variableAnnotationClass); + } else if (member instanceof Method method) { + annotation = method.getAnnotation(variableAnnotationClass); } else { - memberAccessorType = FIELD_OR_GETTER_METHOD_WITH_SETTER; + throw new IllegalStateException("Member must be a field or a method, but was (%s)." + .formatted(member.getClass().getSimpleName())); } - var memberAccessor = descriptorPolicy.getMemberAccessorFactory().buildAndCacheMemberAccessor(member, - memberAccessorType, variableAnnotationClass, descriptorPolicy.getDomainAccessType()); + if (annotation == null) { + throw new IllegalStateException("Impossible state: cannot get annotation on a %s-annotated member (%s)." + .formatted(variableAnnotationClass, member)); + } + var memberAccessor = descriptorPolicy.getMemberAccessorFactory() + .buildAndCacheMemberAccessor(member, FIELD_OR_GETTER_METHOD_WITH_SETTER, variableAnnotationClass, + descriptorPolicy.getDomainAccessType()); registerVariableAccessor(variableDescriptorCounter.intValue(), variableAnnotationClass, memberAccessor); variableDescriptorCounter.increment(); } @@ -695,10 +676,6 @@ public GenuineVariableDescriptor getGenuineVariableDescriptor(String return shadowVariableLoopedDescriptor; } - public boolean hasAnyGenuineVariables() { - return !effectiveGenuineVariableDescriptorMap.isEmpty(); - } - public boolean hasBothGenuineListAndBasicVariables() { if (!isGenuine()) { return false; @@ -732,7 +709,7 @@ public boolean hasAnyGenuineListVariables() { } public boolean isGenuine() { - return hasAnyGenuineVariables(); + return !effectiveGenuineVariableDescriptorMap.isEmpty(); } public ListVariableDescriptor getGenuineListVariableDescriptor() { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraintFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraintFactory.java index 6acb8eaa7d8..22a39a4fc9b 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraintFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraintFactory.java @@ -135,7 +135,7 @@ public > Stream_ share( public @NonNull UniConstraintStream from(@NonNull Class fromClass) { assertValidFromType(fromClass); var entityDescriptor = solutionDescriptor.findEntityDescriptor(fromClass); - if (entityDescriptor != null && entityDescriptor.hasAnyGenuineVariables()) { + if (entityDescriptor != null && entityDescriptor.isGenuine()) { var predicate = (Predicate) entityDescriptor.getIsInitializedPredicate(); return share(new BavetForEachUniConstraintStream<>(this, fromClass, predicate, RetrievalSemantics.LEGACY)); } else { diff --git a/core/src/test/java/ai/timefold/solver/core/impl/domain/common/accessor/ReflectionBeanPropertyMemberAccessorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/domain/common/accessor/ReflectionBeanPropertyMemberAccessorTest.java index 96688aac153..9fd46c9cb54 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/domain/common/accessor/ReflectionBeanPropertyMemberAccessorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/domain/common/accessor/ReflectionBeanPropertyMemberAccessorTest.java @@ -1,10 +1,13 @@ package ai.timefold.solver.core.impl.domain.common.accessor; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; import ai.timefold.solver.core.api.domain.variable.PlanningVariable; import ai.timefold.solver.core.testdomain.TestdataEntity; import ai.timefold.solver.core.testdomain.TestdataValue; +import ai.timefold.solver.core.testdomain.invalid.gettersetter.TestdataDifferentGetterSetterVisibilityEntity; +import ai.timefold.solver.core.testdomain.invalid.gettersetter.TestdataInvalidGetterEntity; import org.junit.jupiter.api.Test; @@ -26,4 +29,31 @@ void methodAnnotatedEntity() throws NoSuchMethodException { assertThat(e1.getValue()).isSameAs(v2); } + @Test + void getterSetterVisibilityDoesNotMatch() { + assertThatCode(() -> new ReflectionBeanPropertyMemberAccessor( + TestdataDifferentGetterSetterVisibilityEntity.class.getDeclaredMethod("getValue1"))) + .hasMessageContainingAll("getterMethod (getValue1)", + "has access modifier (public)", + "not match the setterMethod (setValue1)", + "access modifier (private)", + "on class (ai.timefold.solver.core.testdomain.invalid.gettersetter.TestdataDifferentGetterSetterVisibilityEntity)"); + assertThatCode(() -> new ReflectionBeanPropertyMemberAccessor( + TestdataDifferentGetterSetterVisibilityEntity.class.getDeclaredMethod("getValue2"))) + .hasMessageContainingAll("getterMethod (getValue2)", + "has access modifier (package-private)", + "not match the setterMethod (setValue2)", + "access modifier (protected)", + "on class (ai.timefold.solver.core.testdomain.invalid.gettersetter.TestdataDifferentGetterSetterVisibilityEntity)"); + } + + @Test + void setterMissing() { + assertThatCode(() -> new ReflectionBeanPropertyMemberAccessor( + TestdataInvalidGetterEntity.class.getDeclaredMethod("getValueWithoutSetter"))) + .hasMessageContainingAll("getterMethod (getValueWithoutSetter)", + "does not have a matching setterMethod", + "on class (ai.timefold.solver.core.testdomain.invalid.gettersetter.TestdataInvalidGetterEntity)"); + } + } diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/invalid/gettersetter/TestdataDifferentGetterSetterVisibilityEntity.java b/core/src/test/java/ai/timefold/solver/core/testdomain/invalid/gettersetter/TestdataDifferentGetterSetterVisibilityEntity.java new file mode 100644 index 00000000000..c77ded8c575 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/invalid/gettersetter/TestdataDifferentGetterSetterVisibilityEntity.java @@ -0,0 +1,32 @@ +package ai.timefold.solver.core.testdomain.invalid.gettersetter; + +import ai.timefold.solver.core.api.domain.entity.PlanningEntity; +import ai.timefold.solver.core.api.domain.variable.PlanningVariable; +import ai.timefold.solver.core.testdomain.TestdataValue; + +@PlanningEntity +public class TestdataDifferentGetterSetterVisibilityEntity { + + private TestdataValue value1; + private TestdataValue value2; + + @PlanningVariable + public TestdataValue getValue1() { + return value1; + } + + private void setValue1(TestdataValue value1) { + this.value1 = value1; + } + + @PlanningVariable + TestdataValue getValue2() { + return value2; + } + + @PlanningVariable + protected TestdataValue setValue2(TestdataValue value2) { + return value2; + } + +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/invalid/gettersetter/TestdataInvalidGetterEntity.java b/core/src/test/java/ai/timefold/solver/core/testdomain/invalid/gettersetter/TestdataInvalidGetterEntity.java new file mode 100644 index 00000000000..1b168afa046 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/invalid/gettersetter/TestdataInvalidGetterEntity.java @@ -0,0 +1,17 @@ +package ai.timefold.solver.core.testdomain.invalid.gettersetter; + +import ai.timefold.solver.core.api.domain.entity.PlanningEntity; +import ai.timefold.solver.core.api.domain.variable.PlanningVariable; +import ai.timefold.solver.core.testdomain.TestdataValue; + +@PlanningEntity +public class TestdataInvalidGetterEntity { + + @PlanningVariable + private TestdataValue valueWithoutSetter; + + public TestdataValue getValueWithoutSetter() { + return valueWithoutSetter; + } + +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/score/TestdataBendableBigDecimalScoreSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/score/TestdataBendableBigDecimalScoreSolution.java index 9c6f61a20f4..66c4defd62c 100644 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/score/TestdataBendableBigDecimalScoreSolution.java +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/score/TestdataBendableBigDecimalScoreSolution.java @@ -75,7 +75,7 @@ public void setEntityList(List entityList) { } @PlanningScore(bendableHardLevelsSize = 1, bendableSoftLevelsSize = 2) - BendableBigDecimalScore getScore() { + public BendableBigDecimalScore getScore() { return score; } diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/score/TestdataBendableLongScoreSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/score/TestdataBendableLongScoreSolution.java index b1baa467ac5..aeba0ee4c48 100644 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/score/TestdataBendableLongScoreSolution.java +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/score/TestdataBendableLongScoreSolution.java @@ -75,7 +75,7 @@ public void setEntityList(List entityList) { } @PlanningScore(bendableHardLevelsSize = 1, bendableSoftLevelsSize = 2) - BendableLongScore getScore() { + public BendableLongScore getScore() { return score; } diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/score/TestdataBendableScoreSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/score/TestdataBendableScoreSolution.java index af4d3943ec6..98c72b687ed 100644 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/score/TestdataBendableScoreSolution.java +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/score/TestdataBendableScoreSolution.java @@ -75,7 +75,7 @@ public void setEntityList(List entityList) { } @PlanningScore(bendableHardLevelsSize = 1, bendableSoftLevelsSize = 2) - BendableScore getScore() { + public BendableScore getScore() { return score; } diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/score/TestdataHardMediumSoftBigDecimalScoreSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/score/TestdataHardMediumSoftBigDecimalScoreSolution.java index 2489b1dc77e..ba2639804a4 100644 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/score/TestdataHardMediumSoftBigDecimalScoreSolution.java +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/score/TestdataHardMediumSoftBigDecimalScoreSolution.java @@ -77,7 +77,7 @@ public void setEntityList(List entityList) { } @PlanningScore - HardMediumSoftBigDecimalScore getScore() { + public HardMediumSoftBigDecimalScore getScore() { return score; } diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/score/TestdataHardMediumSoftLongScoreSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/score/TestdataHardMediumSoftLongScoreSolution.java index 79e837713c2..37c22847f12 100644 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/score/TestdataHardMediumSoftLongScoreSolution.java +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/score/TestdataHardMediumSoftLongScoreSolution.java @@ -75,7 +75,7 @@ public void setEntityList(List entityList) { } @PlanningScore - HardMediumSoftLongScore getScore() { + public HardMediumSoftLongScore getScore() { return score; } diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/score/TestdataHardMediumSoftScoreSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/score/TestdataHardMediumSoftScoreSolution.java index 53bc066ba17..2687af2e6a5 100644 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/score/TestdataHardMediumSoftScoreSolution.java +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/score/TestdataHardMediumSoftScoreSolution.java @@ -75,7 +75,7 @@ public void setEntityList(List entityList) { } @PlanningScore - HardMediumSoftScore getScore() { + public HardMediumSoftScore getScore() { return score; } diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/score/TestdataHardSoftBigDecimalScoreSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/score/TestdataHardSoftBigDecimalScoreSolution.java index 7d259be1a86..4a148c012b2 100644 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/score/TestdataHardSoftBigDecimalScoreSolution.java +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/score/TestdataHardSoftBigDecimalScoreSolution.java @@ -75,7 +75,7 @@ public void setEntityList(List entityList) { } @PlanningScore - HardSoftBigDecimalScore getScore() { + public HardSoftBigDecimalScore getScore() { return score; } diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/score/TestdataHardSoftLongScoreSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/score/TestdataHardSoftLongScoreSolution.java index 942be690838..1a3ca68357a 100644 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/score/TestdataHardSoftLongScoreSolution.java +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/score/TestdataHardSoftLongScoreSolution.java @@ -75,7 +75,7 @@ public void setEntityList(List entityList) { } @PlanningScore - HardSoftLongScore getScore() { + public HardSoftLongScore getScore() { return score; } diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/score/TestdataHardSoftScoreSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/score/TestdataHardSoftScoreSolution.java index a0182f5aae0..b2f739bcb63 100644 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/score/TestdataHardSoftScoreSolution.java +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/score/TestdataHardSoftScoreSolution.java @@ -75,7 +75,7 @@ public void setEntityList(List entityList) { } @PlanningScore - HardSoftScore getScore() { + public HardSoftScore getScore() { return score; } diff --git a/docs/src/modules/ROOT/pages/upgrading-timefold-solver/upgrade-to-latest-version.adoc b/docs/src/modules/ROOT/pages/upgrading-timefold-solver/upgrade-to-latest-version.adoc index 18833abffdf..6416b813c94 100644 --- a/docs/src/modules/ROOT/pages/upgrading-timefold-solver/upgrade-to-latest-version.adoc +++ b/docs/src/modules/ROOT/pages/upgrading-timefold-solver/upgrade-to-latest-version.adoc @@ -70,6 +70,45 @@ We have now deprecated the `pinningFilter` field on `@PlanningEntity` annotation and we encourage you to use the xref:responding-to-change/responding-to-change.adoc#pinnedPlanningEntities[`@PlanningPin` annotation] instead. ==== +''' + +.icon:info-circle[role=yellow] Stricter variable accessors +[%collapsible%open] +==== +In your Java domain classes, the following rules now apply to getters and setters over genuine and shadow planning variables: + +- If a getter exists, so must a setter. +- If a setter exists, so must a getter. +- Both getter and setter must have the same level of visibility, preferably `public`. + +Before in `*.java`: + +[source,java] +---- + @PlanningVariable + private Visit visit; + + Visit getVisit() { + return visit; + } +---- + +After in `*.java`: + +[source,java] +---- + @PlanningVariable + private Visit visit; + + public Visit getVisit() { + return visit; + } + + public void setVisit(Visit visit) { + this.visit = visit; + } +---- +==== === Upgrade from 1.21.0 to 1.22.0 diff --git a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorGeneratedGizmoSupplierTest.java b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorGeneratedGizmoSupplierTest.java index 183737cb414..70ff03754f9 100644 --- a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorGeneratedGizmoSupplierTest.java +++ b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorGeneratedGizmoSupplierTest.java @@ -99,13 +99,17 @@ public interface DummyInterfaceEntity { @ShadowVariable(variableListenerClass = DummyVariableListener.class, sourceEntityClass = TestdataEntity.class, sourceVariableName = "value") Integer getLength(); + + void setLength(Integer length); } @PlanningEntity public abstract static class DummyAbstractEntity { @ShadowVariable(variableListenerClass = DummyVariableListener.class, sourceEntityClass = TestdataEntity.class, sourceVariableName = "value") - abstract Integer getLength(); + public abstract Integer getLength(); + + public abstract void setLength(Integer length); } public static class DummySolutionPartitioner implements SolutionPartitioner { diff --git a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/testdomain/gizmo/TestDataKitchenSinkEntity.java b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/testdomain/gizmo/TestDataKitchenSinkEntity.java index 5d0d2a37291..6b5c1bfd180 100644 --- a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/testdomain/gizmo/TestDataKitchenSinkEntity.java +++ b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/testdomain/gizmo/TestDataKitchenSinkEntity.java @@ -52,7 +52,7 @@ public class TestDataKitchenSinkEntity { private boolean isPinned; @PlanningVariable(valueRangeProviderRefs = { "ints" }) - private Integer getIntVariable() { + public Integer getIntVariable() { return intVariable; }