diff --git a/quarkus-integration/quarkus/deployment/pom.xml b/quarkus-integration/quarkus/deployment/pom.xml index 411d9bf409..bbc8bcc5ab 100644 --- a/quarkus-integration/quarkus/deployment/pom.xml +++ b/quarkus-integration/quarkus/deployment/pom.xml @@ -101,6 +101,16 @@ + + org.revapi + revapi-maven-plugin + + + + ${project.groupId}:timefold-solver-quarkus-deployment:1.18.0 + + + maven-surefire-plugin diff --git a/quarkus-integration/quarkus/deployment/src/build/revapi-differences.json b/quarkus-integration/quarkus/deployment/src/build/revapi-differences.json new file mode 100644 index 0000000000..6441d68975 --- /dev/null +++ b/quarkus-integration/quarkus/deployment/src/build/revapi-differences.json @@ -0,0 +1,9 @@ +[ + { + "extension": "revapi.differences", + "configuration": { + "differences": [ + ] + } + } +] diff --git a/quarkus-integration/quarkus/deployment/src/build/revapi-filter.json b/quarkus-integration/quarkus/deployment/src/build/revapi-filter.json new file mode 100644 index 0000000000..6b4a8a5528 --- /dev/null +++ b/quarkus-integration/quarkus/deployment/src/build/revapi-filter.json @@ -0,0 +1,12 @@ +[ + { + "extension": "revapi.filter", + "configuration": { + "elements": { + "include": [ + "class ai\\.timefold\\.solver\\.quarkus\\.deployment\\.api.*" + ] + } + } + } +] \ No newline at end of file diff --git a/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/SolverConfigBuildItem.java b/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/SolverConfigBuildItem.java index f913479591..8329625898 100644 --- a/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/SolverConfigBuildItem.java +++ b/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/SolverConfigBuildItem.java @@ -15,7 +15,8 @@ public final class SolverConfigBuildItem extends SimpleBuildItem { * Constructor for multiple solver configurations. */ public SolverConfigBuildItem(Map solverConfig, GeneratedGizmoClasses generatedGizmoClasses) { - this.solverConfigurations = solverConfig; + // Defensive copy to avoid changing the map in dependent build items. + this.solverConfigurations = Map.copyOf(solverConfig); this.generatedGizmoClasses = generatedGizmoClasses; } diff --git a/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/TimefoldProcessor.java b/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/TimefoldProcessor.java index ad04eb4db8..93c98cb679 100644 --- a/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/TimefoldProcessor.java +++ b/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/TimefoldProcessor.java @@ -30,6 +30,7 @@ import ai.timefold.solver.core.api.domain.solution.ProblemFactCollectionProperty; import ai.timefold.solver.core.api.score.calculator.EasyScoreCalculator; import ai.timefold.solver.core.api.score.calculator.IncrementalScoreCalculator; +import ai.timefold.solver.core.api.score.stream.ConstraintMetaModel; import ai.timefold.solver.core.api.score.stream.ConstraintProvider; import ai.timefold.solver.core.api.solver.SolverFactory; import ai.timefold.solver.core.api.solver.SolverManager; @@ -40,10 +41,12 @@ import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; import ai.timefold.solver.core.impl.heuristic.selector.common.nearby.NearbyDistanceMeter; import ai.timefold.solver.quarkus.TimefoldRecorder; +import ai.timefold.solver.quarkus.bean.BeanUtil; import ai.timefold.solver.quarkus.bean.DefaultTimefoldBeanProvider; import ai.timefold.solver.quarkus.bean.TimefoldSolverBannerBean; import ai.timefold.solver.quarkus.bean.UnavailableTimefoldBeanProvider; import ai.timefold.solver.quarkus.config.TimefoldRuntimeConfig; +import ai.timefold.solver.quarkus.deployment.api.ConstraintMetaModelBuildItem; import ai.timefold.solver.quarkus.deployment.config.SolverBuildTimeConfig; import ai.timefold.solver.quarkus.deployment.config.TimefoldBuildTimeConfig; import ai.timefold.solver.quarkus.devui.DevUISolverConfig; @@ -573,6 +576,29 @@ private SolverConfig loadSolverConfig(IndexView indexView, return solverConfig; } + @BuildStep + void buildConstraintMetaModel(SolverConfigBuildItem solverConfigBuildItem, + BuildProducer constraintMetaModelBuildItemBuildProducer) { + if (solverConfigBuildItem.getSolverConfigMap().isEmpty()) { + return; + } + + Map constraintMetaModelsBySolverNames = new HashMap<>(); + solverConfigBuildItem.getSolverConfigMap().forEach((solverName, solverConfig) -> { + // Gizmo-generated member accessors are not yet available at build time. + DomainAccessType originalDomainAccessType = solverConfig.getDomainAccessType(); + solverConfig.setDomainAccessType(DomainAccessType.REFLECTION); + + var solverFactory = SolverFactory.create(solverConfig); + ConstraintMetaModel constraintMetaModel = BeanUtil.buildConstraintMetaModel(solverFactory); + // Avoid changing the original solver config. + solverConfig.setDomainAccessType(originalDomainAccessType); + constraintMetaModelsBySolverNames.put(solverName, constraintMetaModel); + }); + + constraintMetaModelBuildItemBuildProducer.produce(new ConstraintMetaModelBuildItem(constraintMetaModelsBySolverNames)); + } + @BuildStep @Record(RUNTIME_INIT) void recordAndRegisterRuntimeBeans(TimefoldRecorder recorder, RecorderContext recorderContext, diff --git a/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/api/ConstraintMetaModelBuildItem.java b/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/api/ConstraintMetaModelBuildItem.java new file mode 100644 index 0000000000..dc3a060db6 --- /dev/null +++ b/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/api/ConstraintMetaModelBuildItem.java @@ -0,0 +1,24 @@ +package ai.timefold.solver.quarkus.deployment.api; + +import java.util.Map; + +import ai.timefold.solver.core.api.score.stream.ConstraintMetaModel; + +import io.quarkus.builder.item.SimpleBuildItem; + +/** + * Represents a {@link ai.timefold.solver.core.api.score.stream.ConstraintMetaModel} at the build time for the purpose + * of Quarkus augmentation. + */ +public final class ConstraintMetaModelBuildItem extends SimpleBuildItem { + + private final Map constraintMetaModelsBySolverNames; + + public ConstraintMetaModelBuildItem(Map constraintMetaModelsBySolverNames) { + this.constraintMetaModelsBySolverNames = Map.copyOf(constraintMetaModelsBySolverNames); + } + + public Map constraintMetaModelsBySolverNames() { + return constraintMetaModelsBySolverNames; + } +} diff --git a/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/api/package-info.java b/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/api/package-info.java new file mode 100644 index 0000000000..1e7fa44728 --- /dev/null +++ b/quarkus-integration/quarkus/deployment/src/main/java/ai/timefold/solver/quarkus/deployment/api/package-info.java @@ -0,0 +1,7 @@ +/** + * Public {@link io.quarkus.builder.item.BuildItem}s consumable by other Quarkus extensions. + *

+ * All classes in this package are part of the public API of this extension. + * Moreover, the extension is responsible for creating instances of these classes during the build phase. + */ +package ai.timefold.solver.quarkus.deployment.api; diff --git a/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/bean/BeanUtil.java b/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/bean/BeanUtil.java new file mode 100644 index 0000000000..c762015861 --- /dev/null +++ b/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/bean/BeanUtil.java @@ -0,0 +1,32 @@ +package ai.timefold.solver.quarkus.bean; + +import java.util.Objects; + +import ai.timefold.solver.core.api.score.stream.ConstraintMetaModel; +import ai.timefold.solver.core.api.solver.SolverFactory; +import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraintStreamScoreDirectorFactory; +import ai.timefold.solver.core.impl.solver.DefaultSolverFactory; + +public class BeanUtil { + + public static ConstraintMetaModel buildConstraintMetaModel(SolverFactory solverFactory) { + if (Objects.requireNonNull(solverFactory) instanceof DefaultSolverFactory defaultSolverFactory) { + var scoreDirectorFactory = defaultSolverFactory.getScoreDirectorFactory(); + if (scoreDirectorFactory instanceof AbstractConstraintStreamScoreDirectorFactory castScoreDirectorFactory) { + return castScoreDirectorFactory.getConstraintMetaModel(); + } else { + throw new IllegalStateException( + "Cannot provide %s because the score director does not use the Constraint Streams API." + .formatted(ConstraintMetaModel.class.getSimpleName())); + } + } else { + throw new IllegalStateException( + "%s is not supported by the solver factory (%s)." + .formatted(ConstraintMetaModel.class.getSimpleName(), solverFactory.getClass().getName())); + } + } + + private BeanUtil() { + throw new IllegalStateException("Utility class"); + } +} diff --git a/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/bean/DefaultTimefoldBeanProvider.java b/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/bean/DefaultTimefoldBeanProvider.java index c8dedd0c84..6a7ca96c92 100644 --- a/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/bean/DefaultTimefoldBeanProvider.java +++ b/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/bean/DefaultTimefoldBeanProvider.java @@ -24,8 +24,6 @@ import ai.timefold.solver.core.api.solver.SolverManager; import ai.timefold.solver.core.config.solver.SolverConfig; import ai.timefold.solver.core.config.solver.SolverManagerConfig; -import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraintStreamScoreDirectorFactory; -import ai.timefold.solver.core.impl.solver.DefaultSolverFactory; import io.quarkus.arc.DefaultBean; import io.quarkus.arc.Lock; @@ -63,14 +61,8 @@ SolverFactory solverFactory(SolverConfig solverConfig) { @Produces ConstraintMetaModel constraintProviderMetaModel(SolverFactory solverFactory) { if (constraintMetaModel == null) { - var scoreDirectorFactory = ((DefaultSolverFactory) solverFactory).getScoreDirectorFactory(); - if (scoreDirectorFactory instanceof AbstractConstraintStreamScoreDirectorFactory castScoreDirectorFactory) { - constraintMetaModel = castScoreDirectorFactory.getConstraintMetaModel(); - } else { - throw new IllegalStateException( - "Cannot provide %s because the score director does not use the Constraint Streams API." - .formatted(ConstraintMetaModel.class.getSimpleName())); - } + // The metamodel is not compatible with Quarkus code recording, thus we need to rebuild it at runtime. + constraintMetaModel = BeanUtil.buildConstraintMetaModel(solverFactory); } return constraintMetaModel; }