Skip to content

Commit 6f0603f

Browse files
Christopher-Chianellitriceo
authored andcommitted
fix: Support repeatable annotations like @ShadowVariable in Quarkus
Jandex, being Jandex, likes to fails sliently without telling you anything. The culprit it this case is IndexView::getAnnotations. IndexView::getAnnotations will appear to work, returning a collection of AnnotationInstances. But it will sliently fail and return an empty list when the annotation repeats on a target. The fix is to use IndexView::getAnnotationsWithRepeatable instead. Also did some cleanup in the generated Gizmo code, removing meaningless try blocks, and not doing reflection to generate the method descriptors.
1 parent 3c01f3f commit 6f0603f

File tree

7 files changed

+285
-50
lines changed

7 files changed

+285
-50
lines changed

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

Lines changed: 33 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import io.quarkus.gizmo.MethodCreator;
2121
import io.quarkus.gizmo.MethodDescriptor;
2222
import io.quarkus.gizmo.ResultHandle;
23-
import io.quarkus.gizmo.TryBlock;
2423

2524
/**
2625
* Generates the bytecode for the MemberAccessor of a particular Member
@@ -126,13 +125,9 @@ private static MemberAccessor createInstance(String className, GizmoClassLoader
126125
// MemberAccessor methods
127126
// ************************************************************************
128127

129-
private static MethodCreator getMethodCreator(ClassCreator classCreator, String methodName, Class<?>... parameters) {
130-
try {
131-
return classCreator.getMethodCreator(
132-
MethodDescriptor.ofMethod(MemberAccessor.class.getMethod(methodName, parameters)));
133-
} catch (NoSuchMethodException e) {
134-
throw new IllegalStateException("No such method: " + methodName, e);
135-
}
128+
private static MethodCreator getMethodCreator(ClassCreator classCreator, Class<?> returnType, String methodName,
129+
Class<?>... parameters) {
130+
return classCreator.getMethodCreator(methodName, returnType, parameters);
136131
}
137132

138133
private static void createConstructor(ClassCreator classCreator, GizmoMemberInfo memberInfo) {
@@ -146,43 +141,35 @@ private static void createConstructor(ClassCreator classCreator, GizmoMemberInfo
146141

147142
ResultHandle declaringClass = methodCreator.loadClass(memberInfo.getDescriptor().getDeclaringClassName());
148143
memberInfo.getDescriptor().whenMetadataIsOnField(fd -> {
149-
TryBlock tryBlock = methodCreator.tryBlock();
150-
ResultHandle name = tryBlock.load(fd.getName());
151-
ResultHandle field = tryBlock.invokeVirtualMethod(MethodDescriptor.ofMethod(Class.class, "getDeclaredField",
144+
ResultHandle name = methodCreator.load(fd.getName());
145+
ResultHandle field = methodCreator.invokeVirtualMethod(MethodDescriptor.ofMethod(Class.class, "getDeclaredField",
152146
Field.class, String.class),
153147
declaringClass, name);
154148
ResultHandle type =
155-
tryBlock.invokeVirtualMethod(MethodDescriptor.ofMethod(Field.class, "getGenericType", Type.class),
149+
methodCreator.invokeVirtualMethod(MethodDescriptor.ofMethod(Field.class, "getGenericType", Type.class),
156150
field);
157-
tryBlock.writeInstanceField(FieldDescriptor.of(classCreator.getClassName(), GENERIC_TYPE_FIELD, Type.class),
151+
methodCreator.writeInstanceField(FieldDescriptor.of(classCreator.getClassName(), GENERIC_TYPE_FIELD, Type.class),
158152
thisObj, type);
159-
tryBlock.writeInstanceField(
153+
methodCreator.writeInstanceField(
160154
FieldDescriptor.of(classCreator.getClassName(), ANNOTATED_ELEMENT_FIELD, AnnotatedElement.class),
161155
thisObj, field);
162-
163-
tryBlock.addCatch(NoSuchFieldException.class).throwException(IllegalStateException.class, "Unable to find field (" +
164-
fd.getName() + ") in class (" + fd.getDeclaringClass() + ").");
165156
});
166157

167158
memberInfo.getDescriptor().whenMetadataIsOnMethod(md -> {
168-
TryBlock tryBlock = methodCreator.tryBlock();
169-
ResultHandle name = tryBlock.load(md.getName());
170-
ResultHandle method = tryBlock.invokeVirtualMethod(MethodDescriptor.ofMethod(Class.class, "getDeclaredMethod",
159+
ResultHandle name = methodCreator.load(md.getName());
160+
ResultHandle method = methodCreator.invokeVirtualMethod(MethodDescriptor.ofMethod(Class.class, "getDeclaredMethod",
171161
Method.class, String.class, Class[].class),
172162
declaringClass, name,
173-
tryBlock.newArray(Class.class, 0));
163+
methodCreator.newArray(Class.class, 0));
174164
ResultHandle type =
175-
tryBlock.invokeVirtualMethod(MethodDescriptor.ofMethod(Method.class, "getGenericReturnType", Type.class),
165+
methodCreator.invokeVirtualMethod(
166+
MethodDescriptor.ofMethod(Method.class, "getGenericReturnType", Type.class),
176167
method);
177-
tryBlock.writeInstanceField(FieldDescriptor.of(classCreator.getClassName(), GENERIC_TYPE_FIELD, Type.class),
168+
methodCreator.writeInstanceField(FieldDescriptor.of(classCreator.getClassName(), GENERIC_TYPE_FIELD, Type.class),
178169
thisObj, type);
179-
tryBlock.writeInstanceField(
170+
methodCreator.writeInstanceField(
180171
FieldDescriptor.of(classCreator.getClassName(), ANNOTATED_ELEMENT_FIELD, AnnotatedElement.class),
181172
thisObj, method);
182-
183-
tryBlock.addCatch(NoSuchMethodException.class).throwException(IllegalStateException.class,
184-
"Unable to find method (" +
185-
md.getName() + ") in class (" + md.getDeclaringClass() + ").");
186173
});
187174

188175
// Return this (it a constructor)
@@ -199,7 +186,7 @@ private static void createConstructor(ClassCreator classCreator, GizmoMemberInfo
199186
* </pre>
200187
*/
201188
private static void createGetDeclaringClass(ClassCreator classCreator, GizmoMemberInfo memberInfo) {
202-
MethodCreator methodCreator = getMethodCreator(classCreator, "getDeclaringClass");
189+
MethodCreator methodCreator = getMethodCreator(classCreator, Class.class, "getDeclaringClass");
203190
ResultHandle out = methodCreator.loadClass(memberInfo.getDescriptor().getDeclaringClassName());
204191
methodCreator.returnValue(out);
205192
}
@@ -255,7 +242,7 @@ private static void assertIsGoodMethod(MethodDescriptor method, Class<? extends
255242
* letter become lowercase
256243
*/
257244
private static void createGetName(ClassCreator classCreator, GizmoMemberInfo memberInfo) {
258-
MethodCreator methodCreator = getMethodCreator(classCreator, "getName");
245+
MethodCreator methodCreator = getMethodCreator(classCreator, String.class, "getName");
259246

260247
// If it is a method, assert that it has the required
261248
// properties
@@ -278,7 +265,7 @@ private static void createGetName(ClassCreator classCreator, GizmoMemberInfo mem
278265
* </pre>
279266
*/
280267
private static void createGetType(ClassCreator classCreator, GizmoMemberInfo memberInfo) {
281-
MethodCreator methodCreator = getMethodCreator(classCreator, "getType");
268+
MethodCreator methodCreator = getMethodCreator(classCreator, Class.class, "getType");
282269
ResultHandle out = methodCreator.loadClass(memberInfo.getDescriptor().getTypeName());
283270
methodCreator.returnValue(out);
284271
}
@@ -297,7 +284,7 @@ private static void createGetType(ClassCreator classCreator, GizmoMemberInfo mem
297284
* is stored in gizmoMemberAccessorNameToGenericType when this method is called.
298285
*/
299286
private static void createGetGenericType(ClassCreator classCreator) {
300-
MethodCreator methodCreator = getMethodCreator(classCreator, "getGenericType");
287+
MethodCreator methodCreator = getMethodCreator(classCreator, Type.class, "getGenericType");
301288
ResultHandle thisObj = methodCreator.getThis();
302289

303290
ResultHandle out =
@@ -331,7 +318,7 @@ private static void createGetGenericType(ClassCreator classCreator) {
331318
* member if it is private (which get passed to the MemberDescriptor).
332319
*/
333320
private static void createExecuteGetter(ClassCreator classCreator, GizmoMemberInfo memberInfo) {
334-
MethodCreator methodCreator = getMethodCreator(classCreator, "executeGetter", Object.class);
321+
MethodCreator methodCreator = getMethodCreator(classCreator, Object.class, "executeGetter", Object.class);
335322
ResultHandle bean = methodCreator.getMethodParam(0);
336323
methodCreator.returnValue(memberInfo.getDescriptor().readMemberValue(methodCreator, bean));
337324
}
@@ -364,7 +351,7 @@ private static void createExecuteGetter(ClassCreator classCreator, GizmoMemberIn
364351
* </pre>
365352
*/
366353
private static void createExecuteSetter(ClassCreator classCreator, GizmoMemberInfo memberInfo) {
367-
MethodCreator methodCreator = getMethodCreator(classCreator, "executeSetter", Object.class,
354+
MethodCreator methodCreator = getMethodCreator(classCreator, void.class, "executeSetter", Object.class,
368355
Object.class);
369356

370357
ResultHandle bean = methodCreator.getMethodParam(0);
@@ -377,17 +364,13 @@ private static void createExecuteSetter(ClassCreator classCreator, GizmoMemberIn
377364
}
378365
}
379366

380-
private static MethodCreator getAnnotationMethodCreator(ClassCreator classCreator, String methodName,
367+
private static MethodCreator getAnnotationMethodCreator(ClassCreator classCreator, Class<?> returnType, String methodName,
381368
Class<?>... parameters) {
382-
return classCreator.getMethodCreator(getAnnotationMethod(methodName, parameters));
369+
return classCreator.getMethodCreator(getAnnotationMethod(returnType, methodName, parameters));
383370
}
384371

385-
private static MethodDescriptor getAnnotationMethod(String methodName, Class<?>... parameters) {
386-
try {
387-
return MethodDescriptor.ofMethod(AnnotatedElement.class.getMethod(methodName, parameters));
388-
} catch (NoSuchMethodException e) {
389-
throw new IllegalStateException("No such method: " + methodName, e);
390-
}
372+
private static MethodDescriptor getAnnotationMethod(Class<?> returnType, String methodName, Class<?>... parameters) {
373+
return MethodDescriptor.ofMethod(AnnotatedElement.class, methodName, returnType, parameters);
391374
}
392375

393376
/**
@@ -402,27 +385,30 @@ private static MethodDescriptor getAnnotationMethod(String methodName, Class<?>.
402385
* </pre>
403386
*/
404387
private static void createGetAnnotation(ClassCreator classCreator) {
405-
MethodCreator methodCreator = getAnnotationMethodCreator(classCreator, "getAnnotation", Class.class);
388+
MethodCreator methodCreator = getAnnotationMethodCreator(classCreator, Annotation.class, "getAnnotation", Class.class);
406389
ResultHandle thisObj = methodCreator.getThis();
407390

408391
ResultHandle annotatedElement = methodCreator.readInstanceField(
409392
FieldDescriptor.of(classCreator.getClassName(), ANNOTATED_ELEMENT_FIELD, AnnotatedElement.class),
410393
thisObj);
411394
ResultHandle query = methodCreator.getMethodParam(0);
412-
ResultHandle out = methodCreator.invokeInterfaceMethod(getAnnotationMethod("getAnnotation", Class.class),
413-
annotatedElement, query);
395+
ResultHandle out =
396+
methodCreator.invokeInterfaceMethod(getAnnotationMethod(Annotation.class, "getAnnotation", Class.class),
397+
annotatedElement, query);
414398
methodCreator.returnValue(out);
415399
}
416400

417401
private static void createDeclaredAnnotationsByType(ClassCreator classCreator) {
418-
MethodCreator methodCreator = getAnnotationMethodCreator(classCreator, "getDeclaredAnnotationsByType", Class.class);
402+
MethodCreator methodCreator =
403+
getAnnotationMethodCreator(classCreator, Annotation[].class, "getDeclaredAnnotationsByType", Class.class);
419404
ResultHandle thisObj = methodCreator.getThis();
420405

421406
ResultHandle annotatedElement = methodCreator.readInstanceField(
422407
FieldDescriptor.of(classCreator.getClassName(), ANNOTATED_ELEMENT_FIELD, AnnotatedElement.class),
423408
thisObj);
424409
ResultHandle query = methodCreator.getMethodParam(0);
425-
ResultHandle out = methodCreator.invokeInterfaceMethod(getAnnotationMethod("getDeclaredAnnotationsByType", Class.class),
410+
ResultHandle out = methodCreator.invokeInterfaceMethod(
411+
getAnnotationMethod(Annotation[].class, "getDeclaredAnnotationsByType", Class.class),
426412
annotatedElement, query);
427413
methodCreator.returnValue(out);
428414
}

quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/TimefoldProcessor.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,7 @@ private void assertNoMemberAnnotationWithoutClassAnnotation(IndexView indexView)
405405
Collection<AnnotationInstance> timefoldFieldAnnotations = new HashSet<>();
406406

407407
for (DotName annotationName : DotNames.PLANNING_ENTITY_FIELD_ANNOTATIONS) {
408-
timefoldFieldAnnotations.addAll(indexView.getAnnotations(annotationName));
408+
timefoldFieldAnnotations.addAll(indexView.getAnnotationsWithRepeatable(annotationName, indexView));
409409
}
410410

411411
for (AnnotationInstance annotationInstance : timefoldFieldAnnotations) {
@@ -444,7 +444,7 @@ private void assertNoMemberAnnotationWithoutClassAnnotation(IndexView indexView)
444444
private void registerClassesFromAnnotations(IndexView indexView, Set<Class<?>> reflectiveClassSet) {
445445
for (DotNames.BeanDefiningAnnotations beanDefiningAnnotation : DotNames.BeanDefiningAnnotations.values()) {
446446
for (AnnotationInstance annotationInstance : indexView
447-
.getAnnotations(beanDefiningAnnotation.getAnnotationDotName())) {
447+
.getAnnotationsWithRepeatable(beanDefiningAnnotation.getAnnotationDotName(), indexView)) {
448448
for (String parameterName : beanDefiningAnnotation.getParameterNames()) {
449449
AnnotationValue value = annotationInstance.value(parameterName);
450450

@@ -559,7 +559,7 @@ private GeneratedGizmoClasses generateDomainAccessors(SolverConfig solverConfig,
559559
// Every entity and solution gets scanned for annotations.
560560
// Annotated members get their accessors generated.
561561
for (DotName dotName : DotNames.GIZMO_MEMBER_ACCESSOR_ANNOTATIONS) {
562-
membersToGeneratedAccessorsFor.addAll(indexView.getAnnotations(dotName));
562+
membersToGeneratedAccessorsFor.addAll(indexView.getAnnotationsWithRepeatable(dotName, indexView));
563563
}
564564
membersToGeneratedAccessorsFor.removeIf(this::shouldIgnoreMember);
565565

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package ai.timefold.solver.quarkus;
2+
3+
import static org.junit.jupiter.api.Assertions.assertNotNull;
4+
import static org.junit.jupiter.api.Assertions.assertSame;
5+
import static org.junit.jupiter.api.Assertions.assertTrue;
6+
7+
import java.util.concurrent.ExecutionException;
8+
import java.util.stream.Collectors;
9+
import java.util.stream.IntStream;
10+
11+
import jakarta.inject.Inject;
12+
13+
import ai.timefold.solver.core.api.score.ScoreManager;
14+
import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore;
15+
import ai.timefold.solver.core.api.solver.SolutionManager;
16+
import ai.timefold.solver.core.api.solver.SolverFactory;
17+
import ai.timefold.solver.core.api.solver.SolverJob;
18+
import ai.timefold.solver.core.api.solver.SolverManager;
19+
import ai.timefold.solver.core.impl.solver.DefaultSolutionManager;
20+
import ai.timefold.solver.core.impl.solver.DefaultSolverFactory;
21+
import ai.timefold.solver.core.impl.solver.DefaultSolverManager;
22+
import ai.timefold.solver.quarkus.testdata.shadowvariable.constraints.TestdataQuarkusShadowVariableConstraintProvider;
23+
import ai.timefold.solver.quarkus.testdata.shadowvariable.domain.TestdataQuarkusShadowVariableEntity;
24+
import ai.timefold.solver.quarkus.testdata.shadowvariable.domain.TestdataQuarkusShadowVariableListener;
25+
import ai.timefold.solver.quarkus.testdata.shadowvariable.domain.TestdataQuarkusShadowVariableSolution;
26+
27+
import org.jboss.shrinkwrap.api.ShrinkWrap;
28+
import org.jboss.shrinkwrap.api.spec.JavaArchive;
29+
import org.junit.jupiter.api.Test;
30+
import org.junit.jupiter.api.extension.RegisterExtension;
31+
32+
import io.quarkus.test.QuarkusUnitTest;
33+
34+
class TimefoldProcessorShadowVariableSolveTest {
35+
36+
@RegisterExtension
37+
static final QuarkusUnitTest config = new QuarkusUnitTest()
38+
.overrideConfigKey("quarkus.timefold.solver.termination.best-score-limit", "0")
39+
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
40+
.addClasses(TestdataQuarkusShadowVariableEntity.class,
41+
TestdataQuarkusShadowVariableSolution.class,
42+
TestdataQuarkusShadowVariableConstraintProvider.class,
43+
TestdataQuarkusShadowVariableListener.class));
44+
45+
@Inject
46+
SolverFactory<TestdataQuarkusShadowVariableSolution> solverFactory;
47+
@Inject
48+
SolverManager<TestdataQuarkusShadowVariableSolution, Long> solverManager;
49+
@Inject
50+
ScoreManager<TestdataQuarkusShadowVariableSolution, SimpleScore> scoreManager;
51+
@Inject
52+
SolutionManager<TestdataQuarkusShadowVariableSolution, SimpleScore> solutionManager;
53+
54+
@Test
55+
void singletonSolverFactory() {
56+
assertNotNull(solverFactory);
57+
assertSame(((DefaultSolverFactory<TestdataQuarkusShadowVariableSolution>) solverFactory).getScoreDirectorFactory(),
58+
((DefaultSolutionManager<TestdataQuarkusShadowVariableSolution, SimpleScore>) solutionManager)
59+
.getScoreDirectorFactory());
60+
assertNotNull(solverManager);
61+
// There is only one SolverFactory instance
62+
assertSame(solverFactory,
63+
((DefaultSolverManager<TestdataQuarkusShadowVariableSolution, Long>) solverManager).getSolverFactory());
64+
assertNotNull(solutionManager);
65+
}
66+
67+
@Test
68+
void solve() throws ExecutionException, InterruptedException {
69+
TestdataQuarkusShadowVariableSolution problem = new TestdataQuarkusShadowVariableSolution();
70+
problem.setValueList(IntStream.range(1, 3)
71+
.mapToObj(i -> "v" + i)
72+
.collect(Collectors.toList()));
73+
problem.setEntityList(IntStream.range(1, 3)
74+
.mapToObj(i -> new TestdataQuarkusShadowVariableEntity())
75+
.collect(Collectors.toList()));
76+
SolverJob<TestdataQuarkusShadowVariableSolution, Long> solverJob = solverManager.solve(1L, problem);
77+
TestdataQuarkusShadowVariableSolution solution = solverJob.getFinalBestSolution();
78+
assertNotNull(solution);
79+
assertTrue(solution.getScore().score() >= 0);
80+
}
81+
82+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package ai.timefold.solver.quarkus.testdata.shadowvariable.constraints;
2+
3+
import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore;
4+
import ai.timefold.solver.core.api.score.stream.Constraint;
5+
import ai.timefold.solver.core.api.score.stream.ConstraintFactory;
6+
import ai.timefold.solver.core.api.score.stream.ConstraintProvider;
7+
import ai.timefold.solver.core.api.score.stream.Joiners;
8+
import ai.timefold.solver.quarkus.testdata.shadowvariable.domain.TestdataQuarkusShadowVariableEntity;
9+
10+
public class TestdataQuarkusShadowVariableConstraintProvider implements ConstraintProvider {
11+
12+
@Override
13+
public Constraint[] defineConstraints(ConstraintFactory factory) {
14+
return new Constraint[] {
15+
factory.forEach(TestdataQuarkusShadowVariableEntity.class)
16+
.join(TestdataQuarkusShadowVariableEntity.class,
17+
Joiners.equal(TestdataQuarkusShadowVariableEntity::getValue1,
18+
TestdataQuarkusShadowVariableEntity::getValue2))
19+
.filter((a, b) -> a.getValue1AndValue2().equals(b.getValue1AndValue2()))
20+
.penalize(SimpleScore.ONE)
21+
.asConstraint("Don't assign 2 entities the same value.")
22+
};
23+
}
24+
25+
}

0 commit comments

Comments
 (0)