Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,11 @@ The member (%s) on the entity class (%s) is a declarative shadow variable, but t
var method = ReflectionHelper.getDeclaredMethod(variableMemberAccessor.getDeclaringClass(), methodName);

if (method == null) {
throw new IllegalArgumentException("Could not find method named %s on the class %s. Maybe you misspelled it?"
.formatted(methodName, variableMemberAccessor.getDeclaringClass().getSimpleName()));
throw new IllegalArgumentException("""
@%s (%s) defines a supplierMethod (%s) that does not exist inside its declaring class (%s).
Maybe you misspelled the supplierMethod name?"""
.formatted(ShadowVariable.class.getSimpleName(), variableName, methodName,
variableMemberAccessor.getDeclaringClass().getCanonicalName()));
}

var shadowVariableUpdater = method.getAnnotation(ShadowSources.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import ai.timefold.solver.core.testdomain.chained.TestdataChainedSolution;
import ai.timefold.solver.core.testdomain.collection.TestdataArrayBasedSolution;
import ai.timefold.solver.core.testdomain.collection.TestdataSetBasedSolution;
import ai.timefold.solver.core.testdomain.declarative.missing.TestdataDeclarativeMissingSupplierSolution;
import ai.timefold.solver.core.testdomain.immutable.enumeration.TestdataEnumSolution;
import ai.timefold.solver.core.testdomain.immutable.record.TestdataRecordSolution;
import ai.timefold.solver.core.testdomain.inheritance.solution.baseannotated.childnot.TestdataOnlyBaseAnnotatedChildEntity;
Expand Down Expand Up @@ -671,4 +672,13 @@ void testBadChainedAndListModel() {
.hasMessageContaining("on a single planning entity")
.hasMessageContaining("is not supported");
}

@Test
void missingDeclarativeSupplierMethod() {
assertThatCode(TestdataDeclarativeMissingSupplierSolution::buildSolutionDescriptor)
.hasMessageContainingAll("@ShadowVariable (endTime)",
"supplierMethod (calculateEndTime) that does not exist",
"inside its declaring class (ai.timefold.solver.core.testdomain.declarative.missing.TestdataDeclarativeMissingSupplierValue).",
"Maybe you misspelled the supplierMethod name?");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package ai.timefold.solver.core.testdomain.declarative.missing;

import ai.timefold.solver.core.api.domain.entity.PlanningEntity;
import ai.timefold.solver.core.api.domain.variable.PlanningVariable;
import ai.timefold.solver.core.api.domain.variable.ShadowVariable;
import ai.timefold.solver.core.preview.api.domain.variable.declarative.ShadowSources;

@PlanningEntity
public class TestdataDeclarativeMissingSupplierEntity {
String id;
@PlanningVariable
TestdataDeclarativeMissingSupplierValue value;

@ShadowVariable(supplierName = "updateDurationInDays")
long durationInDays;

public TestdataDeclarativeMissingSupplierEntity(String id, TestdataDeclarativeMissingSupplierValue value) {
this.id = id;
this.value = value;
}

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public TestdataDeclarativeMissingSupplierValue getValue() {
return value;
}

public void setValue(TestdataDeclarativeMissingSupplierValue value) {
this.value = value;
}

@ShadowSources("value")
public long updateDurationInDays() {
if (value != null) {
return value.getDuration().toDays();
}
return 0;
}

public long getDurationInDays() {
return durationInDays;
}

@Override
public String toString() {
return "TestdataDeclarativeMissingSupplierEntity{" +
"id=" + id +
", value=" + value +
'}';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package ai.timefold.solver.core.testdomain.declarative.missing;

import java.util.EnumSet;
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.valuerange.ValueRangeProvider;
import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore;
import ai.timefold.solver.core.config.solver.PreviewFeature;
import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor;

@PlanningSolution
public class TestdataDeclarativeMissingSupplierSolution {

public static SolutionDescriptor<TestdataDeclarativeMissingSupplierSolution> buildSolutionDescriptor() {
return SolutionDescriptor.buildSolutionDescriptor(EnumSet.of(PreviewFeature.DECLARATIVE_SHADOW_VARIABLES),
TestdataDeclarativeMissingSupplierSolution.class, TestdataDeclarativeMissingSupplierEntity.class,
TestdataDeclarativeMissingSupplierValue.class);
}

@PlanningEntityCollectionProperty
List<TestdataDeclarativeMissingSupplierEntity> entities;

@PlanningEntityCollectionProperty
@ValueRangeProvider
List<TestdataDeclarativeMissingSupplierValue> values;

@PlanningScore
HardSoftScore score;

public TestdataDeclarativeMissingSupplierSolution() {
}

public TestdataDeclarativeMissingSupplierSolution(List<TestdataDeclarativeMissingSupplierEntity> entities,
List<TestdataDeclarativeMissingSupplierValue> values) {
this.values = values;
this.entities = entities;
}

public List<TestdataDeclarativeMissingSupplierValue> getValues() {
return values;
}

public void setValues(List<TestdataDeclarativeMissingSupplierValue> values) {
this.values = values;
}

public List<TestdataDeclarativeMissingSupplierEntity> getEntities() {
return entities;
}

public void setEntities(
List<TestdataDeclarativeMissingSupplierEntity> entities) {
this.entities = entities;
}

public HardSoftScore getScore() {
return score;
}

public void setScore(HardSoftScore score) {
this.score = score;
}

@Override
public String toString() {
return "TestdataDeclarativeMissingSupplierSolution{" +
"entities=" + entities +
", values=" + values +
", score=" + score +
'}';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package ai.timefold.solver.core.testdomain.declarative.missing;

import java.time.Duration;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

import ai.timefold.solver.core.api.domain.entity.PlanningEntity;
import ai.timefold.solver.core.api.domain.variable.InverseRelationShadowVariable;
import ai.timefold.solver.core.api.domain.variable.ShadowVariable;
import ai.timefold.solver.core.preview.api.domain.variable.declarative.ShadowSources;
import ai.timefold.solver.core.preview.api.domain.variable.declarative.ShadowVariableLooped;

@PlanningEntity
public class TestdataDeclarativeMissingSupplierValue {
public static final LocalDateTime DEFAULT_TIME =
LocalDateTime.of(2025, 4, 29, 18, 40, 0);

String id;

@ShadowVariable(supplierName = "calculateStartTime")
LocalDateTime startTime;

@ShadowVariable(supplierName = "calculateEndTime")
LocalDateTime endTime;

@ShadowVariableLooped
boolean isInvalid;

@InverseRelationShadowVariable(sourceVariableName = "value")
List<TestdataDeclarativeMissingSupplierEntity> entityList = new ArrayList<>();

Duration duration;

public TestdataDeclarativeMissingSupplierValue() {
}

public TestdataDeclarativeMissingSupplierValue(String id, Duration duration) {
this.id = id;
this.duration = duration;
}

public List<TestdataDeclarativeMissingSupplierEntity> getEntityList() {
return entityList;
}

public void setEntityList(List<TestdataDeclarativeMissingSupplierEntity> entityList) {
this.entityList = entityList;
}

public LocalDateTime getStartTime() {
return startTime;
}

public void setStartTime(LocalDateTime startTime) {
this.startTime = startTime;
}

@ShadowSources({ "entityList" })
public LocalDateTime calculateStartTime() {
LocalDateTime readyTime = DEFAULT_TIME.plusDays(10);
if (!entityList.isEmpty()) {
readyTime = DEFAULT_TIME.plusDays(entityList.size());
}
return readyTime;
}

public LocalDateTime getEndTime() {
return endTime;
}

public void setEndTime(LocalDateTime endTime) {
this.endTime = endTime;
}

public Duration getDuration() {
return duration;
}

public void setDuration(Duration duration) {
this.duration = duration;
}

public boolean isInvalid() {
return isInvalid;
}

public void setInvalid(boolean invalid) {
isInvalid = invalid;
}

@Override
public String toString() {
return id + "{" +
"endTime=" + endTime +
"]}";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
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.variable.ShadowVariable;
import ai.timefold.solver.core.api.score.calculator.EasyScoreCalculator;
import ai.timefold.solver.core.api.score.calculator.IncrementalScoreCalculator;
import ai.timefold.solver.core.api.score.stream.ConstraintMetaModel;
Expand Down Expand Up @@ -1006,33 +1007,20 @@ private GeneratedGizmoClasses generateDomainAccessors(Map<String, SolverConfig>
});

for (var annotatedMember : membersToGeneratedAccessorsForCollection) {
ClassInfo classInfo = null;
String memberName = null;
switch (annotatedMember.target().kind()) {
case FIELD -> {
var fieldInfo = annotatedMember.target().asField();
var classInfo = fieldInfo.declaringClass();
classInfo = fieldInfo.declaringClass();
memberName = fieldInfo.name();
buildFieldAccessor(annotatedMember, generatedMemberAccessorsClassNameSet, entityEnhancer, classOutput,
classInfo, fieldInfo, transformers);
if (annotatedMember.name().equals(DotNames.CASCADING_UPDATE_SHADOW_VARIABLE)) {
// The source method name also must be included
// targetMethodName is a required field and is always present
var targetMethodName = annotatedMember.value("targetMethodName").asString();
var methodInfo = classInfo.method(targetMethodName);
buildMethodAccessor(null, generatedMemberAccessorsClassNameSet, entityEnhancer, classOutput,
classInfo, methodInfo, false, transformers);
} else if (annotatedMember.name().equals(DotNames.SHADOW_VARIABLE)
&& annotatedMember.value("supplierName") != null) {
// The source method name also must be included
var targetMethodName = annotatedMember.value("supplierName")
.asString();
var methodInfo = classInfo.method(targetMethodName);
buildMethodAccessor(annotatedMember, generatedMemberAccessorsClassNameSet, entityEnhancer,
classOutput,
classInfo, methodInfo, true, transformers);
}
}
case METHOD -> {
var methodInfo = annotatedMember.target().asMethod();
var classInfo = methodInfo.declaringClass();
classInfo = methodInfo.declaringClass();
memberName = methodInfo.name();
buildMethodAccessor(annotatedMember, generatedMemberAccessorsClassNameSet, entityEnhancer, classOutput,
classInfo, methodInfo, true, transformers);
}
Expand All @@ -1041,6 +1029,30 @@ private GeneratedGizmoClasses generateDomainAccessors(Map<String, SolverConfig>
"The member (%s) is not on a field or method.".formatted(annotatedMember));
}
}
if (annotatedMember.name().equals(DotNames.CASCADING_UPDATE_SHADOW_VARIABLE)) {
// The source method name also must be included
// targetMethodName is a required field and is always present
var targetMethodName = annotatedMember.value("targetMethodName").asString();
var methodInfo = classInfo.method(targetMethodName);
buildMethodAccessor(null, generatedMemberAccessorsClassNameSet, entityEnhancer, classOutput,
classInfo, methodInfo, false, transformers);
} else if (annotatedMember.name().equals(DotNames.SHADOW_VARIABLE)
&& annotatedMember.value("supplierName") != null) {
// The source method name also must be included
var targetMethodName = annotatedMember.value("supplierName")
.asString();
var methodInfo = classInfo.method(targetMethodName);
if (methodInfo == null) {
throw new IllegalArgumentException("""
@%s (%s) defines a supplierMethod (%s) that does not exist inside its declaring class (%s).
Maybe you misspelled the supplierMethod name?"""
.formatted(ShadowVariable.class.getSimpleName(), memberName, targetMethodName,
classInfo.name().toString()));
}
buildMethodAccessor(annotatedMember, generatedMemberAccessorsClassNameSet, entityEnhancer,
classOutput,
classInfo, methodInfo, true, transformers);
}
}
// The ConstraintWeightOverrides field is not annotated, but it needs a member accessor
var solutionClassInstance = planningSolutionAnnotationInstanceCollection.iterator().next();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@

package ai.timefold.solver.quarkus;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.fail;

import ai.timefold.solver.quarkus.testdomain.suppliervariable.missing.TestdataQuarkusDeclarativeMissingSupplierEasyScoreCalculator;
import ai.timefold.solver.quarkus.testdomain.suppliervariable.missing.TestdataQuarkusDeclarativeMissingSupplierEntity;
import ai.timefold.solver.quarkus.testdomain.suppliervariable.missing.TestdataQuarkusDeclarativeMissingSupplierSolution;
import ai.timefold.solver.quarkus.testdomain.suppliervariable.missing.TestdataQuarkusDeclarativeMissingSupplierValue;

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 io.quarkus.test.QuarkusUnitTest;

class TimefoldProcessorMissingSupplierForDeclarativeVariableTest {

// Empty classes
@RegisterExtension
static final QuarkusUnitTest config1 = new QuarkusUnitTest()
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class).addClasses(
TestdataQuarkusDeclarativeMissingSupplierSolution.class,
TestdataQuarkusDeclarativeMissingSupplierEntity.class,
TestdataQuarkusDeclarativeMissingSupplierValue.class,
TestdataQuarkusDeclarativeMissingSupplierEasyScoreCalculator.class))
.assertException(t -> assertThat(t)
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContainingAll(
"@ShadowVariable (endTime)",
"supplierMethod (calculateEndTime) that does not exist",
"inside its declaring class (ai.timefold.solver.quarkus.testdomain.suppliervariable.missing.TestdataQuarkusDeclarativeMissingSupplierValue).",
"Maybe you misspelled the supplierMethod name?"));

@Test
void test() {
fail("Should not call this method.");
}
}
Loading
Loading