Skip to content

Commit 58e14aa

Browse files
authored
feat: expose ConstraintMetaModel at build time (#1343)
Fixes #1323.
1 parent 9f53d4a commit 58e14aa

File tree

9 files changed

+124
-11
lines changed

9 files changed

+124
-11
lines changed

quarkus-integration/quarkus/deployment/pom.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,16 @@
101101
</execution>
102102
</executions>
103103
</plugin>
104+
<plugin>
105+
<groupId>org.revapi</groupId>
106+
<artifactId>revapi-maven-plugin</artifactId>
107+
<configuration>
108+
<!-- Introduction of API package to check -->
109+
<oldArtifacts>
110+
<artifact>${project.groupId}:timefold-solver-quarkus-deployment:1.18.0</artifact>
111+
</oldArtifacts>
112+
</configuration>
113+
</plugin>
104114
<plugin>
105115
<artifactId>maven-surefire-plugin</artifactId>
106116
<configuration>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[
2+
{
3+
"extension": "revapi.differences",
4+
"configuration": {
5+
"differences": [
6+
]
7+
}
8+
}
9+
]
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[
2+
{
3+
"extension": "revapi.filter",
4+
"configuration": {
5+
"elements": {
6+
"include": [
7+
"class ai\\.timefold\\.solver\\.quarkus\\.deployment\\.api.*"
8+
]
9+
}
10+
}
11+
}
12+
]

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ public final class SolverConfigBuildItem extends SimpleBuildItem {
1515
* Constructor for multiple solver configurations.
1616
*/
1717
public SolverConfigBuildItem(Map<String, SolverConfig> solverConfig, GeneratedGizmoClasses generatedGizmoClasses) {
18-
this.solverConfigurations = solverConfig;
18+
// Defensive copy to avoid changing the map in dependent build items.
19+
this.solverConfigurations = Map.copyOf(solverConfig);
1920
this.generatedGizmoClasses = generatedGizmoClasses;
2021
}
2122

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import ai.timefold.solver.core.api.domain.solution.ProblemFactCollectionProperty;
3131
import ai.timefold.solver.core.api.score.calculator.EasyScoreCalculator;
3232
import ai.timefold.solver.core.api.score.calculator.IncrementalScoreCalculator;
33+
import ai.timefold.solver.core.api.score.stream.ConstraintMetaModel;
3334
import ai.timefold.solver.core.api.score.stream.ConstraintProvider;
3435
import ai.timefold.solver.core.api.solver.SolverFactory;
3536
import ai.timefold.solver.core.api.solver.SolverManager;
@@ -40,10 +41,12 @@
4041
import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor;
4142
import ai.timefold.solver.core.impl.heuristic.selector.common.nearby.NearbyDistanceMeter;
4243
import ai.timefold.solver.quarkus.TimefoldRecorder;
44+
import ai.timefold.solver.quarkus.bean.BeanUtil;
4345
import ai.timefold.solver.quarkus.bean.DefaultTimefoldBeanProvider;
4446
import ai.timefold.solver.quarkus.bean.TimefoldSolverBannerBean;
4547
import ai.timefold.solver.quarkus.bean.UnavailableTimefoldBeanProvider;
4648
import ai.timefold.solver.quarkus.config.TimefoldRuntimeConfig;
49+
import ai.timefold.solver.quarkus.deployment.api.ConstraintMetaModelBuildItem;
4750
import ai.timefold.solver.quarkus.deployment.config.SolverBuildTimeConfig;
4851
import ai.timefold.solver.quarkus.deployment.config.TimefoldBuildTimeConfig;
4952
import ai.timefold.solver.quarkus.devui.DevUISolverConfig;
@@ -573,6 +576,29 @@ private SolverConfig loadSolverConfig(IndexView indexView,
573576
return solverConfig;
574577
}
575578

579+
@BuildStep
580+
void buildConstraintMetaModel(SolverConfigBuildItem solverConfigBuildItem,
581+
BuildProducer<ConstraintMetaModelBuildItem> constraintMetaModelBuildItemBuildProducer) {
582+
if (solverConfigBuildItem.getSolverConfigMap().isEmpty()) {
583+
return;
584+
}
585+
586+
Map<String, ConstraintMetaModel> constraintMetaModelsBySolverNames = new HashMap<>();
587+
solverConfigBuildItem.getSolverConfigMap().forEach((solverName, solverConfig) -> {
588+
// Gizmo-generated member accessors are not yet available at build time.
589+
DomainAccessType originalDomainAccessType = solverConfig.getDomainAccessType();
590+
solverConfig.setDomainAccessType(DomainAccessType.REFLECTION);
591+
592+
var solverFactory = SolverFactory.create(solverConfig);
593+
ConstraintMetaModel constraintMetaModel = BeanUtil.buildConstraintMetaModel(solverFactory);
594+
// Avoid changing the original solver config.
595+
solverConfig.setDomainAccessType(originalDomainAccessType);
596+
constraintMetaModelsBySolverNames.put(solverName, constraintMetaModel);
597+
});
598+
599+
constraintMetaModelBuildItemBuildProducer.produce(new ConstraintMetaModelBuildItem(constraintMetaModelsBySolverNames));
600+
}
601+
576602
@BuildStep
577603
@Record(RUNTIME_INIT)
578604
void recordAndRegisterRuntimeBeans(TimefoldRecorder recorder, RecorderContext recorderContext,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package ai.timefold.solver.quarkus.deployment.api;
2+
3+
import java.util.Map;
4+
5+
import ai.timefold.solver.core.api.score.stream.ConstraintMetaModel;
6+
7+
import io.quarkus.builder.item.SimpleBuildItem;
8+
9+
/**
10+
* Represents a {@link ai.timefold.solver.core.api.score.stream.ConstraintMetaModel} at the build time for the purpose
11+
* of Quarkus augmentation.
12+
*/
13+
public final class ConstraintMetaModelBuildItem extends SimpleBuildItem {
14+
15+
private final Map<String, ConstraintMetaModel> constraintMetaModelsBySolverNames;
16+
17+
public ConstraintMetaModelBuildItem(Map<String, ConstraintMetaModel> constraintMetaModelsBySolverNames) {
18+
this.constraintMetaModelsBySolverNames = Map.copyOf(constraintMetaModelsBySolverNames);
19+
}
20+
21+
public Map<String, ConstraintMetaModel> constraintMetaModelsBySolverNames() {
22+
return constraintMetaModelsBySolverNames;
23+
}
24+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/**
2+
* Public {@link io.quarkus.builder.item.BuildItem}s consumable by other Quarkus extensions.
3+
* <p>
4+
* All classes in this package are part of the public API of this extension.
5+
* Moreover, the extension is responsible for creating instances of these classes during the build phase.
6+
*/
7+
package ai.timefold.solver.quarkus.deployment.api;
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package ai.timefold.solver.quarkus.bean;
2+
3+
import java.util.Objects;
4+
5+
import ai.timefold.solver.core.api.score.stream.ConstraintMetaModel;
6+
import ai.timefold.solver.core.api.solver.SolverFactory;
7+
import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraintStreamScoreDirectorFactory;
8+
import ai.timefold.solver.core.impl.solver.DefaultSolverFactory;
9+
10+
public class BeanUtil {
11+
12+
public static ConstraintMetaModel buildConstraintMetaModel(SolverFactory<?> solverFactory) {
13+
if (Objects.requireNonNull(solverFactory) instanceof DefaultSolverFactory<?> defaultSolverFactory) {
14+
var scoreDirectorFactory = defaultSolverFactory.getScoreDirectorFactory();
15+
if (scoreDirectorFactory instanceof AbstractConstraintStreamScoreDirectorFactory<?, ?> castScoreDirectorFactory) {
16+
return castScoreDirectorFactory.getConstraintMetaModel();
17+
} else {
18+
throw new IllegalStateException(
19+
"Cannot provide %s because the score director does not use the Constraint Streams API."
20+
.formatted(ConstraintMetaModel.class.getSimpleName()));
21+
}
22+
} else {
23+
throw new IllegalStateException(
24+
"%s is not supported by the solver factory (%s)."
25+
.formatted(ConstraintMetaModel.class.getSimpleName(), solverFactory.getClass().getName()));
26+
}
27+
}
28+
29+
private BeanUtil() {
30+
throw new IllegalStateException("Utility class");
31+
}
32+
}

quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/bean/DefaultTimefoldBeanProvider.java

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@
2424
import ai.timefold.solver.core.api.solver.SolverManager;
2525
import ai.timefold.solver.core.config.solver.SolverConfig;
2626
import ai.timefold.solver.core.config.solver.SolverManagerConfig;
27-
import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraintStreamScoreDirectorFactory;
28-
import ai.timefold.solver.core.impl.solver.DefaultSolverFactory;
2927

3028
import io.quarkus.arc.DefaultBean;
3129
import io.quarkus.arc.Lock;
@@ -63,14 +61,8 @@ <Solution_> SolverFactory<Solution_> solverFactory(SolverConfig solverConfig) {
6361
@Produces
6462
ConstraintMetaModel constraintProviderMetaModel(SolverFactory<?> solverFactory) {
6563
if (constraintMetaModel == null) {
66-
var scoreDirectorFactory = ((DefaultSolverFactory<?>) solverFactory).getScoreDirectorFactory();
67-
if (scoreDirectorFactory instanceof AbstractConstraintStreamScoreDirectorFactory<?, ?> castScoreDirectorFactory) {
68-
constraintMetaModel = castScoreDirectorFactory.getConstraintMetaModel();
69-
} else {
70-
throw new IllegalStateException(
71-
"Cannot provide %s because the score director does not use the Constraint Streams API."
72-
.formatted(ConstraintMetaModel.class.getSimpleName()));
73-
}
64+
// The metamodel is not compatible with Quarkus code recording, thus we need to rebuild it at runtime.
65+
constraintMetaModel = BeanUtil.buildConstraintMetaModel(solverFactory);
7466
}
7567
return constraintMetaModel;
7668
}

0 commit comments

Comments
 (0)