Skip to content

Commit 3be2031

Browse files
chore: do not enforce non-final read-only fields have setters
Additionally, the code for GIZMO to use getters/setters is now done in core.
1 parent 7287fac commit 3be2031

File tree

21 files changed

+78
-118
lines changed

21 files changed

+78
-118
lines changed

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

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,11 @@ public static MemberAccessor buildMemberAccessor(Member member, MemberAccessorTy
6262
private static MemberAccessor buildReflectiveMemberAccessor(Member member, MemberAccessorType memberAccessorType,
6363
Class<? extends Annotation> annotationClass) {
6464
return buildReflectiveMemberAccessor(member, memberAccessorType, annotationClass,
65-
(AnnotatedElement) member, memberAccessorType == MemberAccessorType.FIELD_OR_GETTER_METHOD_WITH_SETTER);
65+
(AnnotatedElement) member);
6666
}
6767

6868
private static MemberAccessor buildReflectiveMemberAccessor(Member member, MemberAccessorType memberAccessorType,
69-
Class<? extends Annotation> annotationClass, AnnotatedElement annotatedElement, boolean requireSetter) {
69+
Class<? extends Annotation> annotationClass, AnnotatedElement annotatedElement) {
7070
var messagePrefix = (annotationClass == null) ? "The" : "The @%s annotated".formatted(annotationClass.getSimpleName());
7171
if (member instanceof Field field) {
7272
var getter = ReflectionHelper.getGetterMethod(field.getDeclaringClass(), field.getName());
@@ -77,7 +77,7 @@ private static MemberAccessor buildReflectiveMemberAccessor(Member member, Membe
7777
.formatted(messagePrefix, field.getName(), field.getDeclaringClass().getCanonicalName(), setter));
7878
}
7979

80-
if (Modifier.isFinal(field.getModifiers()) && requireSetter) {
80+
if (Modifier.isFinal(field.getModifiers()) && memberAccessorType.isSetterRequired()) {
8181
throw new IllegalArgumentException("%s field (%s) on class (%s) is final but requires a setter."
8282
.formatted(messagePrefix, field.getName(), field.getDeclaringClass().getCanonicalName()));
8383
}
@@ -93,7 +93,7 @@ private static MemberAccessor buildReflectiveMemberAccessor(Member member, Membe
9393
// Final fields may only have a getter
9494
// Non-final fields MUST have both a getter and setter
9595
return buildReflectiveMemberAccessor(getter, memberAccessorType, annotationClass,
96-
field, requireSetter || !Modifier.isFinal(field.getModifiers()));
96+
field);
9797
} else if (member instanceof Method method) {
9898
MemberAccessor memberAccessor;
9999
if (!Modifier.isPublic(method.getModifiers())) {
@@ -117,7 +117,7 @@ private static MemberAccessor buildReflectiveMemberAccessor(Member member, Membe
117117
}
118118
// Intentionally fall through (no break)
119119
case FIELD_OR_GETTER_METHOD, FIELD_OR_GETTER_METHOD_WITH_SETTER:
120-
boolean getterOnly = !requireSetter;
120+
boolean getterOnly = !memberAccessorType.isSetterRequired();
121121
if (annotationClass == null) {
122122
ReflectionHelper.assertGetterMethod(method);
123123
} else {
@@ -210,7 +210,21 @@ public enum MemberAccessorType {
210210
FIELD_OR_READ_METHOD,
211211
FIELD_OR_READ_METHOD_WITH_OPTIONAL_PARAMETER,
212212
FIELD_OR_GETTER_METHOD,
213-
FIELD_OR_GETTER_METHOD_WITH_SETTER,
214-
VOID_METHOD
213+
FIELD_OR_GETTER_METHOD_WITH_SETTER(true),
214+
VOID_METHOD;
215+
216+
private final boolean setterRequired;
217+
218+
MemberAccessorType() {
219+
setterRequired = false;
220+
}
221+
222+
MemberAccessorType(boolean setterRequired) {
223+
this.setterRequired = setterRequired;
224+
}
225+
226+
public boolean isSetterRequired() {
227+
return setterRequired;
228+
}
215229
}
216230
}

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

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,54 @@
11
package ai.timefold.solver.core.impl.domain.common.accessor.gizmo;
22

3+
import java.lang.reflect.Modifier;
34
import java.lang.reflect.Type;
45
import java.util.function.Consumer;
56

7+
import ai.timefold.solver.core.impl.domain.common.ReflectionHelper;
8+
9+
import org.jspecify.annotations.NullMarked;
10+
import org.jspecify.annotations.Nullable;
11+
612
import io.quarkus.gizmo2.Expr;
713
import io.quarkus.gizmo2.creator.BlockCreator;
814
import io.quarkus.gizmo2.desc.FieldDesc;
915
import io.quarkus.gizmo2.desc.MethodDesc;
1016

17+
@NullMarked
1118
final class GizmoFieldHandler implements GizmoMemberHandler {
1219

1320
private final Class<?> declaringClass;
1421
private final FieldDesc fieldDescriptor;
22+
private final @Nullable MethodDesc getterDescriptor;
23+
private final @Nullable MethodDesc setterDescriptor;
1524
private final boolean canBeWritten;
1625

1726
GizmoFieldHandler(Class<?> declaringClass, FieldDesc fieldDescriptor, boolean canBeWritten) {
1827
this.declaringClass = declaringClass;
1928
this.fieldDescriptor = fieldDescriptor;
20-
this.canBeWritten = canBeWritten;
29+
var getterMethod = ReflectionHelper.getGetterMethod(declaringClass, fieldDescriptor.name());
30+
var setterMethod = ReflectionHelper.getSetterMethod(declaringClass, fieldDescriptor.name());
31+
32+
if (getterMethod == null) {
33+
if (setterMethod != null) {
34+
throw new IllegalArgumentException("Field (%s) in class (%s) is a write-only field."
35+
.formatted(fieldDescriptor.name(), declaringClass.getName()));
36+
}
37+
getterDescriptor = null;
38+
setterDescriptor = null;
39+
this.canBeWritten = canBeWritten;
40+
} else {
41+
ReflectionHelper.assertGetterMethod(getterMethod);
42+
getterDescriptor = MethodDesc.of(getterMethod);
43+
44+
if (setterMethod != null && Modifier.isPublic(setterMethod.getModifiers())) {
45+
setterDescriptor = MethodDesc.of(setterMethod);
46+
this.canBeWritten = true;
47+
} else {
48+
setterDescriptor = null;
49+
this.canBeWritten = false;
50+
}
51+
}
2152
}
2253

2354
@Override
@@ -32,6 +63,9 @@ public void whenIsMethod(Consumer<MethodDesc> methodDescriptorConsumer) {
3263

3364
@Override
3465
public Expr readMemberValue(BlockCreator bytecodeCreator, Expr thisObj) {
66+
if (getterDescriptor != null) {
67+
return bytecodeCreator.invokeVirtual(getterDescriptor, thisObj);
68+
}
3569
return thisObj.field(fieldDescriptor);
3670
}
3771

@@ -44,6 +78,10 @@ public Expr readMemberValue(BlockCreator bytecodeCreator, Expr thisObj, Expr par
4478
public boolean writeMemberValue(MethodDesc setter, BlockCreator bytecodeCreator, Expr thisObj,
4579
Expr newValue) {
4680
if (canBeWritten) {
81+
if (setterDescriptor != null) {
82+
bytecodeCreator.invokeVirtual(setterDescriptor, thisObj, newValue);
83+
return true;
84+
}
4785
bytecodeCreator.set(thisObj.field(fieldDescriptor), newValue);
4886
return true;
4987
} else {

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -420,15 +420,15 @@ private static void createGetGetterMethodParameterType(GeneratedClassInfo genera
420420
/**
421421
* Generates the following code:
422422
* <p>
423-
* For a field
423+
* For a field without a getter
424424
*
425425
* <pre>
426426
* Object executeGetter(Object bean) {
427427
* return ((DeclaringClass) bean).field;
428428
* }
429429
* </pre>
430430
*
431-
* For a method with returning type
431+
* For a field with a getter or a method with returning type
432432
*
433433
* <pre>
434434
* Object executeGetter(Object bean) {

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
import ai.timefold.solver.core.api.domain.common.DomainAccessType;
1313
import ai.timefold.solver.core.impl.domain.common.ReflectionHelper;
1414

15+
import org.jspecify.annotations.NullMarked;
16+
import org.jspecify.annotations.Nullable;
17+
1518
import io.quarkus.gizmo2.Expr;
1619
import io.quarkus.gizmo2.creator.BlockCreator;
1720
import io.quarkus.gizmo2.desc.FieldDesc;
@@ -20,6 +23,7 @@
2023
/**
2124
* Describe and provide simplified/unified access for {@link Member}.
2225
*/
26+
@NullMarked
2327
public final class GizmoMemberDescriptor {
2428

2529
/**
@@ -34,6 +38,7 @@ public final class GizmoMemberDescriptor {
3438
/**
3539
* The type of the read method parameter. Null if the method does not accept a parameter.
3640
*/
41+
@Nullable
3742
private final Type methodParameterType;
3843

3944
private final GizmoMemberHandler memberHandler;
@@ -46,6 +51,7 @@ public final class GizmoMemberDescriptor {
4651
/**
4752
* The MethodDescriptor of the corresponding setter. Empty if not present.
4853
*/
54+
@Nullable
4955
private final MethodDesc setter;
5056

5157
public GizmoMemberDescriptor(Member member) {
@@ -91,7 +97,7 @@ public GizmoMemberDescriptor(String name, FieldDesc fieldDescriptor, Class<?> de
9197
}
9298

9399
public GizmoMemberDescriptor(String name, MethodDesc memberDescriptor, MethodDesc metadataDescriptor,
94-
Type methodParameterType, Class<?> declaringClass, MethodDesc setterDescriptor) {
100+
Type methodParameterType, Class<?> declaringClass, @Nullable MethodDesc setterDescriptor) {
95101
this.name = name;
96102
this.memberHandler = GizmoMemberHandler.of(declaringClass, (Class<?>) methodParameterType, memberDescriptor);
97103
this.metadataHandler = memberDescriptor == metadataDescriptor ? this.memberHandler
@@ -101,7 +107,7 @@ public GizmoMemberDescriptor(String name, MethodDesc memberDescriptor, MethodDes
101107
}
102108

103109
public GizmoMemberDescriptor(String name, MethodDesc memberDescriptor, Type methodParameterType, Class<?> declaringClass,
104-
MethodDesc setterDescriptor) {
110+
@Nullable MethodDesc setterDescriptor) {
105111
this.name = name;
106112
this.memberHandler = GizmoMemberHandler.of(declaringClass, (Class<?>) methodParameterType, memberDescriptor);
107113
this.metadataHandler = this.memberHandler;
@@ -110,14 +116,15 @@ public GizmoMemberDescriptor(String name, MethodDesc memberDescriptor, Type meth
110116
}
111117

112118
public GizmoMemberDescriptor(String name, MethodDesc memberDescriptor, FieldDesc metadataDescriptor,
113-
Type methodParameterType, Class<?> declaringClass, MethodDesc setterDescriptor) {
119+
@Nullable Type methodParameterType, Class<?> declaringClass, @Nullable MethodDesc setterDescriptor) {
114120
this.name = name;
115121
this.memberHandler = GizmoMemberHandler.of(declaringClass, (Class<?>) methodParameterType, memberDescriptor);
116122
this.metadataHandler = GizmoMemberHandler.of(declaringClass, name, metadataDescriptor, true);
117123
this.methodParameterType = methodParameterType;
118124
this.setter = setterDescriptor;
119125
}
120126

127+
@Nullable
121128
public static Type getMethodParameterType(Method method, boolean methodWithParameter) {
122129
var parameterCount = method.getParameterCount();
123130
Type methodParameterType = null;
@@ -244,6 +251,7 @@ public Type getType() {
244251
return metadataHandler.getType();
245252
}
246253

254+
@Nullable
247255
public Type getMethodParameterType() {
248256
return methodParameterType;
249257
}

core/src/test/java/ai/timefold/solver/core/testdomain/inheritance/entity/multiple/baseannotated/classes/childnot/TestdataMultipleChildNotAnnotatedBaseEntity.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,6 @@ public Long getId() {
2424
return id;
2525
}
2626

27-
public void setId(Long id) {
28-
this.id = id;
29-
}
30-
3127
public String getValue() {
3228
return value;
3329
}

core/src/test/java/ai/timefold/solver/core/testdomain/inheritance/entity/multiple/baseannotated/classes/childtoo/TestdataMultipleBothAnnotatedBaseEntity.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,6 @@ public Long getId() {
2424
return id;
2525
}
2626

27-
public void setId(Long id) {
28-
this.id = id;
29-
}
30-
3127
public String getValue() {
3228
return value;
3329
}

core/src/test/java/ai/timefold/solver/core/testdomain/inheritance/entity/single/baseannotated/classes/addvar/TestdataAddVarBaseEntity.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,6 @@ public Long getId() {
2424
return id;
2525
}
2626

27-
public void setId(Long id) {
28-
this.id = id;
29-
}
30-
3127
public String getValue() {
3228
return value;
3329
}

core/src/test/java/ai/timefold/solver/core/testdomain/inheritance/entity/single/baseannotated/classes/childnot/TestdataChildNotAnnotatedBaseEntity.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,6 @@ public Long getId() {
2424
return id;
2525
}
2626

27-
public void setId(Long id) {
28-
this.id = id;
29-
}
30-
3127
public String getValue() {
3228
return value;
3329
}

core/src/test/java/ai/timefold/solver/core/testdomain/inheritance/entity/single/basenot/classes/TestdataBaseNotAnnotatedBaseEntity.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,6 @@ public Long getId() {
2222
return id;
2323
}
2424

25-
public void setId(Long id) {
26-
this.id = id;
27-
}
28-
2925
public String getValue() {
3026
return value;
3127
}

core/src/test/java/ai/timefold/solver/core/testdomain/inheritance/solution/baseannotated/childtooabstract/TestdataBothAnnotatedAbstractBaseEntity.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,6 @@ public Long getId() {
2424
return id;
2525
}
2626

27-
public void setId(Long id) {
28-
this.id = id;
29-
}
30-
3127
public String getValue() {
3228
return value;
3329
}

0 commit comments

Comments
 (0)