diff --git a/optaplanner-quarkus-integration/optaplanner-quarkus/deployment/src/main/java/org/optaplanner/quarkus/deployment/OptaPlannerProcessor.java b/optaplanner-quarkus-integration/optaplanner-quarkus/deployment/src/main/java/org/optaplanner/quarkus/deployment/OptaPlannerProcessor.java index 4a750853374..46f8e2baae0 100644 --- a/optaplanner-quarkus-integration/optaplanner-quarkus/deployment/src/main/java/org/optaplanner/quarkus/deployment/OptaPlannerProcessor.java +++ b/optaplanner-quarkus-integration/optaplanner-quarkus/deployment/src/main/java/org/optaplanner/quarkus/deployment/OptaPlannerProcessor.java @@ -18,6 +18,7 @@ import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT; +import java.io.StringWriter; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collection; @@ -54,10 +55,12 @@ import org.optaplanner.core.config.solver.SolverConfig; import org.optaplanner.core.config.solver.SolverManagerConfig; import org.optaplanner.core.impl.domain.solution.descriptor.SolutionDescriptor; +import org.optaplanner.core.impl.io.jaxb.SolverConfigIO; import org.optaplanner.quarkus.OptaPlannerBeanProvider; import org.optaplanner.quarkus.OptaPlannerRecorder; import org.optaplanner.quarkus.config.OptaPlannerRuntimeConfig; import org.optaplanner.quarkus.deployment.config.OptaPlannerBuildTimeConfig; +import org.optaplanner.quarkus.devui.OptaPlannerDevUIPropertiesSupplier; import org.optaplanner.quarkus.gizmo.OptaPlannerGizmoBeanFactory; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; @@ -67,6 +70,7 @@ import io.quarkus.arc.deployment.UnremovableBeanBuildItem; import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.GeneratedClassGizmoAdaptor; +import io.quarkus.deployment.IsDevelopment; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.Record; @@ -79,6 +83,7 @@ import io.quarkus.deployment.builditem.nativeimage.ReflectiveHierarchyBuildItem; import io.quarkus.deployment.pkg.steps.NativeBuild; import io.quarkus.deployment.recording.RecorderContext; +import io.quarkus.devconsole.spi.DevConsoleRuntimeTemplateInfoBuildItem; import io.quarkus.gizmo.ClassOutput; import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; @@ -120,9 +125,24 @@ void makeGizmoBeanFactoryUnremovable(BuildProducer unr unremovableBeans.produce(UnremovableBeanBuildItem.beanTypes(OptaPlannerGizmoBeanFactory.class)); } + @BuildStep(onlyIf = IsDevelopment.class) + public DevConsoleRuntimeTemplateInfoBuildItem getSolverConfig(SolverConfigBuildStep solverConfigBuildStep) { + SolverConfig solverConfig = solverConfigBuildStep.getSolverConfig(); + if (solverConfig != null) { + StringWriter effectiveSolverConfigWriter = new StringWriter(); + SolverConfigIO solverConfigIO = new SolverConfigIO(); + solverConfigIO.write(solverConfig, effectiveSolverConfigWriter); + return new DevConsoleRuntimeTemplateInfoBuildItem("solverConfigProperties", + new OptaPlannerDevUIPropertiesSupplier(effectiveSolverConfigWriter.toString())); + } else { + return new DevConsoleRuntimeTemplateInfoBuildItem("solverConfigProperties", + new OptaPlannerDevUIPropertiesSupplier()); + } + } + @BuildStep @Record(STATIC_INIT) - void recordAndRegisterBeans(OptaPlannerRecorder recorder, RecorderContext recorderContext, + SolverConfigBuildStep recordAndRegisterBeans(OptaPlannerRecorder recorder, RecorderContext recorderContext, CombinedIndexBuildItem combinedIndex, Capabilities capabilities, BuildProducer reflectiveHierarchyClass, BuildProducer syntheticBeanBuildItemBuildProducer, @@ -142,7 +162,7 @@ void recordAndRegisterBeans(OptaPlannerRecorder recorder, RecorderContext record + " the Jandex index by using the jandex-maven-plugin in that dependency, or by adding" + "application.properties entries (quarkus.index-dependency..group-id" + " and quarkus.index-dependency..artifact-id)."); - return; + return new SolverConfigBuildStep(null); } // Quarkus extensions must always use getContextClassLoader() @@ -211,6 +231,7 @@ void recordAndRegisterBeans(OptaPlannerRecorder recorder, RecorderContext record additionalBeans.produce(new AdditionalBeanBuildItem(OptaPlannerBeanProvider.class)); unremovableBeans.produce(UnremovableBeanBuildItem.beanTypes(OptaPlannerRuntimeConfig.class)); + return new SolverConfigBuildStep(solverConfig); } private void generateConstraintVerifier(SolverConfig solverConfig, diff --git a/optaplanner-quarkus-integration/optaplanner-quarkus/deployment/src/main/java/org/optaplanner/quarkus/deployment/SolverConfigBuildStep.java b/optaplanner-quarkus-integration/optaplanner-quarkus/deployment/src/main/java/org/optaplanner/quarkus/deployment/SolverConfigBuildStep.java new file mode 100644 index 00000000000..98f49e7ff50 --- /dev/null +++ b/optaplanner-quarkus-integration/optaplanner-quarkus/deployment/src/main/java/org/optaplanner/quarkus/deployment/SolverConfigBuildStep.java @@ -0,0 +1,33 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.optaplanner.quarkus.deployment; + +import org.optaplanner.core.config.solver.SolverConfig; + +import io.quarkus.builder.item.SimpleBuildItem; + +public final class SolverConfigBuildStep extends SimpleBuildItem { + SolverConfig solverConfig; + + public SolverConfigBuildStep(SolverConfig solverConfig) { + this.solverConfig = solverConfig; + } + + public SolverConfig getSolverConfig() { + return solverConfig; + } +} diff --git a/optaplanner-quarkus-integration/optaplanner-quarkus/deployment/src/main/resources/dev-templates/constraints.html b/optaplanner-quarkus-integration/optaplanner-quarkus/deployment/src/main/resources/dev-templates/constraints.html new file mode 100644 index 00000000000..4a301c48961 --- /dev/null +++ b/optaplanner-quarkus-integration/optaplanner-quarkus/deployment/src/main/resources/dev-templates/constraints.html @@ -0,0 +1,35 @@ + + +{#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/optaplanner-quarkus-integration/optaplanner-quarkus/deployment/src/main/resources/dev-templates/embedded.html b/optaplanner-quarkus-integration/optaplanner-quarkus/deployment/src/main/resources/dev-templates/embedded.html new file mode 100644 index 00000000000..15ac5b85f2a --- /dev/null +++ b/optaplanner-quarkus-integration/optaplanner-quarkus/deployment/src/main/resources/dev-templates/embedded.html @@ -0,0 +1,27 @@ + + + + View Effective Solver Configuration + + + + View Model + + + + View Constraints + \ No newline at end of file diff --git a/optaplanner-quarkus-integration/optaplanner-quarkus/deployment/src/main/resources/dev-templates/model.html b/optaplanner-quarkus-integration/optaplanner-quarkus/deployment/src/main/resources/dev-templates/model.html new file mode 100644 index 00000000000..a4f4b73b372 --- /dev/null +++ b/optaplanner-quarkus-integration/optaplanner-quarkus/deployment/src/main/resources/dev-templates/model.html @@ -0,0 +1,78 @@ + +{#include main} + +{#title}Model{/title} + + + +{#body} +
+
Solution: {info:solverConfigProperties.optaPlannerModelProperties.solutionClass}
+
+ {#for entityClass in info:solverConfigProperties.optaPlannerModelProperties.entityClassList} +
+
Entity: {entityClass}
+
+ {#if !info:solverConfigProperties.optaPlannerModelProperties.entityClassToGenuineVariableListMap.get(entityClass).isEmpty()} + + + + + + + + {#for genuineVariable in info:solverConfigProperties.optaPlannerModelProperties.entityClassToGenuineVariableListMap.get(entityClass)} + + {/for} + +
Genuine Variables
{genuineVariable}
+ {/if} + {#if !info:solverConfigProperties.optaPlannerModelProperties.entityClassToShadowVariableListMap.get(entityClass).isEmpty()} + + + + + + + + {#for shadowVariable in info:solverConfigProperties.optaPlannerModelProperties.entityClassToShadowVariableListMap.get(entityClass)} + + {/for} + +
Shadow Variables
{shadowVariable}
+ {/if} +
+
+ {/for} +
+
+{/body} +{/include} \ No newline at end of file diff --git a/optaplanner-quarkus-integration/optaplanner-quarkus/deployment/src/main/resources/dev-templates/solverConfig.html b/optaplanner-quarkus-integration/optaplanner-quarkus/deployment/src/main/resources/dev-templates/solverConfig.html new file mode 100644 index 00000000000..c8f67dd22d9 --- /dev/null +++ b/optaplanner-quarkus-integration/optaplanner-quarkus/deployment/src/main/resources/dev-templates/solverConfig.html @@ -0,0 +1,24 @@ + +{#include main} + +{#title}Solver Configuration{/title} +{#body} +
+{info:solverConfigProperties.effectiveSolverConfig}
+
+{/body} +{/include} \ No newline at end of file diff --git a/optaplanner-quarkus-integration/optaplanner-quarkus/devui-integration-test/.gitignore b/optaplanner-quarkus-integration/optaplanner-quarkus/devui-integration-test/.gitignore new file mode 100644 index 00000000000..222af0cf114 --- /dev/null +++ b/optaplanner-quarkus-integration/optaplanner-quarkus/devui-integration-test/.gitignore @@ -0,0 +1,10 @@ +/target +/local + +# Eclipse, Netbeans and IntelliJ files +/.* +!.gitignore +/nbproject +/*.ipr +/*.iws +/*.iml diff --git a/optaplanner-quarkus-integration/optaplanner-quarkus/devui-integration-test/pom.xml b/optaplanner-quarkus-integration/optaplanner-quarkus/devui-integration-test/pom.xml new file mode 100644 index 00000000000..23f59a9efc4 --- /dev/null +++ b/optaplanner-quarkus-integration/optaplanner-quarkus/devui-integration-test/pom.xml @@ -0,0 +1,125 @@ + + + + 4.0.0 + + org.optaplanner + optaplanner-quarkus-parent + 8.10.0-SNAPSHOT + + + optaplanner-quarkus-devui-integration-test + OptaPlanner Quarkus - Dev UI Integration tests + Quarkus Dev UI integration tests for OptaPlanner + + + org.optaplanner.quarkus + + + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-resteasy + + + org.optaplanner + optaplanner-quarkus + + + + + org.assertj + assertj-core + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + io.quarkus + quarkus-junit5-internal + test + + + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + + analyze-only + none + + + + + maven-failsafe-plugin + + true + + + + + + + + native + + + native + + + + + + maven-failsafe-plugin + + false + + + + + integration-test + verify + + + + ${project.build.directory}/${project.build.finalName}-runner + + + + + + + + + native + + + + diff --git a/optaplanner-quarkus-integration/optaplanner-quarkus/devui-integration-test/src/main/java/org/optaplanner/quarkus/it/devui/OptaPlannerTestResource.java b/optaplanner-quarkus-integration/optaplanner-quarkus/devui-integration-test/src/main/java/org/optaplanner/quarkus/it/devui/OptaPlannerTestResource.java new file mode 100644 index 00000000000..ccaa90d797e --- /dev/null +++ b/optaplanner-quarkus-integration/optaplanner-quarkus/devui-integration-test/src/main/java/org/optaplanner/quarkus/it/devui/OptaPlannerTestResource.java @@ -0,0 +1,56 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.optaplanner.quarkus.it.devui; + +import java.util.Arrays; +import java.util.concurrent.ExecutionException; + +import javax.inject.Inject; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import org.optaplanner.core.api.solver.SolverJob; +import org.optaplanner.core.api.solver.SolverManager; +import org.optaplanner.quarkus.it.devui.domain.TestdataStringLengthShadowEntity; +import org.optaplanner.quarkus.it.devui.domain.TestdataStringLengthShadowSolution; + +@Path("/optaplanner/test") +public class OptaPlannerTestResource { + + @Inject + SolverManager solverManager; + + @POST + @Path("/solver-factory") + @Produces(MediaType.TEXT_PLAIN) + public String solveWithSolverFactory() { + TestdataStringLengthShadowSolution planningProblem = new TestdataStringLengthShadowSolution(); + planningProblem.setEntityList(Arrays.asList( + new TestdataStringLengthShadowEntity(), + new TestdataStringLengthShadowEntity())); + planningProblem.setValueList(Arrays.asList("a", "bb", "ccc")); + SolverJob solverJob = solverManager.solve(1L, planningProblem); + try { + return solverJob.getFinalBestSolution().getScore().toString(); + } catch (InterruptedException | ExecutionException e) { + throw new IllegalStateException("Solving failed.", e); + } + } + +} diff --git a/optaplanner-quarkus-integration/optaplanner-quarkus/devui-integration-test/src/main/java/org/optaplanner/quarkus/it/devui/domain/StringLengthVariableListener.java b/optaplanner-quarkus-integration/optaplanner-quarkus/devui-integration-test/src/main/java/org/optaplanner/quarkus/it/devui/domain/StringLengthVariableListener.java new file mode 100644 index 00000000000..aeacdcfdd56 --- /dev/null +++ b/optaplanner-quarkus-integration/optaplanner-quarkus/devui-integration-test/src/main/java/org/optaplanner/quarkus/it/devui/domain/StringLengthVariableListener.java @@ -0,0 +1,74 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.optaplanner.quarkus.it.devui.domain; + +import org.optaplanner.core.api.domain.variable.VariableListener; +import org.optaplanner.core.api.score.director.ScoreDirector; + +public class StringLengthVariableListener + implements VariableListener { + + @Override + public void beforeEntityAdded(ScoreDirector scoreDirector, + TestdataStringLengthShadowEntity entity) { + /* Nothing to do */ + } + + @Override + public void afterEntityAdded(ScoreDirector scoreDirector, + TestdataStringLengthShadowEntity entity) { + /* Nothing to do */ + } + + @Override + public void beforeVariableChanged(ScoreDirector scoreDirector, + TestdataStringLengthShadowEntity entity) { + /* Nothing to do */ + } + + @Override + public void afterVariableChanged(ScoreDirector scoreDirector, + TestdataStringLengthShadowEntity entity) { + int oldLength = (entity.getLength() != null) ? entity.getLength() : 0; + int newLength = getLength(entity.getValue()); + if (oldLength != newLength) { + scoreDirector.beforeVariableChanged(entity, "length"); + entity.setLength(getLength(entity.getValue())); + scoreDirector.afterVariableChanged(entity, "length"); + } + } + + @Override + public void beforeEntityRemoved(ScoreDirector scoreDirector, + TestdataStringLengthShadowEntity entity) { + /* Nothing to do */ + } + + @Override + public void afterEntityRemoved(ScoreDirector scoreDirector, + TestdataStringLengthShadowEntity entity) { + /* Nothing to do */ + } + + private static int getLength(String value) { + if (value != null) { + return value.length(); + } else { + return 0; + } + } +} diff --git a/optaplanner-quarkus-integration/optaplanner-quarkus/devui-integration-test/src/main/java/org/optaplanner/quarkus/it/devui/domain/TestdataStringLengthShadowEntity.java b/optaplanner-quarkus-integration/optaplanner-quarkus/devui-integration-test/src/main/java/org/optaplanner/quarkus/it/devui/domain/TestdataStringLengthShadowEntity.java new file mode 100644 index 00000000000..3eb55b7b7f9 --- /dev/null +++ b/optaplanner-quarkus-integration/optaplanner-quarkus/devui-integration-test/src/main/java/org/optaplanner/quarkus/it/devui/domain/TestdataStringLengthShadowEntity.java @@ -0,0 +1,57 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.optaplanner.quarkus.it.devui.domain; + +import org.optaplanner.core.api.domain.entity.PlanningEntity; +import org.optaplanner.core.api.domain.variable.CustomShadowVariable; +import org.optaplanner.core.api.domain.variable.PlanningVariable; +import org.optaplanner.core.api.domain.variable.PlanningVariableReference; + +@PlanningEntity +public class TestdataStringLengthShadowEntity { + + @PlanningVariable(valueRangeProviderRefs = "valueRange") + private String value; + + @CustomShadowVariable(variableListenerClass = StringLengthVariableListener.class, + sources = { + @PlanningVariableReference(entityClass = TestdataStringLengthShadowEntity.class, + variableName = "value") + }) + private Integer length; + + // ************************************************************************ + // Getters/setters + // ************************************************************************ + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public Integer getLength() { + return length; + } + + public void setLength(Integer length) { + this.length = length; + } + +} diff --git a/optaplanner-quarkus-integration/optaplanner-quarkus/devui-integration-test/src/main/java/org/optaplanner/quarkus/it/devui/domain/TestdataStringLengthShadowSolution.java b/optaplanner-quarkus-integration/optaplanner-quarkus/devui-integration-test/src/main/java/org/optaplanner/quarkus/it/devui/domain/TestdataStringLengthShadowSolution.java new file mode 100644 index 00000000000..5cca4f0e825 --- /dev/null +++ b/optaplanner-quarkus-integration/optaplanner-quarkus/devui-integration-test/src/main/java/org/optaplanner/quarkus/it/devui/domain/TestdataStringLengthShadowSolution.java @@ -0,0 +1,65 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.optaplanner.quarkus.it.devui.domain; + +import java.util.List; + +import org.optaplanner.core.api.domain.solution.PlanningEntityCollectionProperty; +import org.optaplanner.core.api.domain.solution.PlanningScore; +import org.optaplanner.core.api.domain.solution.PlanningSolution; +import org.optaplanner.core.api.domain.valuerange.ValueRangeProvider; +import org.optaplanner.core.api.score.buildin.hardsoft.HardSoftScore; + +@PlanningSolution +public class TestdataStringLengthShadowSolution { + + @ValueRangeProvider(id = "valueRange") + private List valueList; + @PlanningEntityCollectionProperty + private List entityList; + + @PlanningScore + private HardSoftScore score; + + // ************************************************************************ + // Getters/setters + // ************************************************************************ + + public List getValueList() { + return valueList; + } + + public void setValueList(List valueList) { + this.valueList = valueList; + } + + public List getEntityList() { + return entityList; + } + + public void setEntityList(List entityList) { + this.entityList = entityList; + } + + public HardSoftScore getScore() { + return score; + } + + public void setScore(HardSoftScore score) { + this.score = score; + } +} diff --git a/optaplanner-quarkus-integration/optaplanner-quarkus/devui-integration-test/src/main/java/org/optaplanner/quarkus/it/devui/solver/TestdataStringLengthConstraintProvider.java b/optaplanner-quarkus-integration/optaplanner-quarkus/devui-integration-test/src/main/java/org/optaplanner/quarkus/it/devui/solver/TestdataStringLengthConstraintProvider.java new file mode 100644 index 00000000000..8394e28fd3d --- /dev/null +++ b/optaplanner-quarkus-integration/optaplanner-quarkus/devui-integration-test/src/main/java/org/optaplanner/quarkus/it/devui/solver/TestdataStringLengthConstraintProvider.java @@ -0,0 +1,41 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.optaplanner.quarkus.it.devui.solver; + +import org.optaplanner.core.api.score.buildin.hardsoft.HardSoftScore; +import org.optaplanner.core.api.score.stream.Constraint; +import org.optaplanner.core.api.score.stream.ConstraintFactory; +import org.optaplanner.core.api.score.stream.ConstraintProvider; +import org.optaplanner.core.api.score.stream.Joiners; +import org.optaplanner.quarkus.it.devui.domain.TestdataStringLengthShadowEntity; + +public class TestdataStringLengthConstraintProvider implements ConstraintProvider { + + @Override + public Constraint[] defineConstraints(ConstraintFactory factory) { + return new Constraint[] { + factory.from(TestdataStringLengthShadowEntity.class) + .join(TestdataStringLengthShadowEntity.class, Joiners.equal(TestdataStringLengthShadowEntity::getValue)) + .filter((a, b) -> a != b) + .penalize("Don't assign 2 entities the same value.", HardSoftScore.ONE_HARD), + factory.from(TestdataStringLengthShadowEntity.class) + .reward("Maximize value length", HardSoftScore.ONE_SOFT, + TestdataStringLengthShadowEntity::getLength) + }; + } + +} diff --git a/optaplanner-quarkus-integration/optaplanner-quarkus/devui-integration-test/src/main/resources/application.properties b/optaplanner-quarkus-integration/optaplanner-quarkus/devui-integration-test/src/main/resources/application.properties new file mode 100644 index 00000000000..71783ae04b6 --- /dev/null +++ b/optaplanner-quarkus-integration/optaplanner-quarkus/devui-integration-test/src/main/resources/application.properties @@ -0,0 +1,18 @@ +# +# Copyright 2020 Red Hat, Inc. and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# The solver runs only for few milliseconds to avoid a HTTP timeout in this simple implementation. +quarkus.optaplanner.solver.termination.best-score-limit=0hard/5soft diff --git a/optaplanner-quarkus-integration/optaplanner-quarkus/devui-integration-test/src/test/java/org/optaplanner/quarkus/it/devui/OptaPlannerDevUITest.java b/optaplanner-quarkus-integration/optaplanner-quarkus/devui-integration-test/src/test/java/org/optaplanner/quarkus/it/devui/OptaPlannerDevUITest.java new file mode 100644 index 00000000000..ddc86007d7e --- /dev/null +++ b/optaplanner-quarkus-integration/optaplanner-quarkus/devui-integration-test/src/test/java/org/optaplanner/quarkus/it/devui/OptaPlannerDevUITest.java @@ -0,0 +1,113 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.optaplanner.quarkus.it.devui; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; + +import javax.xml.parsers.ParserConfigurationException; + +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.namespace.QName; +import groovy.util.Node; +import groovy.xml.XmlParser; +import io.quarkus.test.QuarkusDevModeTest; +import io.restassured.RestAssured; + +public class OptaPlannerDevUITest { + @RegisterExtension + static final QuarkusDevModeTest config = new QuarkusDevModeTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addPackages(true, "org.optaplanner.quarkus.it.devui")); + + static final String OPTAPLANNER_DEV_UI_BASE_URL = "/q/dev/org.optaplanner.optaplanner-quarkus/"; + + public static String getPage(String pageName) { + return OPTAPLANNER_DEV_UI_BASE_URL + pageName; + } + + @Test + public 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 = ((Node) (node.getAt(QName.valueOf("body")) + .getAt(QName.valueOf("div")) + .getAt(QName.valueOf("pre")) + .get(0))).text(); + assertThat(solverConfig).isEqualToIgnoringWhitespace( + "\n" + + "\n" + + "\n" + + " org.optaplanner.quarkus.it.devui.domain.TestdataStringLengthShadowSolution\n" + + " org.optaplanner.quarkus.it.devui.domain.TestdataStringLengthShadowEntity\n" + + " GIZMO\n" + + " \n" + + " org.optaplanner.quarkus.it.devui.solver.TestdataStringLengthConstraintProvider\n" + + " \n" + + ""); + } + + @Test + public 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 = ((Node) (node.getAt(QName.valueOf("body")) + .getAt(QName.valueOf("div")) + .getAt(QName.valueOf("div")) + .get(0))).toString(); + assertThat(model) + .contains("value=[Solution: org.optaplanner.quarkus.it.devui.domain.TestdataStringLengthShadowSolution]"); + assertThat(model).contains("value=[Entity: org.optaplanner.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]]"); + } + + @Test + public 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 = ((Node) (node.getAt(QName.valueOf("body")) + .getAt(QName.valueOf("div")) + .getAt(QName.valueOf("table")) + .get(0))).text(); + assertThat(constraints).contains("org.optaplanner.quarkus.it.devui.domain/Don't assign 2 entities the same value"); + assertThat(constraints).contains("org.optaplanner.quarkus.it.devui.domain/Maximize value length"); + } +} diff --git a/optaplanner-quarkus-integration/optaplanner-quarkus/pom.xml b/optaplanner-quarkus-integration/optaplanner-quarkus/pom.xml index 9eba39ae61d..a31cd25e36a 100644 --- a/optaplanner-quarkus-integration/optaplanner-quarkus/pom.xml +++ b/optaplanner-quarkus-integration/optaplanner-quarkus/pom.xml @@ -17,6 +17,7 @@ runtime deployment integration-test + devui-integration-test drl-integration-test diff --git a/optaplanner-quarkus-integration/optaplanner-quarkus/runtime/src/main/java/org/optaplanner/quarkus/devui/OptaPlannerDevUIProperties.java b/optaplanner-quarkus-integration/optaplanner-quarkus/runtime/src/main/java/org/optaplanner/quarkus/devui/OptaPlannerDevUIProperties.java new file mode 100644 index 00000000000..3e12aa3cb93 --- /dev/null +++ b/optaplanner-quarkus-integration/optaplanner-quarkus/runtime/src/main/java/org/optaplanner/quarkus/devui/OptaPlannerDevUIProperties.java @@ -0,0 +1,44 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.optaplanner.quarkus.devui; + +import java.util.List; + +public class OptaPlannerDevUIProperties { + private final OptaPlannerModelProperties optaPlannerModelProperties; + private final String effectiveSolverConfigXML; + private final List constraintList; + + public OptaPlannerDevUIProperties(OptaPlannerModelProperties optaPlannerModelProperties, String effectiveSolverConfigXML, + List constraintList) { + this.optaPlannerModelProperties = optaPlannerModelProperties; + this.effectiveSolverConfigXML = effectiveSolverConfigXML; + this.constraintList = constraintList; + } + + public OptaPlannerModelProperties getOptaPlannerModelProperties() { + return optaPlannerModelProperties; + } + + public String getEffectiveSolverConfig() { + return effectiveSolverConfigXML; + } + + public List getConstraintList() { + return constraintList; + } +} diff --git a/optaplanner-quarkus-integration/optaplanner-quarkus/runtime/src/main/java/org/optaplanner/quarkus/devui/OptaPlannerDevUIPropertiesSupplier.java b/optaplanner-quarkus-integration/optaplanner-quarkus/runtime/src/main/java/org/optaplanner/quarkus/devui/OptaPlannerDevUIPropertiesSupplier.java new file mode 100644 index 00000000000..e160b2d0afe --- /dev/null +++ b/optaplanner-quarkus-integration/optaplanner-quarkus/runtime/src/main/java/org/optaplanner/quarkus/devui/OptaPlannerDevUIPropertiesSupplier.java @@ -0,0 +1,138 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.optaplanner.quarkus.devui; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import org.optaplanner.core.api.domain.entity.PlanningEntity; +import org.optaplanner.core.api.domain.solution.PlanningSolution; +import org.optaplanner.core.api.score.stream.Constraint; +import org.optaplanner.core.api.solver.SolverFactory; +import org.optaplanner.core.impl.domain.entity.descriptor.EntityDescriptor; +import org.optaplanner.core.impl.domain.solution.descriptor.SolutionDescriptor; +import org.optaplanner.core.impl.domain.variable.descriptor.GenuineVariableDescriptor; +import org.optaplanner.core.impl.domain.variable.descriptor.VariableDescriptor; +import org.optaplanner.core.impl.score.director.stream.AbstractConstraintStreamScoreDirectorFactory; +import org.optaplanner.core.impl.solver.DefaultSolverFactory; + +import io.quarkus.arc.Arc; + +public class OptaPlannerDevUIPropertiesSupplier implements Supplier { + private String effectiveSolverConfigXml; + + public OptaPlannerDevUIPropertiesSupplier() { + this.effectiveSolverConfigXml = null; + } + + public OptaPlannerDevUIPropertiesSupplier(String effectiveSolverConfigXml) { + this.effectiveSolverConfigXml = effectiveSolverConfigXml; + } + + // Needed for Quarkus Dev UI serialization + public String getEffectiveSolverConfigXml() { + return effectiveSolverConfigXml; + } + + public void setEffectiveSolverConfigXml(String effectiveSolverConfigXml) { + this.effectiveSolverConfigXml = effectiveSolverConfigXml; + } + + @Override + public OptaPlannerDevUIProperties get() { + 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 OptaPlannerDevUIProperties(getModelInfo(), + getXmlContentWithComment("Properties that can be set at runtime are not included"), + getConstraintList()); + } else { + return new OptaPlannerDevUIProperties(getModelInfo(), + "\n", + Collections.emptyList()); + } + } + + private OptaPlannerModelProperties getModelInfo() { + if (effectiveSolverConfigXml != null) { + DefaultSolverFactory solverFactory = + (DefaultSolverFactory) Arc.container().instance(SolverFactory.class).get(); + SolutionDescriptor solutionDescriptor = solverFactory.getScoreDirectorFactory().getSolutionDescriptor(); + OptaPlannerModelProperties out = new OptaPlannerModelProperties(); + out.setSolutionClass(solutionDescriptor.getSolutionClass().getName()); + List entityClassList = new ArrayList<>(); + Map> entityClassToGenuineVariableListMap = new HashMap<>(); + Map> entityClassToShadowVariableListMap = new HashMap<>(); + for (EntityDescriptor entityDescriptor : solutionDescriptor.getEntityDescriptors()) { + entityClassList.add(entityDescriptor.getEntityClass().getName()); + List entityClassToGenuineVariableList = new ArrayList<>(); + List entityClassToShadowVariableList = new ArrayList<>(); + for (VariableDescriptor variableDescriptor : entityDescriptor.getDeclaredVariableDescriptors()) { + if (variableDescriptor instanceof GenuineVariableDescriptor) { + entityClassToGenuineVariableList.add(variableDescriptor.getVariableName()); + } else { + entityClassToShadowVariableList.add(variableDescriptor.getVariableName()); + } + } + entityClassToGenuineVariableListMap.put(entityDescriptor.getEntityClass().getName(), + entityClassToGenuineVariableList); + entityClassToShadowVariableListMap.put(entityDescriptor.getEntityClass().getName(), + entityClassToShadowVariableList); + } + out.setEntityClassList(entityClassList); + out.setEntityClassToGenuineVariableListMap(entityClassToGenuineVariableListMap); + out.setEntityClassToShadowVariableListMap(entityClassToShadowVariableListMap); + return out; + } else { + return new OptaPlannerModelProperties(); + } + } + + private List getConstraintList() { + if (effectiveSolverConfigXml != null) { + DefaultSolverFactory solverFactory = + (DefaultSolverFactory) Arc.container().instance(SolverFactory.class).get(); + if (solverFactory.getScoreDirectorFactory() instanceof AbstractConstraintStreamScoreDirectorFactory) { + AbstractConstraintStreamScoreDirectorFactory scoreDirectorFactory = + (AbstractConstraintStreamScoreDirectorFactory) solverFactory.getScoreDirectorFactory(); + return Arrays.stream(scoreDirectorFactory.getConstraints()).map(Constraint::getConstraintId) + .collect(Collectors.toList()); + } + } + return Collections.emptyList(); + } + + private String getXmlContentWithComment(String comment) { + int indexOfPreambleEnd = effectiveSolverConfigXml.indexOf("?>"); + if (indexOfPreambleEnd != -1) { + return effectiveSolverConfigXml.substring(0, indexOfPreambleEnd + 2) + + "\n\n" + + effectiveSolverConfigXml.substring(indexOfPreambleEnd + 2); + } else { + return "\n" + effectiveSolverConfigXml; + } + } +} diff --git a/optaplanner-quarkus-integration/optaplanner-quarkus/runtime/src/main/java/org/optaplanner/quarkus/devui/OptaPlannerModelProperties.java b/optaplanner-quarkus-integration/optaplanner-quarkus/runtime/src/main/java/org/optaplanner/quarkus/devui/OptaPlannerModelProperties.java new file mode 100644 index 00000000000..3d3f79a4456 --- /dev/null +++ b/optaplanner-quarkus-integration/optaplanner-quarkus/runtime/src/main/java/org/optaplanner/quarkus/devui/OptaPlannerModelProperties.java @@ -0,0 +1,69 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.optaplanner.quarkus.devui; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class OptaPlannerModelProperties { + String solutionClass; + List entityClassList; + Map> entityClassToGenuineVariableListMap; + Map> entityClassToShadowVariableListMap; + + public OptaPlannerModelProperties() { + solutionClass = "null"; + entityClassList = Collections.emptyList(); + entityClassToGenuineVariableListMap = Collections.emptyMap(); + entityClassToShadowVariableListMap = Collections.emptyMap(); + } + + public String getSolutionClass() { + return solutionClass; + } + + public void setSolutionClass(String solutionClass) { + this.solutionClass = solutionClass; + } + + public List getEntityClassList() { + return entityClassList; + } + + public void setEntityClassList(List entityClassList) { + this.entityClassList = entityClassList; + } + + public Map> getEntityClassToGenuineVariableListMap() { + return entityClassToGenuineVariableListMap; + } + + public void setEntityClassToGenuineVariableListMap( + Map> entityClassToGenuineVariableListMap) { + this.entityClassToGenuineVariableListMap = entityClassToGenuineVariableListMap; + } + + public Map> getEntityClassToShadowVariableListMap() { + return entityClassToShadowVariableListMap; + } + + public void setEntityClassToShadowVariableListMap( + Map> entityClassToShadowVariableListMap) { + this.entityClassToShadowVariableListMap = entityClassToShadowVariableListMap; + } +}