Skip to content

Commit 287a193

Browse files
Christopher-Chianellitriceo
authored andcommitted
chore: enfore getter match setter visibility for variables and score
1 parent 18a9c0c commit 287a193

15 files changed

+122
-16
lines changed

core/src/main/java/ai/timefold/solver/core/impl/domain/common/ReflectionHelper.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ public static Method getDeclaredGetterMethod(Class<?> containingClass, String pr
136136
var baseClass = containingClass;
137137
while (baseClass != null) {
138138
try {
139-
return containingClass.getDeclaredMethod(getterName);
139+
return baseClass.getDeclaredMethod(getterName);
140140
} catch (NoSuchMethodException e) {
141141
baseClass = baseClass.getSuperclass();
142142
}
@@ -158,12 +158,12 @@ public static Method getDeclaredGetterMethod(Class<?> containingClass, String pr
158158
* @param methodName never null
159159
* @return sometimes null
160160
*/
161-
public static Method getDeclaredMethod(Class<?> containingClass, String methodName) {
161+
public static Method getDeclaredMethod(Class<?> containingClass, String methodName, Class<?>... parameterTypes) {
162162
var baseClass = containingClass;
163163

164164
while (baseClass != null) {
165165
try {
166-
return containingClass.getDeclaredMethod(methodName);
166+
return baseClass.getDeclaredMethod(methodName, parameterTypes);
167167
} catch (NoSuchMethodException e) {
168168
baseClass = baseClass.getSuperclass();
169169
}
@@ -233,6 +233,17 @@ public static Method getSetterMethod(Class<?> containingClass, Class<?> property
233233
}
234234
}
235235

236+
/**
237+
* @param containingClass never null
238+
* @param propertyType never null
239+
* @param propertyName never null
240+
* @return null if it doesn't exist
241+
*/
242+
public static Method getDeclaredSetterMethod(Class<?> containingClass, Class<?> propertyType, String propertyName) {
243+
String setterName = PROPERTY_MUTATOR_PREFIX + capitalizePropertyName(propertyName);
244+
return getDeclaredMethod(containingClass, setterName, propertyType);
245+
}
246+
236247
/**
237248
* @param containingClass never null
238249
* @param propertyName never null

core/src/main/java/ai/timefold/solver/core/impl/domain/common/accessor/ReflectionBeanPropertyMemberAccessor.java

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
import java.lang.invoke.MethodHandle;
55
import java.lang.invoke.MethodHandles;
66
import java.lang.reflect.Method;
7+
import java.lang.reflect.Modifier;
78
import java.lang.reflect.Type;
9+
import java.util.function.IntPredicate;
810

911
import ai.timefold.solver.core.impl.domain.common.ReflectionHelper;
1012

@@ -49,8 +51,16 @@ public ReflectionBeanPropertyMemberAccessor(Method getterMethod, boolean getterO
4951
setterMethod = null;
5052
setterMethodHandle = null;
5153
} else {
52-
setterMethod = ReflectionHelper.getSetterMethod(declaringClass, getterMethod.getReturnType(), propertyName);
54+
setterMethod = ReflectionHelper.getDeclaredSetterMethod(declaringClass, getterMethod.getReturnType(), propertyName);
5355
if (setterMethod != null) {
56+
var getterAccess = AccessModifier.forMethod(getterMethod);
57+
var setterAccess = AccessModifier.forMethod(setterMethod);
58+
if (getterAccess != setterAccess) {
59+
throw new IllegalArgumentException(
60+
"The getter method (%s) has access modifier (%s) which does not match the setter method's (%s) access modifier (%s) on class (%s)."
61+
.formatted(getterMethod.getName(), getterAccess, setterMethod.getName(), setterAccess,
62+
getterMethod.getDeclaringClass().getSimpleName()));
63+
}
5464
try {
5565
setterMethod.setAccessible(true); // Performance hack by avoiding security checks
5666
this.setterMethodHandle = lookup.unreflect(setterMethod)
@@ -69,6 +79,41 @@ public ReflectionBeanPropertyMemberAccessor(Method getterMethod, boolean getterO
6979
}
7080
}
7181

82+
private enum AccessModifier {
83+
PUBLIC("public", Modifier::isPublic),
84+
PROTECTED("protected", Modifier::isProtected),
85+
PACKAGE_PRIVATE("package-private", modifier -> false),
86+
PRIVATE("private", Modifier::isPrivate);
87+
88+
final String name;
89+
final IntPredicate predicate;
90+
91+
AccessModifier(String name, IntPredicate predicate) {
92+
this.name = name;
93+
this.predicate = predicate;
94+
}
95+
96+
public static AccessModifier forMethod(Method method) {
97+
var modifiers = method.getModifiers();
98+
for (var accessModifier : AccessModifier.values()) {
99+
if (accessModifier.predicate.test(modifiers)) {
100+
return accessModifier;
101+
}
102+
}
103+
return PACKAGE_PRIVATE;
104+
}
105+
106+
@Override
107+
public String toString() {
108+
return name;
109+
}
110+
}
111+
112+
private int getAccessLevel(Method method) {
113+
int mask = Modifier.PUBLIC | Modifier.PROTECTED | Modifier.PRIVATE;
114+
return method.getModifiers() & mask;
115+
}
116+
72117
@Override
73118
public Class<?> getDeclaringClass() {
74119
return getterMethod.getDeclaringClass();

core/src/test/java/ai/timefold/solver/core/impl/domain/common/accessor/ReflectionBeanPropertyMemberAccessorTest.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package ai.timefold.solver.core.impl.domain.common.accessor;
22

33
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.assertj.core.api.Assertions.assertThatCode;
45

56
import ai.timefold.solver.core.api.domain.variable.PlanningVariable;
67
import ai.timefold.solver.core.testdomain.TestdataEntity;
78
import ai.timefold.solver.core.testdomain.TestdataValue;
9+
import ai.timefold.solver.core.testdomain.invalid.gettersettervisibility.TestdataDifferentGetterSetterVisibilityEntity;
810

911
import org.junit.jupiter.api.Test;
1012

@@ -26,4 +28,22 @@ void methodAnnotatedEntity() throws NoSuchMethodException {
2628
assertThat(e1.getValue()).isSameAs(v2);
2729
}
2830

31+
@Test
32+
void getterSetterVisibilityDoesNotMatch() {
33+
assertThatCode(() -> new ReflectionBeanPropertyMemberAccessor(
34+
TestdataDifferentGetterSetterVisibilityEntity.class.getDeclaredMethod("getValue1")))
35+
.hasMessageContainingAll("getter method (getValue1)",
36+
"has access modifier (public)",
37+
"not match the setter method's (setValue1)",
38+
"access modifier (private)",
39+
"on class (TestdataDifferentGetterSetterVisibilityEntity)");
40+
assertThatCode(() -> new ReflectionBeanPropertyMemberAccessor(
41+
TestdataDifferentGetterSetterVisibilityEntity.class.getDeclaredMethod("getValue2")))
42+
.hasMessageContainingAll("getter method (getValue2)",
43+
"has access modifier (package-private)",
44+
"not match the setter method's (setValue2)",
45+
"access modifier (protected)",
46+
"on class (TestdataDifferentGetterSetterVisibilityEntity)");
47+
}
48+
2949
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package ai.timefold.solver.core.testdomain.invalid.gettersettervisibility;
2+
3+
import ai.timefold.solver.core.api.domain.entity.PlanningEntity;
4+
import ai.timefold.solver.core.api.domain.variable.PlanningVariable;
5+
import ai.timefold.solver.core.testdomain.TestdataValue;
6+
7+
@PlanningEntity
8+
public class TestdataDifferentGetterSetterVisibilityEntity {
9+
private TestdataValue value1;
10+
private TestdataValue value2;
11+
12+
@PlanningVariable
13+
public TestdataValue getValue1() {
14+
return value1;
15+
}
16+
17+
private void setValue1(TestdataValue value1) {
18+
this.value1 = value1;
19+
}
20+
21+
@PlanningVariable
22+
TestdataValue getValue2() {
23+
return value2;
24+
}
25+
26+
@PlanningVariable
27+
protected TestdataValue setValue2(TestdataValue value2) {
28+
return value2;
29+
}
30+
}

core/src/test/java/ai/timefold/solver/core/testdomain/score/TestdataBendableBigDecimalScoreSolution.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ public void setEntityList(List<TestdataEntity> entityList) {
7575
}
7676

7777
@PlanningScore(bendableHardLevelsSize = 1, bendableSoftLevelsSize = 2)
78-
BendableBigDecimalScore getScore() {
78+
public BendableBigDecimalScore getScore() {
7979
return score;
8080
}
8181

core/src/test/java/ai/timefold/solver/core/testdomain/score/TestdataBendableLongScoreSolution.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ public void setEntityList(List<TestdataEntity> entityList) {
7575
}
7676

7777
@PlanningScore(bendableHardLevelsSize = 1, bendableSoftLevelsSize = 2)
78-
BendableLongScore getScore() {
78+
public BendableLongScore getScore() {
7979
return score;
8080
}
8181

core/src/test/java/ai/timefold/solver/core/testdomain/score/TestdataBendableScoreSolution.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ public void setEntityList(List<TestdataEntity> entityList) {
7575
}
7676

7777
@PlanningScore(bendableHardLevelsSize = 1, bendableSoftLevelsSize = 2)
78-
BendableScore getScore() {
78+
public BendableScore getScore() {
7979
return score;
8080
}
8181

core/src/test/java/ai/timefold/solver/core/testdomain/score/TestdataHardMediumSoftBigDecimalScoreSolution.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public void setEntityList(List<TestdataEntity> entityList) {
7777
}
7878

7979
@PlanningScore
80-
HardMediumSoftBigDecimalScore getScore() {
80+
public HardMediumSoftBigDecimalScore getScore() {
8181
return score;
8282
}
8383

core/src/test/java/ai/timefold/solver/core/testdomain/score/TestdataHardMediumSoftLongScoreSolution.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ public void setEntityList(List<TestdataEntity> entityList) {
7575
}
7676

7777
@PlanningScore
78-
HardMediumSoftLongScore getScore() {
78+
public HardMediumSoftLongScore getScore() {
7979
return score;
8080
}
8181

core/src/test/java/ai/timefold/solver/core/testdomain/score/TestdataHardMediumSoftScoreSolution.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ public void setEntityList(List<TestdataEntity> entityList) {
7575
}
7676

7777
@PlanningScore
78-
HardMediumSoftScore getScore() {
78+
public HardMediumSoftScore getScore() {
7979
return score;
8080
}
8181

core/src/test/java/ai/timefold/solver/core/testdomain/score/TestdataHardSoftBigDecimalScoreSolution.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ public void setEntityList(List<TestdataEntity> entityList) {
7575
}
7676

7777
@PlanningScore
78-
HardSoftBigDecimalScore getScore() {
78+
public HardSoftBigDecimalScore getScore() {
7979
return score;
8080
}
8181

core/src/test/java/ai/timefold/solver/core/testdomain/score/TestdataHardSoftLongScoreSolution.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ public void setEntityList(List<TestdataEntity> entityList) {
7575
}
7676

7777
@PlanningScore
78-
HardSoftLongScore getScore() {
78+
public HardSoftLongScore getScore() {
7979
return score;
8080
}
8181

core/src/test/java/ai/timefold/solver/core/testdomain/score/TestdataHardSoftScoreSolution.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ public void setEntityList(List<TestdataEntity> entityList) {
7575
}
7676

7777
@PlanningScore
78-
HardSoftScore getScore() {
78+
public HardSoftScore getScore() {
7979
return score;
8080
}
8181

quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorGeneratedGizmoSupplierTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,9 @@ public interface DummyInterfaceEntity {
107107
public abstract static class DummyAbstractEntity {
108108
@ShadowVariable(variableListenerClass = DummyVariableListener.class,
109109
sourceEntityClass = TestdataEntity.class, sourceVariableName = "value")
110-
abstract Integer getLength();
110+
public abstract Integer getLength();
111111

112-
abstract void setLength(Integer length);
112+
public abstract void setLength(Integer length);
113113
}
114114

115115
public static class DummySolutionPartitioner implements SolutionPartitioner<TestdataSolution> {

quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/testdomain/gizmo/TestDataKitchenSinkEntity.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public class TestDataKitchenSinkEntity {
5252
private boolean isPinned;
5353

5454
@PlanningVariable(valueRangeProviderRefs = { "ints" })
55-
private Integer getIntVariable() {
55+
public Integer getIntVariable() {
5656
return intVariable;
5757
}
5858

0 commit comments

Comments
 (0)