diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/StatisticRegistry.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/StatisticRegistry.java index 7dff56c13a..4172e64824 100644 --- a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/StatisticRegistry.java +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/StatisticRegistry.java @@ -3,7 +3,6 @@ import java.math.BigDecimal; import java.math.BigInteger; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -20,7 +19,6 @@ import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope; import ai.timefold.solver.core.impl.score.definition.ScoreDefinition; import ai.timefold.solver.core.impl.solver.scope.SolverScope; -import ai.timefold.solver.core.impl.util.Pair; import io.micrometer.core.instrument.Gauge; import io.micrometer.core.instrument.Meter; @@ -31,6 +29,9 @@ public class StatisticRegistry extends SimpleMeterRegistry implements PhaseLifecycleListener { + private static final String CONSTRAINT_PACKAGE_TAG = "constraint.package"; + private static final String CONSTRAINT_NAME_TAG = "constraint.name"; + List>> stepMeterListenerList = new ArrayList<>(); List>> bestSolutionMeterListenerList = new ArrayList<>(); AbstractStepScope bestSolutionStepScope = null; @@ -102,25 +103,21 @@ public void extractScoreFromMeters(SolverMetric metric, Tags runId, Consumer> constraintMatchTotalConsumer) { - Set meterIds = getMeterIds(metric, runId); - Set> constraintPackageNamePairs = new HashSet<>(); // Add the constraint ids from the meter ids - meterIds.forEach(meterId -> constraintPackageNamePairs - .add(new Pair<>(meterId.getTag("constraint.package"), meterId.getTag("constraint.name")))); - constraintPackageNamePairs.forEach(constraintPackageNamePair -> { - String constraintPackage = constraintPackageNamePair.key(); - String constraintName = constraintPackageNamePair.value(); - Tags constraintMatchTotalRunId = runId.and("constraint.package", constraintPackage) - .and("constraint.name", constraintName); - // Get the score from the corresponding constraint package and constraint name meters - extractScoreFromMeters(metric, constraintMatchTotalRunId, - // Get the count gauge (add constraint package and constraint name to the run tags) - score -> getGaugeValue(metric.getMeterId() + ".count", - constraintMatchTotalRunId, - count -> constraintMatchTotalConsumer.accept( - new ConstraintSummary(ConstraintRef.of(constraintPackage, constraintName), score, - count.intValue())))); - }); + getMeterIds(metric, runId) + .stream() + .map(meterId -> ConstraintRef.of(meterId.getTag(CONSTRAINT_PACKAGE_TAG), meterId.getTag(CONSTRAINT_NAME_TAG))) + .distinct() + .forEach(constraintRef -> { + var constraintMatchTotalRunId = runId.and(CONSTRAINT_PACKAGE_TAG, constraintRef.packageName()) + .and(CONSTRAINT_NAME_TAG, constraintRef.constraintName()); + // Get the score from the corresponding constraint package and constraint name meters + extractScoreFromMeters(metric, constraintMatchTotalRunId, + // Get the count gauge (add constraint package and constraint name to the run tags) + score -> getGaugeValue(metric.getMeterId() + ".count", constraintMatchTotalRunId, + count -> constraintMatchTotalConsumer + .accept(new ConstraintSummary(constraintRef, score, count.intValue())))); + }); } public void getGaugeValue(SolverMetric metric, Tags runId, Consumer gaugeConsumer) { diff --git a/core/src/main/java/ai/timefold/solver/core/api/domain/constraintweight/ConstraintConfiguration.java b/core/src/main/java/ai/timefold/solver/core/api/domain/constraintweight/ConstraintConfiguration.java index 7e72121e53..8d1c2a319a 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/domain/constraintweight/ConstraintConfiguration.java +++ b/core/src/main/java/ai/timefold/solver/core/api/domain/constraintweight/ConstraintConfiguration.java @@ -26,7 +26,9 @@ * This is the default for every {@link ConstraintWeight#constraintPackage()} in the annotated class. * * @return defaults to the annotated class's package. + * @deprecated Leave empty and let the solver provide the default. Do not rely on constraint package in user code. */ + @Deprecated(forRemoval = true, since = "1.13.0") String constraintPackage() default ""; } diff --git a/core/src/main/java/ai/timefold/solver/core/api/domain/constraintweight/ConstraintWeight.java b/core/src/main/java/ai/timefold/solver/core/api/domain/constraintweight/ConstraintWeight.java index a7a3cf2014..7c10ab33da 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/domain/constraintweight/ConstraintWeight.java +++ b/core/src/main/java/ai/timefold/solver/core/api/domain/constraintweight/ConstraintWeight.java @@ -28,7 +28,9 @@ * concatenated with "/" and {@link #value() the constraint name}. * * @return defaults to {@link ConstraintConfiguration#constraintPackage()} + * @deprecated Leave empty and let the solver provide the default. Do not rely on constraint package in user code. */ + @Deprecated(forRemoval = true, since = "1.13.0") String constraintPackage() default ""; /** diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/analysis/ConstraintAnalysis.java b/core/src/main/java/ai/timefold/solver/core/api/score/analysis/ConstraintAnalysis.java index 57acf86f75..05621ae14c 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/score/analysis/ConstraintAnalysis.java +++ b/core/src/main/java/ai/timefold/solver/core/api/score/analysis/ConstraintAnalysis.java @@ -160,7 +160,9 @@ static > ConstraintAnalysis diff( * Return package name of the constraint that this analysis is for. * * @return equal to {@code constraintRef.packageName()} + * @deprecated Do not rely on constraint package in user code. */ + @Deprecated(forRemoval = true, since = "1.13.0") public String constraintPackage() { return constraintRef.packageName(); } diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/analysis/ScoreAnalysis.java b/core/src/main/java/ai/timefold/solver/core/api/score/analysis/ScoreAnalysis.java index 756b333213..0454a36adc 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/score/analysis/ScoreAnalysis.java +++ b/core/src/main/java/ai/timefold/solver/core/api/score/analysis/ScoreAnalysis.java @@ -92,11 +92,41 @@ public ConstraintAnalysis getConstraintAnalysis(ConstraintRef constraint * @param constraintPackage never null * @param constraintName never null * @return null if no constraint matches of such constraint are present + * @deprecated Use {@link #getConstraintAnalysis(String)} instead. */ + @Deprecated(forRemoval = true, since = "1.13.0") public ConstraintAnalysis getConstraintAnalysis(String constraintPackage, String constraintName) { return getConstraintAnalysis(ConstraintRef.of(constraintPackage, constraintName)); } + /** + * As defined by {@link #getConstraintAnalysis(ConstraintRef)}. + * + * @param constraintName never null + * @return null if no constraint matches of such constraint are present + * @throws IllegalStateException if multiple constraints with the same name are present, + * which is possible if they are in different constraint packages. + * Constraint packages are deprecated, we recommend avoiding them and instead naming constraints uniquely. + * If you must use constraint packages, see {@link #getConstraintAnalysis(String, String)} + * (also deprecated) and reach out to us to discuss your use case. + */ + public ConstraintAnalysis getConstraintAnalysis(String constraintName) { + var constraintAnalysisList = constraintMap.entrySet() + .stream() + .filter(entry -> entry.getKey().constraintName().equals(constraintName)) + .map(Map.Entry::getValue) + .toList(); + return switch (constraintAnalysisList.size()) { + case 0 -> null; + case 1 -> constraintAnalysisList.get(0); + default -> throw new IllegalStateException(""" + Multiple constraints with the same name (%s) are present in the score analysis. + This may be caused by the use of multiple constraint packages, a deprecated feature. + Please avoid using constraint packages and keep constraint names unique.""" + .formatted(constraintName)); + }; + } + /** * Compare this {@link ScoreAnalysis} to another {@link ScoreAnalysis} * and retrieve the difference between them. diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/constraint/ConstraintRef.java b/core/src/main/java/ai/timefold/solver/core/api/score/constraint/ConstraintRef.java index 065441a608..41319b48ad 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/score/constraint/ConstraintRef.java +++ b/core/src/main/java/ai/timefold/solver/core/api/score/constraint/ConstraintRef.java @@ -14,6 +14,9 @@ * @param packageName The constraint package is the namespace of the constraint. * When using a {@link ConstraintConfiguration}, * it is equal to the {@link ConstraintWeight#constraintPackage()}. + * It is not recommended for the user to set this; + * instead, the user should use whatever the solver provided as default and not rely on this information at all. + * The entire concept of constraint package is likely to be removed in a future version of the solver. * @param constraintName The constraint name. * It might not be unique, but {@link #constraintId()} is unique. * When using a {@link ConstraintConfiguration}, diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/stream/ConstraintBuilder.java b/core/src/main/java/ai/timefold/solver/core/api/score/stream/ConstraintBuilder.java index 724b93bb29..6fd7974258 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/score/stream/ConstraintBuilder.java +++ b/core/src/main/java/ai/timefold/solver/core/api/score/stream/ConstraintBuilder.java @@ -21,7 +21,9 @@ public interface ConstraintBuilder { * @param constraintName never null, shows up in {@link ConstraintMatchTotal} during score justification * @param constraintPackage never null * @return never null + * @deprecated Constraint package should no longer be used, use {@link #asConstraint(String)} instead. */ + @Deprecated(forRemoval = true, since = "1.13.0") Constraint asConstraint(String constraintPackage, String constraintName); } diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/stream/ConstraintFactory.java b/core/src/main/java/ai/timefold/solver/core/api/score/stream/ConstraintFactory.java index 4f449f95b8..b41734a358 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/score/stream/ConstraintFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/api/score/stream/ConstraintFactory.java @@ -27,7 +27,9 @@ public interface ConstraintFactory { * otherwise the package of the {@link PlanningSolution} class. * * @return never null + * @deprecated Do not rely on any constraint package in user code. */ + @Deprecated(forRemoval = true, since = "1.13.0") String getDefaultConstraintPackage(); // ************************************************************************ diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraintFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraintFactory.java index d1673f11d7..42cdec499f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraintFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraintFactory.java @@ -20,6 +20,18 @@ public final class BavetConstraintFactory extends InnerConstraintFactory> { + /** + * Used for code in no package, also called the "unnamed package". + * Classes here can only be instantiated via reflection, + * they cannot be imported and used directly. + * But still, in corner cases such as Kotlin notebooks, + * all code is in the unnamed package. + * Assume a constraint provider under these conditions, + * where asConstraint(...) only specifies constraint name, not constraint package. + * In this situation, the default constraint package is used. + */ + private static final String DEFAULT_CONSTRAINT_PACKAGE = "unnamed.package"; + private final SolutionDescriptor solutionDescriptor; private final EnvironmentMode environmentMode; private final String defaultConstraintPackage; @@ -33,13 +45,21 @@ public BavetConstraintFactory(SolutionDescriptor solutionDescriptor, ConstraintConfigurationDescriptor configurationDescriptor = solutionDescriptor .getConstraintConfigurationDescriptor(); if (configurationDescriptor == null) { - Package pack = solutionDescriptor.getSolutionClass().getPackage(); - defaultConstraintPackage = (pack == null) ? "" : pack.getName(); + defaultConstraintPackage = determineDefaultConstraintPackage(solutionDescriptor.getSolutionClass().getPackage()); } else { - defaultConstraintPackage = configurationDescriptor.getConstraintPackage(); + defaultConstraintPackage = determineDefaultConstraintPackage(configurationDescriptor.getConstraintPackage()); } } + private static String determineDefaultConstraintPackage(Package pkg) { + var asString = pkg == null ? "" : pkg.getName(); + return determineDefaultConstraintPackage(asString); + } + + private static String determineDefaultConstraintPackage(String constraintPackage) { + return constraintPackage == null || constraintPackage.isEmpty() ? DEFAULT_CONSTRAINT_PACKAGE : constraintPackage; + } + public > Stream_ share(Stream_ stream) { return share(stream, t -> { }); diff --git a/core/src/test/java/TestdataInUnnamedPackageSolution.java b/core/src/test/java/TestdataInUnnamedPackageSolution.java new file mode 100644 index 0000000000..5a1dd1c479 --- /dev/null +++ b/core/src/test/java/TestdataInUnnamedPackageSolution.java @@ -0,0 +1,88 @@ +import java.util.ArrayList; +import java.util.List; + +import ai.timefold.solver.core.api.domain.solution.PlanningEntityCollectionProperty; +import ai.timefold.solver.core.api.domain.solution.PlanningScore; +import ai.timefold.solver.core.api.domain.solution.PlanningSolution; +import ai.timefold.solver.core.api.domain.solution.ProblemFactCollectionProperty; +import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider; +import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; +import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; +import ai.timefold.solver.core.impl.testdata.domain.TestdataEntity; +import ai.timefold.solver.core.impl.testdata.domain.TestdataObject; +import ai.timefold.solver.core.impl.testdata.domain.TestdataValue; + +@PlanningSolution +public class TestdataInUnnamedPackageSolution extends TestdataObject { + + public static SolutionDescriptor buildSolutionDescriptor() { + return SolutionDescriptor.buildSolutionDescriptor(TestdataInUnnamedPackageSolution.class, TestdataEntity.class); + } + + public static TestdataInUnnamedPackageSolution generateSolution() { + return generateSolution(5, 7); + } + + public static TestdataInUnnamedPackageSolution generateSolution(int valueListSize, int entityListSize) { + TestdataInUnnamedPackageSolution solution = new TestdataInUnnamedPackageSolution("Generated Solution 0"); + List valueList = new ArrayList<>(valueListSize); + for (int i = 0; i < valueListSize; i++) { + TestdataValue value = new TestdataValue("Generated Value " + i); + valueList.add(value); + } + solution.setValueList(valueList); + List entityList = new ArrayList<>(entityListSize); + for (int i = 0; i < entityListSize; i++) { + TestdataValue value = valueList.get(i % valueListSize); + TestdataEntity entity = new TestdataEntity("Generated Entity " + i, value); + entityList.add(entity); + } + solution.setEntityList(entityList); + return solution; + } + + private List valueList; + private List entityList; + + private SimpleScore score; + + public TestdataInUnnamedPackageSolution() { + } + + public TestdataInUnnamedPackageSolution(String code) { + super(code); + } + + @ValueRangeProvider(id = "valueRange") + @ProblemFactCollectionProperty + public List getValueList() { + return valueList; + } + + public void setValueList(List valueList) { + this.valueList = valueList; + } + + @PlanningEntityCollectionProperty + public List getEntityList() { + return entityList; + } + + public void setEntityList(List entityList) { + this.entityList = entityList; + } + + @PlanningScore + public SimpleScore getScore() { + return score; + } + + public void setScore(SimpleScore score) { + this.score = score; + } + + // ************************************************************************ + // Complex methods + // ************************************************************************ + +} diff --git a/core/src/test/java/ai/timefold/solver/core/api/score/analysis/ScoreAnalysisTest.java b/core/src/test/java/ai/timefold/solver/core/api/score/analysis/ScoreAnalysisTest.java index e10c45b08c..503a674bf4 100644 --- a/core/src/test/java/ai/timefold/solver/core/api/score/analysis/ScoreAnalysisTest.java +++ b/core/src/test/java/ai/timefold/solver/core/api/score/analysis/ScoreAnalysisTest.java @@ -81,7 +81,7 @@ Explanation of score (27): // Complete score analysis var summary = scoreAnalysis.summarize(); - assertThat(scoreAnalysis.getConstraintAnalysis(constraintPackage, constraintName1).matchCount()).isEqualTo(5); + assertThat(scoreAnalysis.getConstraintAnalysis(constraintName1).matchCount()).isEqualTo(5); assertThat(summary) .isEqualTo(""" Explanation of score (67): @@ -126,7 +126,7 @@ Explanation of score (0): // Complete score analysis var summary = scoreAnalysis.summarize(); - assertThat(scoreAnalysis.getConstraintAnalysis(constraintPackage, constraintName1).matchCount()).isZero(); + assertThat(scoreAnalysis.getConstraintAnalysis(constraintName1).matchCount()).isZero(); assertThat(summary) .isEqualTo(""" Explanation of score (3init/0): @@ -206,19 +206,19 @@ void compareWithConstraintMatchesWithoutMatchAnalysis() { constraintMatchTotal4.getConstraintRef()); }); // Matches for constraint1 not present. - var constraintAnalysis1 = comparison.getConstraintAnalysis(constraintPackage, constraintName1); + var constraintAnalysis1 = comparison.getConstraintAnalysis(constraintName1); assertSoftly(softly -> { softly.assertThat(constraintAnalysis1.score()).isEqualTo(SimpleScore.of(20)); softly.assertThat(constraintAnalysis1.matches()).isNull(); }); // Matches for constraint2 still not present. - var constraintAnalysis2 = comparison.getConstraintAnalysis(constraintPackage, constraintName2); + var constraintAnalysis2 = comparison.getConstraintAnalysis(constraintName2); assertSoftly(softly -> { softly.assertThat(constraintAnalysis2.score()).isEqualTo(SimpleScore.of(18)); softly.assertThat(constraintAnalysis2.matches()).isNull(); }); // Matches for constraint3 not present. - var constraintAnalysis3 = comparison.getConstraintAnalysis(constraintPackage, constraintName3); + var constraintAnalysis3 = comparison.getConstraintAnalysis(constraintName3); assertSoftly(softly -> { softly.assertThat(constraintAnalysis3.score()).isEqualTo(SimpleScore.of(-30)); softly.assertThat(constraintAnalysis3.matches()).isNull(); @@ -232,19 +232,19 @@ void compareWithConstraintMatchesWithoutMatchAnalysis() { constraintMatchTotal4.getConstraintRef()); }); // Matches for constraint1 not present. - var reverseConstraintAnalysis1 = reverseComparison.getConstraintAnalysis(constraintPackage, constraintName1); + var reverseConstraintAnalysis1 = reverseComparison.getConstraintAnalysis(constraintName1); assertSoftly(softly -> { softly.assertThat(reverseConstraintAnalysis1.score()).isEqualTo(SimpleScore.of(-20)); softly.assertThat(reverseConstraintAnalysis1.matches()).isNull(); }); // Matches for constraint2 still not present. - var reverseConstraintAnalysis2 = reverseComparison.getConstraintAnalysis(constraintPackage, constraintName2); + var reverseConstraintAnalysis2 = reverseComparison.getConstraintAnalysis(constraintName2); assertSoftly(softly -> { softly.assertThat(reverseConstraintAnalysis2.score()).isEqualTo(SimpleScore.of(-18)); softly.assertThat(reverseConstraintAnalysis2.matches()).isNull(); }); // Matches for constraint3 not present in reverse. - var reverseConstraintAnalysis3 = reverseComparison.getConstraintAnalysis(constraintPackage, constraintName3); + var reverseConstraintAnalysis3 = reverseComparison.getConstraintAnalysis(constraintName3); assertSoftly(softly -> { softly.assertThat(reverseConstraintAnalysis3.score()).isEqualTo(SimpleScore.of(30)); softly.assertThat(reverseConstraintAnalysis3.matches()).isNull(); @@ -302,7 +302,7 @@ void compareWithConstraintMatchesAndMatchAnalysis() { constraintMatchTotal4.getConstraintRef()); }); // Matches for constraint1 present. - var constraintAnalysis1 = comparison.getConstraintAnalysis(constraintPackage, constraintName1); + var constraintAnalysis1 = comparison.getConstraintAnalysis(constraintName1); assertSoftly(softly -> { softly.assertThat(constraintAnalysis1.score()).isEqualTo(SimpleScore.of(20)); var matchAnalyses = constraintAnalysis1.matches(); @@ -314,7 +314,7 @@ void compareWithConstraintMatchesAndMatchAnalysis() { matchAnalysisOf(constraintAnalysis1.constraintRef(), 8)); }); // Matches for constraint2 present in both. - var constraintAnalysis2 = comparison.getConstraintAnalysis(constraintPackage, constraintName2); + var constraintAnalysis2 = comparison.getConstraintAnalysis(constraintName2); assertSoftly(softly -> { softly.assertThat(constraintAnalysis2.score()).isEqualTo(SimpleScore.of(18)); var matchAnalyses = constraintAnalysis2.matches(); @@ -328,7 +328,7 @@ void compareWithConstraintMatchesAndMatchAnalysis() { matchAnalysisOf(constraintAnalysis2.constraintRef(), -6, "A", "B")); }); // Matches for constraint3 not present. - var constraintAnalysis3 = comparison.getConstraintAnalysis(constraintPackage, constraintName3); + var constraintAnalysis3 = comparison.getConstraintAnalysis(constraintName3); assertSoftly(softly -> { softly.assertThat(constraintAnalysis3.score()).isEqualTo(SimpleScore.of(-30)); var matchAnalyses = constraintAnalysis3.matches(); @@ -348,7 +348,7 @@ void compareWithConstraintMatchesAndMatchAnalysis() { constraintMatchTotal4.getConstraintRef()); }); // Matches for constraint1 not present. - var reverseConstraintAnalysis1 = reverseComparison.getConstraintAnalysis(constraintPackage, constraintName1); + var reverseConstraintAnalysis1 = reverseComparison.getConstraintAnalysis(constraintName1); assertSoftly(softly -> { softly.assertThat(reverseConstraintAnalysis1.score()).isEqualTo(SimpleScore.of(-20)); var matchAnalyses = reverseConstraintAnalysis1.matches(); @@ -360,7 +360,7 @@ void compareWithConstraintMatchesAndMatchAnalysis() { matchAnalysisOf(reverseConstraintAnalysis1.constraintRef(), -8)); }); // Matches for constraint2 present in both. - var reverseConstraintAnalysis2 = reverseComparison.getConstraintAnalysis(constraintPackage, constraintName2); + var reverseConstraintAnalysis2 = reverseComparison.getConstraintAnalysis(constraintName2); assertSoftly(softly -> { softly.assertThat(reverseConstraintAnalysis2.score()).isEqualTo(SimpleScore.of(-18)); var matchAnalyses = reverseConstraintAnalysis2.matches(); @@ -374,7 +374,7 @@ void compareWithConstraintMatchesAndMatchAnalysis() { matchAnalysisOf(reverseConstraintAnalysis2.constraintRef(), 6, "A", "B")); }); // Matches for constraint3 present in reverse. - var reverseConstraintAnalysis3 = reverseComparison.getConstraintAnalysis(constraintPackage, constraintName3); + var reverseConstraintAnalysis3 = reverseComparison.getConstraintAnalysis(constraintName3); assertSoftly(softly -> { softly.assertThat(reverseConstraintAnalysis3.score()).isEqualTo(SimpleScore.of(30)); var matchAnalyses = reverseConstraintAnalysis3.matches(); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraintStreamTest.java index daa1402cf0..d5becf4dac 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraintStreamTest.java @@ -69,8 +69,10 @@ protected void assertScore(InnerScoreDirector assertableMatch.score) .sum(); if (implSupport.isConstreamMatchEnabled()) { - String constraintPackage = scoreDirector.getSolutionDescriptor().getSolutionClass().getPackage().getName(); for (AssertableMatch assertableMatch : assertableMatches) { + String constraintPackage = assertableMatch.constraintPackage == null + ? scoreDirector.getSolutionDescriptor().getSolutionClass().getPackage().getName() + : assertableMatch.constraintPackage; Map> constraintMatchTotals = scoreDirector.getConstraintMatchTotalMap(); String constraintId = ConstraintRef.composeConstraintId(constraintPackage, assertableMatch.constraintName); @@ -106,6 +108,10 @@ protected static AssertableMatch assertMatch(Object... justifications) { return assertMatchWithScore(-1, justifications); } + protected static AssertableMatch assertMatch(String constraintPackage, String constraintName, Object... justifications) { + return assertMatchWithScore(-1, constraintPackage, constraintName, justifications); + } + protected static AssertableMatch assertMatch(String constraintName, Object... justifications) { return assertMatchWithScore(-1, constraintName, justifications); } @@ -118,14 +124,25 @@ protected static AssertableMatch assertMatchWithScore(int score, String constrai return new AssertableMatch(score, constraintName, justifications); } + protected static AssertableMatch assertMatchWithScore(int score, String constraintPackage, String constraintName, + Object... justifications) { + return new AssertableMatch(score, constraintPackage, constraintName, justifications); + } + protected static class AssertableMatch { private final int score; + private final String constraintPackage; private final String constraintName; private final List justificationList; public AssertableMatch(int score, String constraintName, Object... justifications) { + this(score, null, constraintName, justifications); + } + + public AssertableMatch(int score, String constraintPackage, String constraintName, Object... justifications) { this.justificationList = Arrays.asList(justifications); + this.constraintPackage = constraintPackage; this.constraintName = constraintName; this.score = score; } @@ -134,6 +151,9 @@ public boolean isEqualTo(ConstraintMatch constraintMatch) { if (score != ((SimpleScore) constraintMatch.getScore()).score()) { return false; } + if (constraintPackage != null && !constraintPackage.equals(constraintMatch.getConstraintRef().packageName())) { + return false; + } if (!constraintName.equals(constraintMatch.getConstraintRef().constraintName())) { return false; } @@ -156,7 +176,11 @@ public boolean isEqualTo(ConstraintMatch constraintMatch) { @Override public String toString() { - return constraintName + " " + justificationList + "=" + score; + if (constraintPackage == null) { + return constraintName + " " + justificationList + "=" + score; + } else { + return constraintPackage + "/" + constraintName + " " + justificationList + "=" + score; + } } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamTest.java index 9a16fda10a..8e40e2faa8 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamTest.java @@ -942,8 +942,8 @@ public void groupBy_1Mapping1Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, TEST_CONSTRAINT_NAME, solution.getFirstEntity().toString(), 6), - assertMatchWithScore(-1, TEST_CONSTRAINT_NAME, solution.getEntityList().get(1).toString(), 5)); + assertMatchWithScore(-1, null, TEST_CONSTRAINT_NAME, solution.getFirstEntity().toString(), 6), + assertMatchWithScore(-1, null, TEST_CONSTRAINT_NAME, solution.getEntityList().get(1).toString(), 5)); // Incremental; we have a new first entity, and less entities in total. TestdataLavishEntity entity = solution.getFirstEntity(); @@ -951,7 +951,7 @@ public void groupBy_1Mapping1Collector() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatchWithScore(-1, TEST_CONSTRAINT_NAME, solution.getFirstEntity().toString(), 5)); + assertMatchWithScore(-1, null, TEST_CONSTRAINT_NAME, solution.getFirstEntity().toString(), 5)); } @Override @@ -973,8 +973,8 @@ public void groupBy_1Mapping2Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, TEST_CONSTRAINT_NAME, entity1.toString(), 2, singleton(entity1)), - assertMatchWithScore(-1, TEST_CONSTRAINT_NAME, entity2.toString(), 1, singleton(entity2))); + assertMatchWithScore(-1, null, TEST_CONSTRAINT_NAME, entity1.toString(), 2, singleton(entity1)), + assertMatchWithScore(-1, null, TEST_CONSTRAINT_NAME, entity2.toString(), 1, singleton(entity2))); // Incremental TestdataLavishEntity entity = solution.getFirstEntity(); @@ -982,7 +982,7 @@ public void groupBy_1Mapping2Collector() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatchWithScore(-1, TEST_CONSTRAINT_NAME, entity2.toString(), 1, singleton(entity2))); + assertMatchWithScore(-1, null, TEST_CONSTRAINT_NAME, entity2.toString(), 1, singleton(entity2))); } @Override @@ -1007,9 +1007,9 @@ public void groupBy_1Mapping3Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, TEST_CONSTRAINT_NAME, entity1.toString(), Long.MAX_VALUE, Long.MAX_VALUE, + assertMatchWithScore(-1, null, TEST_CONSTRAINT_NAME, entity1.toString(), Long.MAX_VALUE, Long.MAX_VALUE, singleton(entity1)), - assertMatchWithScore(-1, TEST_CONSTRAINT_NAME, entity2.toString(), Long.MIN_VALUE, Long.MIN_VALUE, + assertMatchWithScore(-1, null, TEST_CONSTRAINT_NAME, entity2.toString(), Long.MIN_VALUE, Long.MIN_VALUE, singleton(entity2))); // Incremental @@ -1018,7 +1018,7 @@ public void groupBy_1Mapping3Collector() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatchWithScore(-1, TEST_CONSTRAINT_NAME, entity2.toString(), Long.MIN_VALUE, Long.MIN_VALUE, + assertMatchWithScore(-1, null, TEST_CONSTRAINT_NAME, entity2.toString(), Long.MIN_VALUE, Long.MIN_VALUE, singleton(entity2))); } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/quad/AbstractQuadConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/quad/AbstractQuadConstraintStreamTest.java index d196f4aad1..fb6053871d 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/quad/AbstractQuadConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/quad/AbstractQuadConstraintStreamTest.java @@ -739,8 +739,8 @@ public void groupBy_1Mapping2Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, TEST_CONSTRAINT_NAME, entity1.toString(), 2, singleton(entity1)), - assertMatchWithScore(-1, TEST_CONSTRAINT_NAME, entity2.toString(), 1, singleton(entity2))); + assertMatchWithScore(-1, null, TEST_CONSTRAINT_NAME, entity1.toString(), 2, singleton(entity1)), + assertMatchWithScore(-1, null, TEST_CONSTRAINT_NAME, entity2.toString(), 1, singleton(entity2))); // Incremental TestdataLavishEntity entity = solution.getFirstEntity(); @@ -748,7 +748,7 @@ public void groupBy_1Mapping2Collector() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatchWithScore(-1, TEST_CONSTRAINT_NAME, entity2.toString(), 1, singleton(entity2))); + assertMatchWithScore(-1, null, TEST_CONSTRAINT_NAME, entity2.toString(), 1, singleton(entity2))); } @Override @@ -777,9 +777,9 @@ public void groupBy_1Mapping3Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, TEST_CONSTRAINT_NAME, entity1.toString(), Long.MAX_VALUE, Long.MAX_VALUE, + assertMatchWithScore(-1, null, TEST_CONSTRAINT_NAME, entity1.toString(), Long.MAX_VALUE, Long.MAX_VALUE, singleton(entity1)), - assertMatchWithScore(-1, TEST_CONSTRAINT_NAME, entity2.toString(), Long.MIN_VALUE, Long.MIN_VALUE, + assertMatchWithScore(-1, null, TEST_CONSTRAINT_NAME, entity2.toString(), Long.MIN_VALUE, Long.MIN_VALUE, singleton(entity2))); // Incremental @@ -788,7 +788,7 @@ public void groupBy_1Mapping3Collector() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatchWithScore(-1, TEST_CONSTRAINT_NAME, entity2.toString(), Long.MIN_VALUE, Long.MIN_VALUE, + assertMatchWithScore(-1, null, TEST_CONSTRAINT_NAME, entity2.toString(), Long.MIN_VALUE, Long.MIN_VALUE, singleton(entity2))); } @@ -1251,7 +1251,7 @@ public void mapToQuad() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(TEST_CONSTRAINT_NAME, solution.getFirstEntity().getCode(), + assertMatch(null, TEST_CONSTRAINT_NAME, solution.getFirstEntity().getCode(), solution.getEntityList().get(1).getCode(), solution.getFirstEntityGroup().getCode(), solution.getEntityGroupList().get(1).getCode())); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/AbstractTriConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/AbstractTriConstraintStreamTest.java index 85c63be3df..f55e635310 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/AbstractTriConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/AbstractTriConstraintStreamTest.java @@ -1054,8 +1054,8 @@ public void groupBy_1Mapping2Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, TEST_CONSTRAINT_NAME, entity1.toString(), 2, singleton(entity1)), - assertMatchWithScore(-1, TEST_CONSTRAINT_NAME, entity2.toString(), 1, singleton(entity2))); + assertMatchWithScore(-1, null, TEST_CONSTRAINT_NAME, entity1.toString(), 2, singleton(entity1)), + assertMatchWithScore(-1, null, TEST_CONSTRAINT_NAME, entity2.toString(), 1, singleton(entity2))); // Incremental TestdataLavishEntity entity = solution.getFirstEntity(); @@ -1063,7 +1063,7 @@ public void groupBy_1Mapping2Collector() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatchWithScore(-1, TEST_CONSTRAINT_NAME, entity2.toString(), 1, singleton(entity2))); + assertMatchWithScore(-1, null, TEST_CONSTRAINT_NAME, entity2.toString(), 1, singleton(entity2))); } @Override @@ -1091,9 +1091,9 @@ public void groupBy_1Mapping3Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, TEST_CONSTRAINT_NAME, entity1.toString(), Long.MAX_VALUE, Long.MAX_VALUE, + assertMatchWithScore(-1, null, TEST_CONSTRAINT_NAME, entity1.toString(), Long.MAX_VALUE, Long.MAX_VALUE, singleton(entity1)), - assertMatchWithScore(-1, TEST_CONSTRAINT_NAME, entity2.toString(), Long.MIN_VALUE, Long.MIN_VALUE, + assertMatchWithScore(-1, null, TEST_CONSTRAINT_NAME, entity2.toString(), Long.MIN_VALUE, Long.MIN_VALUE, singleton(entity2))); // Incremental @@ -1102,7 +1102,7 @@ public void groupBy_1Mapping3Collector() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatchWithScore(-1, TEST_CONSTRAINT_NAME, entity2.toString(), Long.MIN_VALUE, Long.MIN_VALUE, + assertMatchWithScore(-1, null, TEST_CONSTRAINT_NAME, entity2.toString(), Long.MIN_VALUE, Long.MIN_VALUE, singleton(entity2))); } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java index 47418d36dd..a000c0dc1c 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java @@ -13,6 +13,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.SoftAssertions.assertSoftly; +import java.lang.reflect.InvocationTargetException; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; @@ -34,10 +35,12 @@ import ai.timefold.solver.core.api.score.stream.ConstraintCollectors; import ai.timefold.solver.core.api.score.stream.ConstraintProvider; import ai.timefold.solver.core.api.score.stream.DefaultConstraintJustification; +import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraintStreamTest; import ai.timefold.solver.core.impl.score.stream.common.ConstraintStreamFunctionalTest; import ai.timefold.solver.core.impl.score.stream.common.ConstraintStreamImplSupport; +import ai.timefold.solver.core.impl.testdata.domain.TestdataConstraintProvider; import ai.timefold.solver.core.impl.testdata.domain.TestdataEntity; import ai.timefold.solver.core.impl.testdata.domain.TestdataSolution; import ai.timefold.solver.core.impl.testdata.domain.TestdataValue; @@ -3670,10 +3673,6 @@ public void zeroConstraintWeightDisabled() { assertThat(oneWeightMonitorCount.get()).isEqualTo(1); } - // ************************************************************************ - // from() (deprecated) - // ************************************************************************ - @TestTemplate @Deprecated(forRemoval = true) public void fromIncludesNullWhenAllowsUnassigned() { @@ -3694,4 +3693,22 @@ public void fromIncludesNullWhenAllowsUnassigned() { assertMatch(entityWithNull), assertMatch(entityWithValue)); } + + @TestTemplate + public void constraintProvidedFromUnknownPackage() throws ClassNotFoundException, NoSuchMethodException, + InvocationTargetException, IllegalAccessException { + var clz = Class.forName("TestdataInUnnamedPackageSolution"); + var solution = clz.getMethod("generateSolution").invoke(null); + var solutionDescriptor = (SolutionDescriptor) clz.getMethod("buildSolutionDescriptor").invoke(null); + var entityList = (List) clz.getMethod("getEntityList") + .invoke(solution); + entityList.removeIf(entity -> !Objects.equals(entity.getCode(), "Generated Entity 0")); + + InnerScoreDirector scoreDirector = buildScoreDirector(solutionDescriptor, new TestdataConstraintProvider()); + + scoreDirector.setWorkingSolution(solution); + assertScore(scoreDirector, + assertMatch("unnamed.package", "Always penalize", entityList.get(0))); + } + } diff --git a/docs/src/modules/ROOT/pages/constraints-and-score/constraint-configuration.adoc b/docs/src/modules/ROOT/pages/constraints-and-score/constraint-configuration.adoc index 085d993e43..24934b3cf3 100644 --- a/docs/src/modules/ROOT/pages/constraints-and-score/constraint-configuration.adoc +++ b/docs/src/modules/ROOT/pages/constraints-and-score/constraint-configuration.adoc @@ -69,7 +69,7 @@ In the constraint configuration class, add a `@ConstraintWeight` field or proper [source,java,options="nowrap"] ---- -@ConstraintConfiguration(constraintPackage = "...conferencescheduling.score") +@ConstraintConfiguration public class ConferenceConstraintConfiguration { @ConstraintWeight("Speaker conflict") @@ -95,9 +95,16 @@ Notice how it defaults the _"Content conflict"_ constraint as ten times more imp Normally, a constraint weight only uses one score level, but it's possible to use multiple score levels (at a small performance cost). -Each constraint has a constraint package and a constraint name, together they form the constraint id. +Each constraint has a constraint name, and optionally a constraint package; together they form the constraint id. These connect the constraint weight with the constraint implementation. -*For each constraint weight, there must be a constraint implementation with the same package and the same name.* +*For each constraint weight, there must be a constraint implementation with the same constraint id.* + +[NOTE] +==== +Constraint packages are optional and have been deprecated. +We recommend that you don't use them, and instead keep constraint names unique. +If constraint package is not provided, the solver will transparently provide a default value. +==== * The `@ConstraintConfiguration` annotation has a `constraintPackage` property that defaults to the package of the constraint configuration class. Cases with xref:constraints-and-score/score-calculation.adoc[Constraint Streams API] normally don't need to specify it. diff --git a/docs/src/modules/ROOT/pages/constraints-and-score/score-calculation.adoc b/docs/src/modules/ROOT/pages/constraints-and-score/score-calculation.adoc index 27bcacc2a4..b2e37516b3 100644 --- a/docs/src/modules/ROOT/pages/constraints-and-score/score-calculation.adoc +++ b/docs/src/modules/ROOT/pages/constraints-and-score/score-calculation.adoc @@ -259,13 +259,11 @@ To do this, every constraint stream must contain a call to either a `penalize()` building block. The `penalize()` building block makes the score worse and the `reward()` building block improves the score. -Each constraint stream is then terminated by calling `asConstraint()` method, which finally builds the constraint. Constraints have several components: +Each constraint stream is then terminated by calling `asConstraint()` method, which finally builds the constraint. +Constraints have several components: -- Constraint package is the Java package that contains the constraint. -The default value is the package that contains the `ConstraintProvider` implementation or the value from -xref:constraints-and-score/constraint-configuration.adoc#constraintConfiguration[constraint configuration], if implemented. -- Constraint name is the human-readable descriptive name for the constraint, which -(together with the constraint package) must be unique within the entire `ConstraintProvider` implementation. +- Constraint name is the human-readable descriptive name for the constraint, +which must be unique within the entire `ConstraintProvider` implementation. - Constraint weight is a constant score value indicating how much every breach of the constraint affects the score. Valid examples include `SimpleScore.ONE`, `HardSoftScore.ONE_HARD` and `HardMediumSoftScore.of(1, 2, 3)`. - Constraint match weigher is an optional function indicating how many times the constraint weight should be applied in @@ -278,6 +276,13 @@ The default value is `1`. Constraints with zero constraint weight are automatically disabled and do not impose any performance penalty. ==== +[NOTE] +==== +The constraint has another component: a constraint package. +It has no practical impact on the solver and it has been deprecated. +We recommend you don't use them; the solver will choose a suitable default value. +==== + The Constraint Streams API supports many different types of penalties. Browse the API in your IDE for the full list of method overloads. Here are some examples: diff --git a/docs/src/modules/ROOT/pages/constraints-and-score/understanding-the-score.adoc b/docs/src/modules/ROOT/pages/constraints-and-score/understanding-the-score.adoc index 81d038651e..0bc00d3913 100644 --- a/docs/src/modules/ROOT/pages/constraints-and-score/understanding-the-score.adoc +++ b/docs/src/modules/ROOT/pages/constraints-and-score/understanding-the-score.adoc @@ -45,7 +45,7 @@ In the string above, there are some previously unexplained concepts. * A _Constraint match_ is created every time a constraint causes a change to the score. * _Justifications_ are user-defined objects that implement the `ai.timefold.solver.core.api.score.stream.ConstraintJustification` interface, which carry meaningful information about a constraint match, -such as its package, name and any metadata that the user chooses to expose. +such as its name and any metadata that the user chooses to expose. Justifications are most easily available via <>. * _Indicted objects_ are objects which were directly involved in causing a constraint to match. For example, if your constraints penalize each vehicle, diff --git a/docs/src/modules/ROOT/pages/upgrade-and-migration/upgrade-to-latest-version.adoc b/docs/src/modules/ROOT/pages/upgrade-and-migration/upgrade-to-latest-version.adoc index a4967152ac..a9ec71910d 100644 --- a/docs/src/modules/ROOT/pages/upgrade-and-migration/upgrade-to-latest-version.adoc +++ b/docs/src/modules/ROOT/pages/upgrade-and-migration/upgrade-to-latest-version.adoc @@ -57,6 +57,38 @@ Every upgrade note indicates how likely your code will be affected by that chang The upgrade recipe often lists the changes as they apply to Java code. We kindly ask Kotlin and Python users to translate the changes accordingly. +=== Upgrade from 1.12.0 to 1.13.0 (Work in progress) + +.icon:info-circle[role=yellow] Constraint packages have been deprecated +[%collapsible%open] +==== +In the solver, constraints are uniquely identified by their package and name. +We have now deprecated the package name and we recommend to keep constraint names unique instead. + +Before in `*ConstraintProvider.java`: + +[source,java] +---- +... + .penalize(ONE_SOFT) + .asConstraint("employees.paris", "maxHoursWorked"); +... +---- + +After in `*ConstraintProvider.java`: + +[source,java] +---- +... + .penalize(ONE_SOFT) + .asConstraint("employees.paris.maxHoursWorked"); +... +---- + +While constraint packages are still supported, they will be removed in a future major version. +==== + + === Upgrade from 1.9.0 to 1.10.0 .icon:info-circle[role=yellow] Pinning unassigned entities now fails fast, unless allowed diff --git a/docs/src/modules/ROOT/pages/using-timefold-solver/running-the-solver.adoc b/docs/src/modules/ROOT/pages/using-timefold-solver/running-the-solver.adoc index 056a1a2999..10c4050d90 100644 --- a/docs/src/modules/ROOT/pages/using-timefold-solver/running-the-solver.adoc +++ b/docs/src/modules/ROOT/pages/using-timefold-solver/running-the-solver.adoc @@ -468,14 +468,14 @@ This does not measure the amount of memory used by a solver; two solvers on the - `CONSTRAINT_MATCH_TOTAL_BEST_SCORE` (Micrometer meter id: "timefold.solver.constraint.match.best.score.*"): Measures the score impact of each constraint on the best solution Timefold Solver found so far. There are separate meters for each level of the score, with tags for each constraint. -For instance, for a `HardSoftScore` for a constraint "Minimize Cost" in package "com.example", -there are `timefold.solver.constraint.match.best.score.hard.score` and `timefold.solver.constraint.match.best.score.soft.score` meters with tags "constraint.package=com.example" and "constraint.name=Minimize Cost". +For instance, for a `HardSoftScore` for a constraint "Minimize Cost", +there are `timefold.solver.constraint.match.best.score.hard.score` and `timefold.solver.constraint.match.best.score.soft.score` meters with a tag "constraint.name=Minimize Cost". - `CONSTRAINT_MATCH_TOTAL_STEP_SCORE` (Micrometer meter id: "timefold.solver.constraint.match.step.score.*"): Measures the score impact of each constraint on the current step. There are separate meters for each level of the score, with tags for each constraint. -For instance, for a `HardSoftScore` for a constraint "Minimize Cost" in package "com.example", -there are `timefold.solver.constraint.match.step.score.hard.score` and `timefold.solver.constraint.match.step.score.soft.score` meters with tags "constraint.package=com.example" and "constraint.name=Minimize Cost". +For instance, for a `HardSoftScore` for a constraint "Minimize Cost", +there are `timefold.solver.constraint.match.step.score.hard.score` and `timefold.solver.constraint.match.step.score.soft.score` meters with a tag "constraint.name=Minimize Cost". - `PICKED_MOVE_TYPE_BEST_SCORE_DIFF` (Micrometer meter id: "timefold.solver.move.type.best.score.diff.*"): Measures how much a particular move type improves the best solution. diff --git a/migration/src/main/java/ai/timefold/solver/migration/v8/AsConstraintRecipe.java b/migration/src/main/java/ai/timefold/solver/migration/v8/AsConstraintRecipe.java index 0bc9c0aaaf..d1b26f8d75 100644 --- a/migration/src/main/java/ai/timefold/solver/migration/v8/AsConstraintRecipe.java +++ b/migration/src/main/java/ai/timefold/solver/migration/v8/AsConstraintRecipe.java @@ -261,8 +261,9 @@ public Expression visitExpression(Expression expression, ExecutionContext execut if (!matcherMeta.constraintPackageIncluded) { templateCode += ".asConstraint(#{any(String)})"; } else { - templateCode += ".asConstraint(#{any(String)}, #{any(String)})"; + templateCode += ".asConstraint(\"#{}\")"; } + System.out.println(templateCode); JavaTemplate template = JavaTemplate.builder(templateCode) .javaParser(buildJavaParser()) .build(); @@ -293,21 +294,22 @@ public Expression visitExpression(Expression expression, ExecutionContext execut if (!matcherMeta.matchWeigherIncluded) { return template.apply(getCursor(), e.getCoordinates().replace(), select, - arguments.get(2), arguments.get(0), arguments.get(1)); + arguments.get(2), mergeExpressions(arguments.get(0), arguments.get(1))); } else { return template.apply(getCursor(), e.getCoordinates().replace(), select, - arguments.get(2), arguments.get(3), arguments.get(0), arguments.get(1)); + arguments.get(2), arguments.get(3), + mergeExpressions(arguments.get(0), arguments.get(1))); } } else { if (!matcherMeta.matchWeigherIncluded) { return template.apply(getCursor(), e.getCoordinates().replace(), select, - arguments.get(0), arguments.get(1)); + mergeExpressions(arguments.get(0), arguments.get(1))); } else { return template.apply(getCursor(), e.getCoordinates().replace(), select, - arguments.get(2), arguments.get(0), arguments.get(1)); + arguments.get(2), mergeExpressions(arguments.get(0), arguments.get(1))); } } } @@ -315,6 +317,10 @@ public Expression visitExpression(Expression expression, ExecutionContext execut }); } + private String mergeExpressions(Expression constraintPackage, Expression constraintName) { + return constraintPackage.toString() + "." + constraintName.toString(); + } + public static JavaParser.Builder buildJavaParser() { return JavaParser.fromJavaVersion() .classpath(JavaParser.runtimeClasspath()); diff --git a/migration/src/main/java/ai/timefold/solver/migration/v8/RemoveConstraintPackageRecipe.java b/migration/src/main/java/ai/timefold/solver/migration/v8/RemoveConstraintPackageRecipe.java new file mode 100644 index 0000000000..9d78862d28 --- /dev/null +++ b/migration/src/main/java/ai/timefold/solver/migration/v8/RemoveConstraintPackageRecipe.java @@ -0,0 +1,61 @@ +package ai.timefold.solver.migration.v8; + +import java.util.List; + +import org.openrewrite.ExecutionContext; +import org.openrewrite.Preconditions; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.JavaParser; +import org.openrewrite.java.JavaTemplate; +import org.openrewrite.java.MethodMatcher; +import org.openrewrite.java.search.UsesMethod; +import org.openrewrite.java.tree.Expression; +import org.openrewrite.java.tree.J; + +public final class RemoveConstraintPackageRecipe extends Recipe { + + @Override + public String getDisplayName() { + return "Constraint Streams: don't use package name in the asConstraint() method"; + } + + @Override + public String getDescription() { + return "Remove the use of constraint package from `asConstraint(package, name)`."; + } + + @Override + public TreeVisitor getVisitor() { + return Preconditions.check( + Preconditions.or(new UsesMethod<>(new MethodMatcher("ai.timefold.solver.core.api.score.stream.ConstraintBuilder asConstraint(String, String)"))), + new JavaIsoVisitor<>() { + + @Override + public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) { + Expression select = method.getSelect(); + List arguments = method.getArguments(); + + String templateCode = "#{any(ai.timefold.solver.core.api.score.stream.ConstraintBuilder)}\n" + + ".asConstraint(\"#{}\")"; + JavaTemplate template = JavaTemplate.builder(templateCode) + .javaParser(buildJavaParser()) + .build(); + return template.apply(getCursor(), + method.getCoordinates().replace(), select, + mergeExpressions(arguments.get(0), arguments.get(1))); + } + }); + } + + private String mergeExpressions(Expression constraintPackage, Expression constraintName) { + return constraintPackage.toString() + "." + constraintName.toString(); + } + + public static JavaParser.Builder buildJavaParser() { + return JavaParser.fromJavaVersion() + .classpath(JavaParser.runtimeClasspath()); + } + +} diff --git a/migration/src/test/java/ai/timefold/solver/migration/v8/AsConstraintRecipeTest.java b/migration/src/test/java/ai/timefold/solver/migration/v8/AsConstraintRecipeTest.java index befa1d83cf..7b30ec05aa 100644 --- a/migration/src/test/java/ai/timefold/solver/migration/v8/AsConstraintRecipeTest.java +++ b/migration/src/test/java/ai/timefold/solver/migration/v8/AsConstraintRecipeTest.java @@ -35,7 +35,7 @@ void uniPenalizeId() { " .penalize(\"My package\", \"My constraint\", HardSoftScore.ONE_HARD);"), wrap(" return f.forEach(String.class)\n" + " .penalize(HardSoftScore.ONE_HARD)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -55,7 +55,7 @@ void uniPenalizeConfigurableId() { " .penalizeConfigurable(\"My package\", \"My constraint\");"), wrap(" return f.forEach(String.class)\n" + " .penalizeConfigurable()\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -75,7 +75,7 @@ void uniPenalizeIdMatchWeigherInt() { " .penalize(\"My package\", \"My constraint\", HardSoftScore.ONE_HARD, (a) -> 7);"), wrap(" return f.forEach(String.class)\n" + " .penalize(HardSoftScore.ONE_HARD, (a) -> 7)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -95,7 +95,7 @@ void uniPenalizeConfigurableIdMatchWeigherInt() { " .penalizeConfigurable(\"My package\", \"My constraint\", (a) -> 7);"), wrap(" return f.forEach(String.class)\n" + " .penalizeConfigurable((a) -> 7)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -115,7 +115,7 @@ void uniPenalizeIdMatchWeigherLong() { " .penalizeLong(\"My package\", \"My constraint\", HardSoftLongScore.ONE_HARD, (a) -> 7L);"), wrap(" return f.forEach(String.class)\n" + " .penalizeLong(HardSoftLongScore.ONE_HARD, (a) -> 7L)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -135,7 +135,7 @@ void uniPenalizeConfigurableIdMatchWeigherLong() { " .penalizeConfigurableLong(\"My package\", \"My constraint\", (a) -> 7L);"), wrap(" return f.forEach(String.class)\n" + " .penalizeConfigurableLong((a) -> 7L)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -155,7 +155,7 @@ void uniPenalizeIdMatchWeigherBigDecimal() { " .penalizeBigDecimal(\"My package\", \"My constraint\", HardSoftBigDecimalScore.ONE_HARD, (a) -> BigDecimal.TEN);"), wrap(" return f.forEach(String.class)\n" + " .penalizeBigDecimal(HardSoftBigDecimalScore.ONE_HARD, (a) -> BigDecimal.TEN)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -175,7 +175,7 @@ void uniPenalizeConfigurableIdMatchWeigherBigDecimal() { " .penalizeConfigurableBigDecimal(\"My package\", \"My constraint\", (a) -> BigDecimal.TEN);"), wrap(" return f.forEach(String.class)\n" + " .penalizeConfigurableBigDecimal((a) -> BigDecimal.TEN)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -195,7 +195,7 @@ void uniRewardId() { " .reward(\"My package\", \"My constraint\", HardSoftScore.ONE_HARD);"), wrap(" return f.forEach(String.class)\n" + " .reward(HardSoftScore.ONE_HARD)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -215,7 +215,7 @@ void uniRewardConfigurableId() { " .rewardConfigurable(\"My package\", \"My constraint\");"), wrap(" return f.forEach(String.class)\n" + " .rewardConfigurable()\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -235,7 +235,7 @@ void uniRewardIdMatchWeigherInt() { " .reward(\"My package\", \"My constraint\", HardSoftScore.ONE_HARD, (a) -> 7);"), wrap(" return f.forEach(String.class)\n" + " .reward(HardSoftScore.ONE_HARD, (a) -> 7)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -255,7 +255,7 @@ void uniRewardConfigurableIdMatchWeigherInt() { " .rewardConfigurable(\"My package\", \"My constraint\", (a) -> 7);"), wrap(" return f.forEach(String.class)\n" + " .rewardConfigurable((a) -> 7)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -275,7 +275,7 @@ void uniRewardIdMatchWeigherLong() { " .rewardLong(\"My package\", \"My constraint\", HardSoftLongScore.ONE_HARD, (a) -> 7L);"), wrap(" return f.forEach(String.class)\n" + " .rewardLong(HardSoftLongScore.ONE_HARD, (a) -> 7L)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -295,7 +295,7 @@ void uniRewardConfigurableIdMatchWeigherLong() { " .rewardConfigurableLong(\"My package\", \"My constraint\", (a) -> 7L);"), wrap(" return f.forEach(String.class)\n" + " .rewardConfigurableLong((a) -> 7L)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -315,7 +315,7 @@ void uniRewardIdMatchWeigherBigDecimal() { " .rewardBigDecimal(\"My package\", \"My constraint\", HardSoftBigDecimalScore.ONE_HARD, (a) -> BigDecimal.TEN);"), wrap(" return f.forEach(String.class)\n" + " .rewardBigDecimal(HardSoftBigDecimalScore.ONE_HARD, (a) -> BigDecimal.TEN)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -335,7 +335,7 @@ void uniRewardConfigurableIdMatchWeigherBigDecimal() { " .rewardConfigurableBigDecimal(\"My package\", \"My constraint\", (a) -> BigDecimal.TEN);"), wrap(" return f.forEach(String.class)\n" + " .rewardConfigurableBigDecimal((a) -> BigDecimal.TEN)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -355,7 +355,7 @@ void uniImpactId() { " .impact(\"My package\", \"My constraint\", HardSoftScore.ONE_HARD);"), wrap(" return f.forEach(String.class)\n" + " .impact(HardSoftScore.ONE_HARD)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -375,7 +375,7 @@ void uniImpactIdMatchWeigherInt() { " .impact(\"My package\", \"My constraint\", HardSoftScore.ONE_HARD, (a) -> 7);"), wrap(" return f.forEach(String.class)\n" + " .impact(HardSoftScore.ONE_HARD, (a) -> 7)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -395,7 +395,7 @@ void uniImpactIdMatchWeigherLong() { " .impactLong(\"My package\", \"My constraint\", HardSoftLongScore.ONE_HARD, (a) -> 7L);"), wrap(" return f.forEach(String.class)\n" + " .impactLong(HardSoftLongScore.ONE_HARD, (a) -> 7L)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -415,7 +415,7 @@ void uniImpactIdMatchWeigherBigDecimal() { " .impactBigDecimal(\"My package\", \"My constraint\", HardSoftBigDecimalScore.ONE_HARD, (a) -> BigDecimal.TEN);"), wrap(" return f.forEach(String.class)\n" + " .impactBigDecimal(HardSoftBigDecimalScore.ONE_HARD, (a) -> BigDecimal.TEN)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } // ************************************************************************ @@ -443,7 +443,7 @@ void biPenalizeId() { wrap(" return f.forEach(String.class)\n" + " .join(String.class)\n" + " .penalize(HardSoftScore.ONE_HARD)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -467,7 +467,7 @@ void biPenalizeConfigurableId() { wrap(" return f.forEach(String.class)\n" + " .join(String.class)\n" + " .penalizeConfigurable()\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -491,7 +491,7 @@ void biPenalizeIdMatchWeigherInt() { wrap(" return f.forEach(String.class)\n" + " .join(String.class)\n" + " .penalize(HardSoftScore.ONE_HARD, (a, b) -> 7)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -515,7 +515,7 @@ void biPenalizeConfigurableIdMatchWeigherInt() { wrap(" return f.forEach(String.class)\n" + " .join(String.class)\n" + " .penalizeConfigurable((a, b) -> 7)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -539,7 +539,7 @@ void biPenalizeIdMatchWeigherLong() { wrap(" return f.forEach(String.class)\n" + " .join(String.class)\n" + " .penalizeLong(HardSoftLongScore.ONE_HARD, (a, b) -> 7L)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -563,7 +563,7 @@ void biPenalizeConfigurableIdMatchWeigherLong() { wrap(" return f.forEach(String.class)\n" + " .join(String.class)\n" + " .penalizeConfigurableLong((a, b) -> 7L)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -587,7 +587,7 @@ void biPenalizeIdMatchWeigherBigDecimal() { wrap(" return f.forEach(String.class)\n" + " .join(String.class)\n" + " .penalizeBigDecimal(HardSoftBigDecimalScore.ONE_HARD, (a, b) -> BigDecimal.TEN)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -611,7 +611,7 @@ void biPenalizeConfigurableIdMatchWeigherBigDecimal() { wrap(" return f.forEach(String.class)\n" + " .join(String.class)\n" + " .penalizeConfigurableBigDecimal((a, b) -> BigDecimal.TEN)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -635,7 +635,7 @@ void biRewardId() { wrap(" return f.forEach(String.class)\n" + " .join(String.class)\n" + " .reward(HardSoftScore.ONE_HARD)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -659,7 +659,7 @@ void biRewardConfigurableId() { wrap(" return f.forEach(String.class)\n" + " .join(String.class)\n" + " .rewardConfigurable()\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -683,7 +683,7 @@ void biRewardIdMatchWeigherInt() { wrap(" return f.forEach(String.class)\n" + " .join(String.class)\n" + " .reward(HardSoftScore.ONE_HARD, (a, b) -> 7)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -707,7 +707,7 @@ void biRewardConfigurableIdMatchWeigherInt() { wrap(" return f.forEach(String.class)\n" + " .join(String.class)\n" + " .rewardConfigurable((a, b) -> 7)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -731,7 +731,7 @@ void biRewardIdMatchWeigherLong() { wrap(" return f.forEach(String.class)\n" + " .join(String.class)\n" + " .rewardLong(HardSoftLongScore.ONE_HARD, (a, b) -> 7L)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -755,7 +755,7 @@ void biRewardConfigurableIdMatchWeigherLong() { wrap(" return f.forEach(String.class)\n" + " .join(String.class)\n" + " .rewardConfigurableLong((a, b) -> 7L)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -779,7 +779,7 @@ void biRewardIdMatchWeigherBigDecimal() { wrap(" return f.forEach(String.class)\n" + " .join(String.class)\n" + " .rewardBigDecimal(HardSoftBigDecimalScore.ONE_HARD, (a, b) -> BigDecimal.TEN)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -803,7 +803,7 @@ void biRewardConfigurableIdMatchWeigherBigDecimal() { wrap(" return f.forEach(String.class)\n" + " .join(String.class)\n" + " .rewardConfigurableBigDecimal((a, b) -> BigDecimal.TEN)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -827,7 +827,7 @@ void biImpactId() { wrap(" return f.forEach(String.class)\n" + " .join(String.class)\n" + " .impact(HardSoftScore.ONE_HARD)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -851,7 +851,7 @@ void biImpactIdMatchWeigherInt() { wrap(" return f.forEach(String.class)\n" + " .join(String.class)\n" + " .impact(HardSoftScore.ONE_HARD, (a, b) -> 7)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -875,7 +875,7 @@ void biImpactIdMatchWeigherLong() { wrap(" return f.forEach(String.class)\n" + " .join(String.class)\n" + " .impactLong(HardSoftLongScore.ONE_HARD, (a, b) -> 7L)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -899,7 +899,7 @@ void biImpactIdMatchWeigherBigDecimal() { wrap(" return f.forEach(String.class)\n" + " .join(String.class)\n" + " .impactBigDecimal(HardSoftBigDecimalScore.ONE_HARD, (a, b) -> BigDecimal.TEN)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } // ************************************************************************ @@ -931,7 +931,7 @@ void triPenalizeId() { " .join(String.class)\n" + " .join(String.class)\n" + " .penalize(HardSoftScore.ONE_HARD)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -959,7 +959,7 @@ void triPenalizeConfigurableId() { " .join(String.class)\n" + " .join(String.class)\n" + " .penalizeConfigurable()\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -987,7 +987,7 @@ void triPenalizeIdMatchWeigherInt() { " .join(String.class)\n" + " .join(String.class)\n" + " .penalize(HardSoftScore.ONE_HARD, (a, b, c) -> 7)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -1015,7 +1015,7 @@ void triPenalizeConfigurableIdMatchWeigherInt() { " .join(String.class)\n" + " .join(String.class)\n" + " .penalizeConfigurable((a, b, c) -> 7)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -1043,7 +1043,7 @@ void triPenalizeIdMatchWeigherLong() { " .join(String.class)\n" + " .join(String.class)\n" + " .penalizeLong(HardSoftLongScore.ONE_HARD, (a, b, c) -> 7L)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -1071,7 +1071,7 @@ void triPenalizeConfigurableIdMatchWeigherLong() { " .join(String.class)\n" + " .join(String.class)\n" + " .penalizeConfigurableLong((a, b, c) -> 7L)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -1099,7 +1099,7 @@ void triPenalizeIdMatchWeigherBigDecimal() { " .join(String.class)\n" + " .join(String.class)\n" + " .penalizeBigDecimal(HardSoftBigDecimalScore.ONE_HARD, (a, b, c) -> BigDecimal.TEN)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -1127,7 +1127,7 @@ void triPenalizeConfigurableIdMatchWeigherBigDecimal() { " .join(String.class)\n" + " .join(String.class)\n" + " .penalizeConfigurableBigDecimal((a, b, c) -> BigDecimal.TEN)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -1155,7 +1155,7 @@ void triRewardId() { " .join(String.class)\n" + " .join(String.class)\n" + " .reward(HardSoftScore.ONE_HARD)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -1183,7 +1183,7 @@ void triRewardConfigurableId() { " .join(String.class)\n" + " .join(String.class)\n" + " .rewardConfigurable()\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -1211,7 +1211,7 @@ void triRewardIdMatchWeigherInt() { " .join(String.class)\n" + " .join(String.class)\n" + " .reward(HardSoftScore.ONE_HARD, (a, b, c) -> 7)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -1239,7 +1239,7 @@ void triRewardConfigurableIdMatchWeigherInt() { " .join(String.class)\n" + " .join(String.class)\n" + " .rewardConfigurable((a, b, c) -> 7)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -1267,7 +1267,7 @@ void triRewardIdMatchWeigherLong() { " .join(String.class)\n" + " .join(String.class)\n" + " .rewardLong(HardSoftLongScore.ONE_HARD, (a, b, c) -> 7L)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -1295,7 +1295,7 @@ void triRewardConfigurableIdMatchWeigherLong() { " .join(String.class)\n" + " .join(String.class)\n" + " .rewardConfigurableLong((a, b, c) -> 7L)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -1323,7 +1323,7 @@ void triRewardIdMatchWeigherBigDecimal() { " .join(String.class)\n" + " .join(String.class)\n" + " .rewardBigDecimal(HardSoftBigDecimalScore.ONE_HARD, (a, b, c) -> BigDecimal.TEN)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -1351,7 +1351,7 @@ void triRewardConfigurableIdMatchWeigherBigDecimal() { " .join(String.class)\n" + " .join(String.class)\n" + " .rewardConfigurableBigDecimal((a, b, c) -> BigDecimal.TEN)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -1379,7 +1379,7 @@ void triImpactId() { " .join(String.class)\n" + " .join(String.class)\n" + " .impact(HardSoftScore.ONE_HARD)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -1407,7 +1407,7 @@ void triImpactIdMatchWeigherInt() { " .join(String.class)\n" + " .join(String.class)\n" + " .impact(HardSoftScore.ONE_HARD, (a, b, c) -> 7)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -1435,7 +1435,7 @@ void triImpactIdMatchWeigherLong() { " .join(String.class)\n" + " .join(String.class)\n" + " .impactLong(HardSoftLongScore.ONE_HARD, (a, b, c) -> 7L)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -1463,7 +1463,7 @@ void triImpactIdMatchWeigherBigDecimal() { " .join(String.class)\n" + " .join(String.class)\n" + " .impactBigDecimal(HardSoftBigDecimalScore.ONE_HARD, (a, b, c) -> BigDecimal.TEN)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } // ************************************************************************ @@ -1499,7 +1499,7 @@ void quadPenalizeId() { " .join(String.class)\n" + " .join(String.class)\n" + " .penalize(HardSoftScore.ONE_HARD)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -1531,7 +1531,7 @@ void quadPenalizeConfigurableId() { " .join(String.class)\n" + " .join(String.class)\n" + " .penalizeConfigurable()\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -1563,7 +1563,7 @@ void quadPenalizeIdMatchWeigherInt() { " .join(String.class)\n" + " .join(String.class)\n" + " .penalize(HardSoftScore.ONE_HARD, (a, b, c, d) -> 7)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -1595,7 +1595,7 @@ void quadPenalizeConfigurableIdMatchWeigherInt() { " .join(String.class)\n" + " .join(String.class)\n" + " .penalizeConfigurable((a, b, c, d) -> 7)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -1627,7 +1627,7 @@ void quadPenalizeIdMatchWeigherLong() { " .join(String.class)\n" + " .join(String.class)\n" + " .penalizeLong(HardSoftLongScore.ONE_HARD, (a, b, c, d) -> 7L)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -1659,7 +1659,7 @@ void quadPenalizeConfigurableIdMatchWeigherLong() { " .join(String.class)\n" + " .join(String.class)\n" + " .penalizeConfigurableLong((a, b, c, d) -> 7L)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -1693,7 +1693,7 @@ void quadPenalizeIdMatchWeigherBigDecimal() { " .join(String.class)\n" + " .penalizeBigDecimal(HardSoftBigDecimalScore.ONE_HARD, (a, b, c, d) -> BigDecimal.TEN)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -1725,7 +1725,7 @@ void quadPenalizeConfigurableIdMatchWeigherBigDecimal() { " .join(String.class)\n" + " .join(String.class)\n" + " .penalizeConfigurableBigDecimal((a, b, c, d) -> BigDecimal.TEN)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -1757,7 +1757,7 @@ void quadRewardId() { " .join(String.class)\n" + " .join(String.class)\n" + " .reward(HardSoftScore.ONE_HARD)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -1789,7 +1789,7 @@ void quadRewardConfigurableId() { " .join(String.class)\n" + " .join(String.class)\n" + " .rewardConfigurable()\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -1821,7 +1821,7 @@ void quadRewardIdMatchWeigherInt() { " .join(String.class)\n" + " .join(String.class)\n" + " .reward(HardSoftScore.ONE_HARD, (a, b, c, d) -> 7)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -1853,7 +1853,7 @@ void quadRewardConfigurableIdMatchWeigherInt() { " .join(String.class)\n" + " .join(String.class)\n" + " .rewardConfigurable((a, b, c, d) -> 7)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -1885,7 +1885,7 @@ void quadRewardIdMatchWeigherLong() { " .join(String.class)\n" + " .join(String.class)\n" + " .rewardLong(HardSoftLongScore.ONE_HARD, (a, b, c, d) -> 7L)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -1917,7 +1917,7 @@ void quadRewardConfigurableIdMatchWeigherLong() { " .join(String.class)\n" + " .join(String.class)\n" + " .rewardConfigurableLong((a, b, c, d) -> 7L)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -1951,7 +1951,7 @@ void quadRewardIdMatchWeigherBigDecimal() { " .join(String.class)\n" + " .rewardBigDecimal(HardSoftBigDecimalScore.ONE_HARD, (a, b, c, d) -> BigDecimal.TEN)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -1983,7 +1983,7 @@ void quadRewardConfigurableIdMatchWeigherBigDecimal() { " .join(String.class)\n" + " .join(String.class)\n" + " .rewardConfigurableBigDecimal((a, b, c, d) -> BigDecimal.TEN)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -2015,7 +2015,7 @@ void quadImpactId() { " .join(String.class)\n" + " .join(String.class)\n" + " .impact(HardSoftScore.ONE_HARD)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -2047,7 +2047,7 @@ void quadImpactIdMatchWeigherInt() { " .join(String.class)\n" + " .join(String.class)\n" + " .impact(HardSoftScore.ONE_HARD, (a, b, c, d) -> 7)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -2079,7 +2079,7 @@ void quadImpactIdMatchWeigherLong() { " .join(String.class)\n" + " .join(String.class)\n" + " .impactLong(HardSoftLongScore.ONE_HARD, (a, b, c, d) -> 7L)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } @Test @@ -2113,7 +2113,7 @@ void quadImpactIdMatchWeigherBigDecimal() { " .join(String.class)\n" + " .impactBigDecimal(HardSoftBigDecimalScore.ONE_HARD, (a, b, c, d) -> BigDecimal.TEN)\n" + - " .asConstraint(\"My package\", \"My constraint\");"))); + " .asConstraint(\"My package.My constraint\");"))); } // ************************************************************************ diff --git a/migration/src/test/java/ai/timefold/solver/migration/v8/RemoveConstraintPackageRecipeTest.java b/migration/src/test/java/ai/timefold/solver/migration/v8/RemoveConstraintPackageRecipeTest.java new file mode 100644 index 0000000000..52513a3573 --- /dev/null +++ b/migration/src/test/java/ai/timefold/solver/migration/v8/RemoveConstraintPackageRecipeTest.java @@ -0,0 +1,93 @@ +package ai.timefold.solver.migration.v8; + +import static org.openrewrite.java.Assertions.java; + +import org.junit.jupiter.api.Test; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +class RemoveConstraintPackageRecipeTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new RemoveConstraintPackageRecipe()) + .parser(RemoveConstraintPackageRecipe.buildJavaParser()); + } + + @Test + void uni() { + rewriteRun( + java( + wrap(" return f.forEach(String.class)\n" + + " .penalize(HardSoftScore.ONE_HARD)\n" + + " .asConstraint(\"My package\", \"My constraint\");"), + wrap(" return f.forEach(String.class)\n" + + " .penalize(HardSoftScore.ONE_HARD)\n" + + " .asConstraint(\"My package.My constraint\");"))); + } + + @Test + void bi() { + rewriteRun( + java( + wrap(" return f.forEach(String.class)\n" + + " .join(String.class)\n" + + " .penalize(HardSoftScore.ONE_HARD)\n" + + " .asConstraint(\"My package\", \"My constraint\");"), + wrap(" return f.forEach(String.class)\n" + + " .join(String.class)\n" + + " .penalize(HardSoftScore.ONE_HARD)\n" + + " .asConstraint(\"My package.My constraint\");"))); + } + + @Test + void tri() { + rewriteRun( + java( + wrap(" return f.forEach(String.class)\n" + + " .join(String.class)\n" + + " .join(String.class)\n" + + " .penalize(HardSoftScore.ONE_HARD)\n" + + " .asConstraint(\"My package\", \"My constraint\");"), + wrap(" return f.forEach(String.class)\n" + + " .join(String.class)\n" + + " .join(String.class)\n" + + " .penalize(HardSoftScore.ONE_HARD)\n" + + " .asConstraint(\"My package.My constraint\");"))); + } + + @Test + void quad() { + rewriteRun( + java( + wrap(" return f.forEach(String.class)\n" + + " .join(String.class)\n" + + " .join(String.class)\n" + + " .join(String.class)\n" + + " .penalize(HardSoftScore.ONE_HARD)\n" + + " .asConstraint(\"My package\", \"My constraint\");"), + wrap(" return f.forEach(String.class)\n" + + " .join(String.class)\n" + + " .join(String.class)\n" + + " .join(String.class)\n" + + " .penalize(HardSoftScore.ONE_HARD)\n" + + " .asConstraint(\"My package.My constraint\");"))); + } + + // ************************************************************************ + // Helper methods + // ************************************************************************ + + private static String wrap(String content) { + return "import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore;\n" + + "import ai.timefold.solver.core.api.score.stream.ConstraintFactory;\n" + + "import ai.timefold.solver.core.api.score.stream.Constraint;\n" + + "\n" + + "class Test {\n" + + " Constraint myConstraint(ConstraintFactory f) {\n" + + content + "\n" + + " }" + + "}\n"; + } + +}