From afac07afb60c877de8769c247519bc929351f953 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Thu, 21 Sep 2023 12:49:54 -0400 Subject: [PATCH] fix: Migrate to new Quarkus Dev UI --- build/build-parent/pom.xml | 2 +- .../quarkus/deployment/TimefoldProcessor.java | 52 ++++++-- .../resources/dev-templates/constraints.html | 19 --- .../resources/dev-templates/embedded.html | 12 -- .../main/resources/dev-templates/model.html | 47 -------- .../resources/dev-templates/solverConfig.html | 9 -- .../main/resources/dev-ui/config-component.js | 61 ++++++++++ .../resources/dev-ui/constraints-component.js | 76 ++++++++++++ .../main/resources/dev-ui/model-component.js | 104 ++++++++++++++++ .../quarkus/devui-integration-test/pom.xml | 11 +- .../quarkus/it/devui/TimefoldDevUITest.java | 111 +++++++----------- quarkus-integration/quarkus/runtime/pom.xml | 6 + .../quarkus/devui/SolverConfigText.java | 17 +++ ...=> TimefoldDevUIPropertiesRPCService.java} | 73 ++++++++---- .../quarkus/devui/TimefoldDevUIRecorder.java | 16 +++ 15 files changed, 413 insertions(+), 203 deletions(-) delete mode 100644 quarkus-integration/quarkus/deployment/src/main/resources/dev-templates/constraints.html delete mode 100644 quarkus-integration/quarkus/deployment/src/main/resources/dev-templates/embedded.html delete mode 100644 quarkus-integration/quarkus/deployment/src/main/resources/dev-templates/model.html delete mode 100644 quarkus-integration/quarkus/deployment/src/main/resources/dev-templates/solverConfig.html create mode 100644 quarkus-integration/quarkus/deployment/src/main/resources/dev-ui/config-component.js create mode 100644 quarkus-integration/quarkus/deployment/src/main/resources/dev-ui/constraints-component.js create mode 100644 quarkus-integration/quarkus/deployment/src/main/resources/dev-ui/model-component.js create mode 100644 quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/devui/SolverConfigText.java rename quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/devui/{TimefoldDevUIPropertiesSupplier.java => TimefoldDevUIPropertiesRPCService.java} (68%) create mode 100644 quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/devui/TimefoldDevUIRecorder.java diff --git a/build/build-parent/pom.xml b/build/build-parent/pom.xml index d794082a81..dceeeac10a 100644 --- a/build/build-parent/pom.xml +++ b/build/build-parent/pom.xml @@ -21,7 +21,7 @@ 1.4.11 2.20.0 - 3.3.2 + 3.4.1 3.6.1 1.10.0 5.2.3 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 0399b0d096..99061c6943 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 @@ -14,6 +14,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Singleton; import ai.timefold.solver.core.api.domain.autodiscover.AutoDiscoverMemberType; @@ -43,7 +44,9 @@ import ai.timefold.solver.quarkus.bean.UnavailableTimefoldBeanProvider; import ai.timefold.solver.quarkus.config.TimefoldRuntimeConfig; import ai.timefold.solver.quarkus.deployment.config.TimefoldBuildTimeConfig; -import ai.timefold.solver.quarkus.devui.TimefoldDevUIPropertiesSupplier; +import ai.timefold.solver.quarkus.devui.SolverConfigText; +import ai.timefold.solver.quarkus.devui.TimefoldDevUIPropertiesRPCService; +import ai.timefold.solver.quarkus.devui.TimefoldDevUIRecorder; import ai.timefold.solver.quarkus.gizmo.TimefoldGizmoBeanFactory; import org.jboss.jandex.AnnotationInstance; @@ -76,11 +79,12 @@ import io.quarkus.deployment.builditem.IndexDependencyBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveHierarchyBuildItem; import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem; -import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem; import io.quarkus.deployment.pkg.steps.NativeBuild; import io.quarkus.deployment.recording.RecorderContext; import io.quarkus.deployment.util.ServiceUtil; -import io.quarkus.devconsole.spi.DevConsoleRuntimeTemplateInfoBuildItem; +import io.quarkus.devui.spi.JsonRPCProvidersBuildItem; +import io.quarkus.devui.spi.page.CardPageBuildItem; +import io.quarkus.devui.spi.page.Page; import io.quarkus.gizmo.ClassOutput; import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; @@ -148,20 +152,48 @@ DetermineIfNativeBuildItem ifNativeBuild() { } @BuildStep(onlyIf = IsDevelopment.class) - public DevConsoleRuntimeTemplateInfoBuildItem getSolverConfig(SolverConfigBuildItem solverConfigBuildItem, - CurateOutcomeBuildItem curateOutcomeBuildItem) { + @Record(STATIC_INIT) + public CardPageBuildItem registerDevUICard( + TimefoldDevUIRecorder devUIRecorder, + SolverConfigBuildItem solverConfigBuildItem, + BuildProducer syntheticBeans) { + CardPageBuildItem cardPageBuildItem = new CardPageBuildItem(); SolverConfig solverConfig = solverConfigBuildItem.getSolverConfig(); if (solverConfig != null) { StringWriter effectiveSolverConfigWriter = new StringWriter(); SolverConfigIO solverConfigIO = new SolverConfigIO(); solverConfigIO.write(solverConfig, effectiveSolverConfigWriter); - return new DevConsoleRuntimeTemplateInfoBuildItem("solverConfigProperties", - new TimefoldDevUIPropertiesSupplier(effectiveSolverConfigWriter.toString()), this.getClass(), - curateOutcomeBuildItem); + syntheticBeans.produce(SyntheticBeanBuildItem.configure(SolverConfigText.class) + .scope(ApplicationScoped.class) + .supplier(devUIRecorder.solverConfigTextSupplier(effectiveSolverConfigWriter.toString())) + .done()); } else { - return new DevConsoleRuntimeTemplateInfoBuildItem("solverConfigProperties", - new TimefoldDevUIPropertiesSupplier(), this.getClass(), curateOutcomeBuildItem); + syntheticBeans.produce(SyntheticBeanBuildItem.configure(SolverConfigText.class) + .scope(ApplicationScoped.class) + .supplier(devUIRecorder.solverConfigTextSupplier("")) + .done()); } + cardPageBuildItem.addPage(Page.webComponentPageBuilder() + .title("Configuration") + .icon("font-awesome-solid:wrench") + .componentLink("config-component.js")); + + cardPageBuildItem.addPage(Page.webComponentPageBuilder() + .title("Model") + .icon("font-awesome-solid:wrench") + .componentLink("model-component.js")); + + cardPageBuildItem.addPage(Page.webComponentPageBuilder() + .title("Constraints") + .icon("font-awesome-solid:wrench") + .componentLink("constraints-component.js")); + + return cardPageBuildItem; + } + + @BuildStep(onlyIf = IsDevelopment.class) + public JsonRPCProvidersBuildItem registerRPCService() { + return new JsonRPCProvidersBuildItem("Timefold Solver", TimefoldDevUIPropertiesRPCService.class); } /** diff --git a/quarkus-integration/quarkus/deployment/src/main/resources/dev-templates/constraints.html b/quarkus-integration/quarkus/deployment/src/main/resources/dev-templates/constraints.html deleted file mode 100644 index 9556dce5e9..0000000000 --- a/quarkus-integration/quarkus/deployment/src/main/resources/dev-templates/constraints.html +++ /dev/null @@ -1,19 +0,0 @@ -{#include main} - -{#title}Constraints{/title} - -{#body} - - - - - - - - {#for constraint in info:solverConfigProperties.constraintList} - - {/for} - -
Constraints
{constraint}
-{/body} -{/include} \ No newline at end of file diff --git a/quarkus-integration/quarkus/deployment/src/main/resources/dev-templates/embedded.html b/quarkus-integration/quarkus/deployment/src/main/resources/dev-templates/embedded.html deleted file mode 100644 index c5b03ee5e1..0000000000 --- a/quarkus-integration/quarkus/deployment/src/main/resources/dev-templates/embedded.html +++ /dev/null @@ -1,12 +0,0 @@ - - - View Effective Solver Configuration - - - - View Model - - - - View Constraints - \ No newline at end of file diff --git a/quarkus-integration/quarkus/deployment/src/main/resources/dev-templates/model.html b/quarkus-integration/quarkus/deployment/src/main/resources/dev-templates/model.html deleted file mode 100644 index f825f964e5..0000000000 --- a/quarkus-integration/quarkus/deployment/src/main/resources/dev-templates/model.html +++ /dev/null @@ -1,47 +0,0 @@ -{#include main} - -{#title}Model{/title} - -{#body} -
-
Solution: {info:solverConfigProperties.timefoldModelProperties.solutionClass}
-
- {#for entityClass in info:solverConfigProperties.timefoldModelProperties.entityClassList} -
-
Entity: {entityClass}
-
- {#if !info:solverConfigProperties.timefoldModelProperties.entityClassToGenuineVariableListMap.get(entityClass).isEmpty()} - - - - - - - - {#for genuineVariable in info:solverConfigProperties.timefoldModelProperties.entityClassToGenuineVariableListMap.get(entityClass)} - - {/for} - -
Genuine Variables
{genuineVariable}
- {/if} - {#if !info:solverConfigProperties.timefoldModelProperties.entityClassToShadowVariableListMap.get(entityClass).isEmpty()} - - - - - - - - {#for shadowVariable in info:solverConfigProperties.timefoldModelProperties.entityClassToShadowVariableListMap.get(entityClass)} - - {/for} - -
Shadow Variables
{shadowVariable}
- {/if} -
-
- {/for} -
-
-{/body} -{/include} \ No newline at end of file diff --git a/quarkus-integration/quarkus/deployment/src/main/resources/dev-templates/solverConfig.html b/quarkus-integration/quarkus/deployment/src/main/resources/dev-templates/solverConfig.html deleted file mode 100644 index 61853afd02..0000000000 --- a/quarkus-integration/quarkus/deployment/src/main/resources/dev-templates/solverConfig.html +++ /dev/null @@ -1,9 +0,0 @@ -{#include main} - -{#title}Solver Configuration{/title} -{#body} -
-{info:solverConfigProperties.effectiveSolverConfig}
-
-{/body} -{/include} \ No newline at end of file diff --git a/quarkus-integration/quarkus/deployment/src/main/resources/dev-ui/config-component.js b/quarkus-integration/quarkus/deployment/src/main/resources/dev-ui/config-component.js new file mode 100644 index 0000000000..7b3bb3e59a --- /dev/null +++ b/quarkus-integration/quarkus/deployment/src/main/resources/dev-ui/config-component.js @@ -0,0 +1,61 @@ +import {css, html, LitElement} from 'lit'; +import {JsonRpc} from 'jsonrpc'; +import '@vaadin/icon'; +import '@vaadin/button'; +import {until} from 'lit/directives/until.js'; +import '@vaadin/grid'; +import '@vaadin/grid/vaadin-grid-sort-column.js'; + +export class ConfigComponent extends LitElement { + + jsonRpc = new JsonRpc("Timefold Solver"); + + // Component style + static styles = css` + .button { + background-color: transparent; + cursor: pointer; + } + + .clearIcon { + color: orange; + } + `; + + // Component properties + static properties = { + "_config": {state: true} + } + + // Components callbacks + + /** + * Called when displayed + */ + connectedCallback() { + super.connectedCallback(); + this.jsonRpc.getConfig().then(jsonRpcResponse => { + this._config = jsonRpcResponse.result.config; + }); + } + + /** + * Called when it needs to render the components + * @returns {*} + */ + render() { + return html`${until(this._renderConfig(), html`Loading config...`)}`; + } + + // View / Templates + + _renderConfig() { + if (this._config) { + let config = this._config; + return html` +
${config}
`; + } + } +} + +customElements.define('config-component', ConfigComponent); \ No newline at end of file diff --git a/quarkus-integration/quarkus/deployment/src/main/resources/dev-ui/constraints-component.js b/quarkus-integration/quarkus/deployment/src/main/resources/dev-ui/constraints-component.js new file mode 100644 index 0000000000..c24da5bc2e --- /dev/null +++ b/quarkus-integration/quarkus/deployment/src/main/resources/dev-ui/constraints-component.js @@ -0,0 +1,76 @@ +import {css, html, LitElement} from 'lit'; +import {JsonRpc} from 'jsonrpc'; +import '@vaadin/icon'; +import '@vaadin/button'; +import {until} from 'lit/directives/until.js'; +import '@vaadin/grid'; +import {columnBodyRenderer} from '@vaadin/grid/lit.js'; +import '@vaadin/grid/vaadin-grid-sort-column.js'; + +export class ConstraintsComponent extends LitElement { + + jsonRpc = new JsonRpc("Timefold Solver"); + + // Component style + static styles = css` + .button { + background-color: transparent; + cursor: pointer; + } + + .clearIcon { + color: orange; + } + `; + + // Component properties + static properties = { + "_constraints": {state: true} + } + + // Components callbacks + + /** + * Called when displayed + */ + connectedCallback() { + super.connectedCallback(); + this.jsonRpc.getConstraints().then(jsonRpcResponse => { + this._constraints = []; + jsonRpcResponse.result.forEach(c => { + this._constraints.push(c); + }); + }); + } + + /** + * Called when it needs to render the components + * @returns {*} + */ + render() { + return html`${until(this._renderConstraintTable(), html`Loading constraints...`)}`; + } + + // View / Templates + + _renderConstraintTable() { + if (this._constraints) { + let constraints = this._constraints; + return html` + + + + `; + } + } + + _nameRenderer(constraint) { + return html` + ${constraint}`; + } + +} + +customElements.define('constraints-component', ConstraintsComponent); \ No newline at end of file diff --git a/quarkus-integration/quarkus/deployment/src/main/resources/dev-ui/model-component.js b/quarkus-integration/quarkus/deployment/src/main/resources/dev-ui/model-component.js new file mode 100644 index 0000000000..8c60ec5c1b --- /dev/null +++ b/quarkus-integration/quarkus/deployment/src/main/resources/dev-ui/model-component.js @@ -0,0 +1,104 @@ +import {css, html, LitElement} from 'lit'; +import {JsonRpc} from 'jsonrpc'; +import '@vaadin/icon'; +import '@vaadin/button'; +import {until} from 'lit/directives/until.js'; +import '@vaadin/grid'; +import {columnBodyRenderer} from '@vaadin/grid/lit.js'; +import '@vaadin/grid/vaadin-grid-sort-column.js'; + +export class ModelComponent extends LitElement { + + jsonRpc = new JsonRpc("Timefold Solver"); + + // Component style + static styles = css` + .button { + background-color: transparent; + cursor: pointer; + } + + .clearIcon { + color: orange; + } + `; + + // Component properties + static properties = { + "_model": {state: true} + } + + // Components callbacks + + /** + * Called when displayed + */ + connectedCallback() { + super.connectedCallback(); + this.jsonRpc.getModelInfo().then(jsonRpcResponse => { + this._model = {}; + this._model.solutionClass = jsonRpcResponse.result.solutionClass; + this._model.entityInfoList = []; + jsonRpcResponse.result.entityClassList.forEach(entityClass => { + const entityInfo = {}; + entityInfo.name = entityClass; + entityInfo.genuineVariableList = jsonRpcResponse.result.entityClassToGenuineVariableListMap[entityClass]; + entityInfo.shadowVariableList = jsonRpcResponse.result.entityClassToShadowVariableListMap[entityClass]; + this._model.entityInfoList.push(entityInfo); + }); + }); + } + + /** + * Called when it needs to render the components + * @returns {*} + */ + render() { + return html`${until(this._renderModel(), html`Loading model...`)}`; + } + + // View / Templates + + _renderModel() { + if (this._model) { + let model = this._model; + return html` +
+ Solution Class: + ${model.solutionClass} +
+ + + + + + + + `; + } + } + + _entityClassNameRenderer(entityInfo) { + return html` + ${entityInfo.name}`; + } + + _genuineVariablesRenderer(entityInfo) { + return html` + ${entityInfo.genuineVariableList.join(', ')}`; + } + + _shadowVariablesRenderer(entityInfo) { + return html` + ${entityInfo.shadowVariableList.join(', ')}`; + } + +} + +customElements.define('model-component', ModelComponent); \ No newline at end of file diff --git a/quarkus-integration/quarkus/devui-integration-test/pom.xml b/quarkus-integration/quarkus/devui-integration-test/pom.xml index 39cda1c926..7f5a77c223 100644 --- a/quarkus-integration/quarkus/devui-integration-test/pom.xml +++ b/quarkus-integration/quarkus/devui-integration-test/pom.xml @@ -17,8 +17,6 @@ ai.timefold.solver.quarkus **/* - - /q/dev-v1 @@ -46,8 +44,8 @@ test - io.rest-assured - rest-assured + io.quarkus + quarkus-vertx-http-dev-ui-tests test @@ -72,11 +70,6 @@ maven-surefire-plugin - - - ${dev.ui.root} - - maven-dependency-plugin diff --git a/quarkus-integration/quarkus/devui-integration-test/src/test/java/ai/timefold/solver/quarkus/it/devui/TimefoldDevUITest.java b/quarkus-integration/quarkus/devui-integration-test/src/test/java/ai/timefold/solver/quarkus/it/devui/TimefoldDevUITest.java index 1a5546d1db..233b0f99d7 100644 --- a/quarkus-integration/quarkus/devui-integration-test/src/test/java/ai/timefold/solver/quarkus/it/devui/TimefoldDevUITest.java +++ b/quarkus-integration/quarkus/devui-integration-test/src/test/java/ai/timefold/solver/quarkus/it/devui/TimefoldDevUITest.java @@ -2,106 +2,75 @@ import static org.assertj.core.api.Assertions.assertThat; -import java.io.IOException; -import java.util.Objects; - -import javax.xml.parsers.ParserConfigurationException; +import ai.timefold.solver.quarkus.it.devui.domain.TestdataStringLengthShadowEntity; +import ai.timefold.solver.quarkus.it.devui.domain.TestdataStringLengthShadowSolution; +import ai.timefold.solver.quarkus.it.devui.solver.TestdataStringLengthConstraintProvider; import org.jboss.shrinkwrap.api.ShrinkWrap; import org.jboss.shrinkwrap.api.spec.JavaArchive; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import org.xml.sax.SAXException; -import groovy.util.Node; -import groovy.xml.XmlParser; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.TextNode; + +import io.quarkus.devui.tests.DevUIJsonRPCTest; import io.quarkus.test.QuarkusDevModeTest; -import io.restassured.RestAssured; -public class TimefoldDevUITest { +public class TimefoldDevUITest extends DevUIJsonRPCTest { @RegisterExtension static final QuarkusDevModeTest config = new QuarkusDevModeTest() .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) - .addPackages(true, "ai.timefold.solver.quarkus.it.devui")); - - // Use the Quarkus 3 context root by default as the Quarkus platform does not pass the system property. - static final String TIMEFOLD_DEV_UI_BASE_URL = - System.getProperty("dev.iu.root", "/q/dev-v1") + "/ai.timefold.solver.timefold-solver-quarkus/"; + .addPackages(true, TimefoldTestResource.class.getPackage().getName())); - public static String getPage(String pageName) { - return TIMEFOLD_DEV_UI_BASE_URL + pageName; + public TimefoldDevUITest() { + super("Timefold Solver"); } @Test - void testSolverConfigPage() throws ParserConfigurationException, SAXException, IOException { - String body = RestAssured.get(getPage("solverConfig")) - .then() - .extract() - .body() - .asPrettyString(); - XmlParser xmlParser = new XmlParser(); - Node node = xmlParser.parseText(body); - String solverConfig = Objects.requireNonNull(findById("timefold-solver-config", node)).text(); + void testSolverConfigPage() throws Exception { + JsonNode configResponse = super.executeJsonRPCMethod("getConfig"); + String solverConfig = configResponse.get("config").asText(); assertThat(solverConfig).isEqualToIgnoringWhitespace( "\n" + "\n" + "\n" - + " ai.timefold.solver.quarkus.it.devui.domain.TestdataStringLengthShadowSolution\n" - + " ai.timefold.solver.quarkus.it.devui.domain.TestdataStringLengthShadowEntity\n" + + " " + TestdataStringLengthShadowSolution.class.getCanonicalName() + + "\n" + + " " + TestdataStringLengthShadowEntity.class.getCanonicalName() + "\n" + " GIZMO\n" + " \n" - + " ai.timefold.solver.quarkus.it.devui.solver.TestdataStringLengthConstraintProvider\n" + + " " + TestdataStringLengthConstraintProvider.class.getCanonicalName() + + "\n" + " \n" + ""); } @Test - void testModelPage() throws ParserConfigurationException, SAXException, IOException { - String body = RestAssured.get(getPage("model")) - .then() - .extract() - .body() - .asPrettyString(); - XmlParser xmlParser = new XmlParser(); - Node node = xmlParser.parseText(body); - String model = Objects.requireNonNull(findById("timefold-solver-model", node)).toString(); - assertThat(model) - .contains("value=[Solution: ai.timefold.solver.quarkus.it.devui.domain.TestdataStringLengthShadowSolution]"); - assertThat(model) - .contains("value=[Entity: ai.timefold.solver.quarkus.it.devui.domain.TestdataStringLengthShadowEntity]"); - assertThat(model).contains( - "value=[Genuine Variables]]]]]], tbody[attributes={}; value=[tr[attributes={}; value=[td[attributes={colspan=1, rowspan=1}; value=[value]]"); - assertThat(model).contains( - "value=[Shadow Variables]]]]]], tbody[attributes={}; value=[tr[attributes={}; value=[td[attributes={colspan=1, rowspan=1}; value=[length]]"); + void testModelPage() throws Exception { + JsonNode modelResponse = super.executeJsonRPCMethod("getModelInfo"); + assertThat(modelResponse.get("solutionClass").asText()) + .contains(TestdataStringLengthShadowSolution.class.getCanonicalName()); + assertThat(modelResponse.get("entityClassList")) + .containsExactly( + new TextNode(TestdataStringLengthShadowEntity.class.getCanonicalName())); + assertThat(modelResponse.get("entityClassToGenuineVariableListMap")).hasSize(1); + assertThat(modelResponse.get("entityClassToGenuineVariableListMap") + .get(TestdataStringLengthShadowEntity.class.getCanonicalName())) + .containsExactly(new TextNode("value")); + assertThat(modelResponse.get("entityClassToShadowVariableListMap")).hasSize(1); + assertThat(modelResponse.get("entityClassToShadowVariableListMap") + .get(TestdataStringLengthShadowEntity.class.getCanonicalName())) + .containsExactly(new TextNode("length")); } @Test - void testConstraintsPage() throws ParserConfigurationException, SAXException, IOException { - String body = RestAssured.get(getPage("constraints")) - .then() - .extract() - .body() - .asPrettyString(); - XmlParser xmlParser = new XmlParser(); - Node node = xmlParser.parseText(body); - String constraints = Objects.requireNonNull(findById("timefold-solver-constraints", node)).text(); - assertThat(constraints).contains("ai.timefold.solver.quarkus.it.devui.domain/Don't assign 2 entities the same value"); - assertThat(constraints).contains("ai.timefold.solver.quarkus.it.devui.domain/Maximize value length"); - } - - private Node findById(String id, Node node) { - if (id.equals(node.attribute("id"))) { - return node; - } - for (Object child : node.children()) { - if (child instanceof Node node1) { - Node maybeFoundNodeText = findById(id, node1); - if (maybeFoundNodeText != null) { - return maybeFoundNodeText; - } - } - } - return null; + void testConstraintsPage() throws Exception { + JsonNode constraintsResponse = super.executeJsonRPCMethod("getConstraints"); + assertThat(constraintsResponse).containsExactly( + new TextNode(TestdataStringLengthShadowSolution.class.getPackage() + .getName() + "/Don't assign 2 entities the same value."), + new TextNode(TestdataStringLengthShadowSolution.class.getPackage().getName() + "/Maximize value length")); } } diff --git a/quarkus-integration/quarkus/runtime/pom.xml b/quarkus-integration/quarkus/runtime/pom.xml index b39d7dbf0d..5ae7651a02 100644 --- a/quarkus-integration/quarkus/runtime/pom.xml +++ b/quarkus-integration/quarkus/runtime/pom.xml @@ -30,6 +30,12 @@ ai.timefold.solver timefold-solver-core + + + io.quarkus + quarkus-vertx-http + true + io.quarkus.gizmo diff --git a/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/devui/SolverConfigText.java b/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/devui/SolverConfigText.java new file mode 100644 index 0000000000..64db1c78ae --- /dev/null +++ b/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/devui/SolverConfigText.java @@ -0,0 +1,17 @@ +package ai.timefold.solver.quarkus.devui; + +public class SolverConfigText { + private String solverConfigText; + + public SolverConfigText(String solverConfigText) { + this.solverConfigText = solverConfigText; + } + + public String getSolverConfigText() { + return solverConfigText; + } + + public void setSolverConfigText(String solverConfigText) { + this.solverConfigText = solverConfigText; + } +} diff --git a/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/devui/TimefoldDevUIPropertiesSupplier.java b/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/devui/TimefoldDevUIPropertiesRPCService.java similarity index 68% rename from quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/devui/TimefoldDevUIPropertiesSupplier.java rename to quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/devui/TimefoldDevUIPropertiesRPCService.java index 64fd1ad47f..6add26c6c4 100644 --- a/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/devui/TimefoldDevUIPropertiesSupplier.java +++ b/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/devui/TimefoldDevUIPropertiesRPCService.java @@ -6,9 +6,12 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.function.Supplier; import java.util.stream.Collectors; +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + import ai.timefold.solver.constraint.streams.common.AbstractConstraintStreamScoreDirectorFactory; import ai.timefold.solver.core.api.domain.entity.PlanningEntity; import ai.timefold.solver.core.api.domain.solution.PlanningSolution; @@ -21,46 +24,66 @@ import ai.timefold.solver.core.impl.solver.DefaultSolverFactory; import io.quarkus.arc.Arc; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; -public class TimefoldDevUIPropertiesSupplier implements Supplier { - private String effectiveSolverConfigXml; - - public TimefoldDevUIPropertiesSupplier() { - this.effectiveSolverConfigXml = null; - } +@ApplicationScoped +public class TimefoldDevUIPropertiesRPCService { - public TimefoldDevUIPropertiesSupplier(String effectiveSolverConfigXml) { - this.effectiveSolverConfigXml = effectiveSolverConfigXml; - } + private final String effectiveSolverConfigXml; - // Needed for Quarkus Dev UI serialization - public String getEffectiveSolverConfigXml() { - return effectiveSolverConfigXml; - } + private TimefoldDevUIProperties devUIProperties; - public void setEffectiveSolverConfigXml(String effectiveSolverConfigXml) { - this.effectiveSolverConfigXml = effectiveSolverConfigXml; + @Inject + public TimefoldDevUIPropertiesRPCService(SolverConfigText solverConfigText) { + this.effectiveSolverConfigXml = solverConfigText.getSolverConfigText(); } - @Override - public TimefoldDevUIProperties get() { + @PostConstruct + public void init() { if (effectiveSolverConfigXml != null) { // SolverConfigIO does not work at runtime, // but the build time SolverConfig does not have properties // that can be set at runtime (ex: termination), so the // effective solver config will be missing some properties - return new TimefoldDevUIProperties(getModelInfo(), - getXmlContentWithComment("Properties that can be set at runtime are not included"), - getConstraintList()); + devUIProperties = new TimefoldDevUIProperties(buildModelInfo(), + buildXmlContentWithComment("Properties that can be set at runtime are not included"), + buildConstraintList()); } else { - return new TimefoldDevUIProperties(getModelInfo(), + devUIProperties = new TimefoldDevUIProperties(buildModelInfo(), "\n", Collections.emptyList()); } } - private TimefoldModelProperties getModelInfo() { + public JsonObject getConfig() { + JsonObject out = new JsonObject(); + out.put("config", devUIProperties.getEffectiveSolverConfig()); + return out; + } + + public JsonArray getConstraints() { + return JsonArray.of(devUIProperties.getConstraintList().toArray()); + } + + public JsonObject getModelInfo() { + TimefoldModelProperties modelProperties = devUIProperties.getTimefoldModelProperties(); + JsonObject out = new JsonObject(); + out.put("solutionClass", modelProperties.solutionClass); + out.put("entityClassList", JsonArray.of(modelProperties.entityClassList.toArray())); + out.put("entityClassToGenuineVariableListMap", + new JsonObject(modelProperties.entityClassToGenuineVariableListMap.entrySet() + .stream() + .collect(Collectors.toMap(Map.Entry::getKey, entry -> JsonArray.of(entry.getValue().toArray()))))); + out.put("entityClassToShadowVariableListMap", + new JsonObject(modelProperties.entityClassToShadowVariableListMap.entrySet() + .stream() + .collect(Collectors.toMap(Map.Entry::getKey, entry -> JsonArray.of(entry.getValue().toArray()))))); + return out; + } + + private TimefoldModelProperties buildModelInfo() { if (effectiveSolverConfigXml != null) { DefaultSolverFactory solverFactory = (DefaultSolverFactory) Arc.container().instance(SolverFactory.class).get(); @@ -95,7 +118,7 @@ private TimefoldModelProperties getModelInfo() { } } - private List getConstraintList() { + private List buildConstraintList() { if (effectiveSolverConfigXml != null) { DefaultSolverFactory solverFactory = (DefaultSolverFactory) Arc.container().instance(SolverFactory.class).get(); @@ -109,7 +132,7 @@ private List getConstraintList() { return Collections.emptyList(); } - private String getXmlContentWithComment(String comment) { + private String buildXmlContentWithComment(String comment) { int indexOfPreambleEnd = effectiveSolverConfigXml.indexOf("?>"); if (indexOfPreambleEnd != -1) { return effectiveSolverConfigXml.substring(0, indexOfPreambleEnd + 2) + diff --git a/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/devui/TimefoldDevUIRecorder.java b/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/devui/TimefoldDevUIRecorder.java new file mode 100644 index 0000000000..1411a31792 --- /dev/null +++ b/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/devui/TimefoldDevUIRecorder.java @@ -0,0 +1,16 @@ +package ai.timefold.solver.quarkus.devui; + +import java.util.function.Supplier; + +import io.quarkus.runtime.annotations.Recorder; + +@Recorder +public class TimefoldDevUIRecorder { + + public Supplier solverConfigTextSupplier(final String solverConfigText) { + return () -> { + return new SolverConfigText(solverConfigText); + }; + } + +}