diff --git a/optaplanner-core/src/main/java/org/optaplanner/core/api/domain/variable/CustomShadowVariable.java b/optaplanner-core/src/main/java/org/optaplanner/core/api/domain/variable/CustomShadowVariable.java
index 258111dc97..8129a9989e 100644
--- a/optaplanner-core/src/main/java/org/optaplanner/core/api/domain/variable/CustomShadowVariable.java
+++ b/optaplanner-core/src/main/java/org/optaplanner/core/api/domain/variable/CustomShadowVariable.java
@@ -18,6 +18,7 @@
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
+import java.util.Comparator;
import org.optaplanner.core.api.domain.entity.PlanningEntity;
import org.optaplanner.core.impl.domain.variable.listener.VariableListener;
@@ -34,6 +35,12 @@
@Retention(RUNTIME)
public @interface CustomShadowVariable {
+ /**
+ * Use this when this shadow variable is updated by the {@link VariableListener} of another {@link @CustomShadowVariable}.
+ * @return null if (and only if) any of the other fields is non null.
+ */
+ PlanningVariableReference variableListenerRef() default @PlanningVariableReference(variableName = "");
+
/**
* A {@link VariableListener} gets notified after a source planning variable has changed.
* That listener changes the shadow variable (often recursively on multiple planning entities) accordingly,
@@ -41,19 +48,23 @@
*
* For example: VRP with time windows uses a {@link VariableListener} to update the arrival times
* of all the trailing entities when an entity is changed.
- * @return never null
+ * @return never null (unless {@link #variableListenerRef()} is not null)
*/
- Class extends VariableListener> variableListenerClass();
+ Class extends VariableListener> variableListenerClass() default NullVariableListener.class;
+
+ /** Workaround for annotation limitation in {@link #variableListenerClass()}. */
+ interface NullVariableListener extends VariableListener {}
/**
* The source variables (masters) that trigger a change to this shadow variable (slave).
- * @return never null, at least 1
+ * @return never null (unless {@link #variableListenerRef()} is not null), at least 1
*/
- Source[] sources();
+ Source[] sources() default {};
/**
* Declares which genuine variable (or other shadow variable) causes the shadow variable to change.
*/
+ // TODO Replace with @PlanningVariableReference when upgrading to 7.0
public static @interface Source {
/**
diff --git a/optaplanner-core/src/main/java/org/optaplanner/core/api/domain/variable/PlanningVariable.java b/optaplanner-core/src/main/java/org/optaplanner/core/api/domain/variable/PlanningVariable.java
index 8ce196aabd..5cbd141f43 100644
--- a/optaplanner-core/src/main/java/org/optaplanner/core/api/domain/variable/PlanningVariable.java
+++ b/optaplanner-core/src/main/java/org/optaplanner/core/api/domain/variable/PlanningVariable.java
@@ -43,7 +43,7 @@
* Any {@link ValueRangeProvider} annotation on a {@link PlanningSolution} or {@link PlanningEntity}
* will automatically be registered with it's {@link ValueRangeProvider#id()}.
*
- * There should be at least 1 valueRangeRef.
+ * There should be at least 1 element in this array.
* @return 1 (or more) registered {@link ValueRangeProvider#id()}
*/
String[] valueRangeProviderRefs() default {};
diff --git a/optaplanner-core/src/main/java/org/optaplanner/core/api/domain/variable/PlanningVariableReference.java b/optaplanner-core/src/main/java/org/optaplanner/core/api/domain/variable/PlanningVariableReference.java
new file mode 100644
index 0000000000..a1bfa70447
--- /dev/null
+++ b/optaplanner-core/src/main/java/org/optaplanner/core/api/domain/variable/PlanningVariableReference.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2015 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.optaplanner.core.api.domain.variable;
+
+import org.optaplanner.core.api.domain.entity.PlanningEntity;
+
+/**
+ * A reference to a genuine {@link PlanningVariable} or a shadow variable.
+ */
+public @interface PlanningVariableReference {
+
+ /**
+ * The {@link PlanningEntity} class of the planning variable.
+ *
+ * Specified if the planning variable is on a different {@link Class}
+ * than the class that uses this referencing annotation.
+ * @return {@link NullEntityClass} when it is null (workaround for annotation limitation).
+ * Defaults to the same {@link Class} as the one that uses this annotation.
+ */
+ Class> entityClass() default NullEntityClass.class;
+
+ /** Workaround for annotation limitation in {@link #entityClass()}. */
+ interface NullEntityClass {}
+
+ /**
+ * The name of the planning variable that is referenced.
+ * @return never null, a genuine or shadow variable name
+ */
+ String variableName();
+
+}
diff --git a/optaplanner-core/src/main/java/org/optaplanner/core/impl/domain/solution/descriptor/SolutionDescriptor.java b/optaplanner-core/src/main/java/org/optaplanner/core/impl/domain/solution/descriptor/SolutionDescriptor.java
index bbde98a83d..d005429726 100644
--- a/optaplanner-core/src/main/java/org/optaplanner/core/impl/domain/solution/descriptor/SolutionDescriptor.java
+++ b/optaplanner-core/src/main/java/org/optaplanner/core/impl/domain/solution/descriptor/SolutionDescriptor.java
@@ -41,7 +41,6 @@
import org.optaplanner.core.api.domain.solution.cloner.PlanningCloneable;
import org.optaplanner.core.api.domain.solution.cloner.SolutionCloner;
import org.optaplanner.core.api.domain.valuerange.ValueRangeProvider;
-import org.optaplanner.core.api.domain.variable.PlanningVariable;
import org.optaplanner.core.api.score.Score;
import org.optaplanner.core.config.util.ConfigUtils;
import org.optaplanner.core.impl.domain.common.AlphabeticMemberComparator;
diff --git a/optaplanner-core/src/main/java/org/optaplanner/core/impl/domain/variable/custom/CustomShadowVariableDescriptor.java b/optaplanner-core/src/main/java/org/optaplanner/core/impl/domain/variable/custom/CustomShadowVariableDescriptor.java
index b3863160f5..4b0c0deffb 100644
--- a/optaplanner-core/src/main/java/org/optaplanner/core/impl/domain/variable/custom/CustomShadowVariableDescriptor.java
+++ b/optaplanner-core/src/main/java/org/optaplanner/core/impl/domain/variable/custom/CustomShadowVariableDescriptor.java
@@ -21,11 +21,11 @@
import java.util.List;
import org.optaplanner.core.api.domain.variable.CustomShadowVariable;
+import org.optaplanner.core.api.domain.variable.PlanningVariableReference;
import org.optaplanner.core.config.util.ConfigUtils;
import org.optaplanner.core.impl.domain.common.accessor.MemberAccessor;
import org.optaplanner.core.impl.domain.entity.descriptor.EntityDescriptor;
import org.optaplanner.core.impl.domain.policy.DescriptorPolicy;
-import org.optaplanner.core.impl.domain.solution.descriptor.SolutionDescriptor;
import org.optaplanner.core.impl.domain.variable.descriptor.ShadowVariableDescriptor;
import org.optaplanner.core.impl.domain.variable.descriptor.VariableDescriptor;
import org.optaplanner.core.impl.domain.variable.listener.VariableListener;
@@ -34,6 +34,8 @@
public class CustomShadowVariableDescriptor extends ShadowVariableDescriptor {
+ protected ShadowVariableDescriptor refVariableDescriptor;
+
protected Class extends VariableListener> variableListenerClass;
protected List sourceVariableDescriptorList;
@@ -49,57 +51,117 @@ public void processAnnotations(DescriptorPolicy descriptorPolicy) {
private void processPropertyAnnotations(DescriptorPolicy descriptorPolicy) {
CustomShadowVariable shadowVariableAnnotation = variableMemberAccessor
.getAnnotation(CustomShadowVariable.class);
+ PlanningVariableReference variableListenerRef = shadowVariableAnnotation.variableListenerRef();
+ if (variableListenerRef.variableName().equals("")) {
+ variableListenerRef = null;
+ }
variableListenerClass = shadowVariableAnnotation.variableListenerClass();
+ if (variableListenerClass == CustomShadowVariable.NullVariableListener.class) {
+ variableListenerClass = null;
+ }
CustomShadowVariable.Source[] sources = shadowVariableAnnotation.sources();
- if (sources.length < 1) {
- throw new IllegalArgumentException("The entityClass (" + entityDescriptor.getEntityClass()
- + ") has a " + CustomShadowVariable.class.getSimpleName()
- + " annotated property (" + variableMemberAccessor.getName()
- + ") with sources (" + Arrays.toString(sources)
- + ") which is empty.");
+ if (variableListenerRef != null) {
+ if (variableListenerClass != null || sources.length > 0) {
+ throw new IllegalArgumentException("The entityClass (" + entityDescriptor.getEntityClass()
+ + ") has a " + CustomShadowVariable.class.getSimpleName()
+ + " annotated property (" + variableMemberAccessor.getName()
+ + ") with a non-null variableListenerRef (" + variableListenerRef
+ + "), so it can not have a variableListenerClass (" + variableListenerClass
+ + ") nor any sources (" + Arrays.toString(sources) + ").");
+ }
+ } else {
+ if (variableListenerClass == null) {
+ throw new IllegalArgumentException("The entityClass (" + entityDescriptor.getEntityClass()
+ + ") has a " + CustomShadowVariable.class.getSimpleName()
+ + " annotated property (" + variableMemberAccessor.getName()
+ + ") which lacks a variableListenerClass (" + variableListenerClass + ").");
+ }
+ if (sources.length < 1) {
+ throw new IllegalArgumentException("The entityClass (" + entityDescriptor.getEntityClass()
+ + ") has a " + CustomShadowVariable.class.getSimpleName()
+ + " annotated property (" + variableMemberAccessor.getName()
+ + ") with sources (" + Arrays.toString(sources)
+ + ") which is empty.");
+ }
}
}
+ @Override
public void linkShadowSources(DescriptorPolicy descriptorPolicy) {
CustomShadowVariable shadowVariableAnnotation = variableMemberAccessor
.getAnnotation(CustomShadowVariable.class);
- SolutionDescriptor solutionDescriptor = entityDescriptor.getSolutionDescriptor();
- CustomShadowVariable.Source[] sources = shadowVariableAnnotation.sources();
- sourceVariableDescriptorList = new ArrayList(sources.length);
- for (CustomShadowVariable.Source source : sources) {
- EntityDescriptor sourceEntityDescriptor;
- Class> sourceEntityClass = source.entityClass();
- if (sourceEntityClass.equals(CustomShadowVariable.Source.NullEntityClass.class)) {
- sourceEntityDescriptor = entityDescriptor;
+ PlanningVariableReference variableListenerRef = shadowVariableAnnotation.variableListenerRef();
+ if (variableListenerRef.variableName().equals("")) {
+ variableListenerRef = null;
+ }
+ if (variableListenerRef != null) {
+ EntityDescriptor refEntityDescriptor;
+ Class> refEntityClass = variableListenerRef.entityClass();
+ if (refEntityClass.equals(PlanningVariableReference.NullEntityClass.class)) {
+ refEntityDescriptor = entityDescriptor;
} else {
- sourceEntityDescriptor = solutionDescriptor.findEntityDescriptor(sourceEntityClass);
- if (sourceEntityDescriptor == null) {
+ refEntityDescriptor = entityDescriptor.getSolutionDescriptor().findEntityDescriptor(refEntityClass);
+ if (refEntityDescriptor == null) {
throw new IllegalArgumentException("The entityClass (" + entityDescriptor.getEntityClass()
+ ") has a " + CustomShadowVariable.class.getSimpleName()
+ " annotated property (" + variableMemberAccessor.getName()
- + ") with a sourceEntityClass (" + sourceEntityClass
+ + ") with a refEntityClass (" + refEntityClass
+ ") which is not a valid planning entity.");
}
}
- String sourceVariableName = source.variableName();
- VariableDescriptor sourceVariableDescriptor = sourceEntityDescriptor.getVariableDescriptor(
- sourceVariableName);
- if (sourceVariableDescriptor == null) {
+ String refVariableName = variableListenerRef.variableName();
+ refVariableDescriptor = refEntityDescriptor.getShadowVariableDescriptor(refVariableName);
+ if (refVariableDescriptor == null) {
throw new IllegalArgumentException("The entityClass (" + entityDescriptor.getEntityClass()
+ ") has a " + CustomShadowVariable.class.getSimpleName()
+ " annotated property (" + variableMemberAccessor.getName()
- + ") with sourceVariableName (" + sourceVariableName
+ + ") with refVariableName (" + refVariableName
+ ") which is not a valid planning variable on entityClass ("
- + sourceEntityDescriptor.getEntityClass() + ").\n"
- + entityDescriptor.buildInvalidVariableNameExceptionMessage(sourceVariableName));
+ + refEntityDescriptor.getEntityClass() + ").\n"
+ + entityDescriptor.buildInvalidVariableNameExceptionMessage(refVariableName));
+ }
+ } else {
+ CustomShadowVariable.Source[] sources = shadowVariableAnnotation.sources();
+ sourceVariableDescriptorList = new ArrayList(sources.length);
+ for (CustomShadowVariable.Source source : sources) {
+ EntityDescriptor sourceEntityDescriptor;
+ Class> sourceEntityClass = source.entityClass();
+ if (sourceEntityClass.equals(CustomShadowVariable.Source.NullEntityClass.class)) {
+ sourceEntityDescriptor = entityDescriptor;
+ } else {
+ sourceEntityDescriptor = entityDescriptor.getSolutionDescriptor()
+ .findEntityDescriptor(sourceEntityClass);
+ if (sourceEntityDescriptor == null) {
+ throw new IllegalArgumentException("The entityClass (" + entityDescriptor.getEntityClass()
+ + ") has a " + CustomShadowVariable.class.getSimpleName()
+ + " annotated property (" + variableMemberAccessor.getName()
+ + ") with a sourceEntityClass (" + sourceEntityClass
+ + ") which is not a valid planning entity.");
+ }
+ }
+ String sourceVariableName = source.variableName();
+ VariableDescriptor sourceVariableDescriptor = sourceEntityDescriptor.getVariableDescriptor(
+ sourceVariableName);
+ if (sourceVariableDescriptor == null) {
+ throw new IllegalArgumentException("The entityClass (" + entityDescriptor.getEntityClass()
+ + ") has a " + CustomShadowVariable.class.getSimpleName()
+ + " annotated property (" + variableMemberAccessor.getName()
+ + ") with sourceVariableName (" + sourceVariableName
+ + ") which is not a valid planning variable on entityClass ("
+ + sourceEntityDescriptor.getEntityClass() + ").\n"
+ + entityDescriptor.buildInvalidVariableNameExceptionMessage(sourceVariableName));
+ }
+ sourceVariableDescriptor.registerShadowVariableDescriptor(this);
+ sourceVariableDescriptorList.add(sourceVariableDescriptor);
}
- sourceVariableDescriptor.registerShadowVariableDescriptor(this);
- sourceVariableDescriptorList.add(sourceVariableDescriptor);
}
}
@Override
public List getSourceVariableDescriptorList() {
+ if (refVariableDescriptor != null) {
+ return refVariableDescriptor.getSourceVariableDescriptorList();
+ }
return sourceVariableDescriptorList;
}
@@ -114,6 +176,11 @@ public Demand getProvidedDemand() {
@Override
public VariableListener buildVariableListener(InnerScoreDirector scoreDirector) {
+ if (refVariableDescriptor != null) {
+ throw new IllegalStateException("The shadowVariableDescriptor (" + this
+ + ") references another shadowVariableDescriptor (" + refVariableDescriptor
+ + ") so it cannot build a " + VariableListener.class.getSimpleName() + ".");
+ }
return ConfigUtils.newInstance(this, "variableListenerClass", variableListenerClass);
}
diff --git a/optaplanner-core/src/main/java/org/optaplanner/core/impl/domain/variable/descriptor/VariableDescriptor.java b/optaplanner-core/src/main/java/org/optaplanner/core/impl/domain/variable/descriptor/VariableDescriptor.java
index 092a77acbf..de9c77eb71 100644
--- a/optaplanner-core/src/main/java/org/optaplanner/core/impl/domain/variable/descriptor/VariableDescriptor.java
+++ b/optaplanner-core/src/main/java/org/optaplanner/core/impl/domain/variable/descriptor/VariableDescriptor.java
@@ -65,6 +65,9 @@ public void registerShadowVariableDescriptor(ShadowVariableDescriptor shadowVari
shadowVariableDescriptorList.add(shadowVariableDescriptor);
}
+ /**
+ * @return never null, only direct, non-referencing shadow variables
+ */
public List getShadowVariableDescriptorList() {
return shadowVariableDescriptorList;
}
diff --git a/optaplanner-core/src/test/java/org/optaplanner/core/impl/domain/variable/custom/CustomVariableListenerTest.java b/optaplanner-core/src/test/java/org/optaplanner/core/impl/domain/variable/custom/CustomVariableListenerTest.java
index 2ea27584df..d85655494a 100644
--- a/optaplanner-core/src/test/java/org/optaplanner/core/impl/domain/variable/custom/CustomVariableListenerTest.java
+++ b/optaplanner-core/src/test/java/org/optaplanner/core/impl/domain/variable/custom/CustomVariableListenerTest.java
@@ -19,6 +19,7 @@
import java.util.Arrays;
import org.junit.Test;
+import org.optaplanner.core.impl.domain.entity.descriptor.EntityDescriptor;
import org.optaplanner.core.impl.domain.solution.descriptor.SolutionDescriptor;
import org.optaplanner.core.impl.domain.variable.descriptor.GenuineVariableDescriptor;
import org.optaplanner.core.impl.score.director.InnerScoreDirector;
@@ -27,6 +28,8 @@
import org.optaplanner.core.impl.testdata.domain.shadow.extended.TestdataExtendedShadowedChildEntity;
import org.optaplanner.core.impl.testdata.domain.shadow.extended.TestdataExtendedShadowedParentEntity;
import org.optaplanner.core.impl.testdata.domain.shadow.extended.TestdataExtendedShadowedSolution;
+import org.optaplanner.core.impl.testdata.domain.shadow.manytomany.TestdataManyToManyShadowedEntity;
+import org.optaplanner.core.impl.testdata.domain.shadow.manytomany.TestdataManyToManyShadowedSolution;
import org.optaplanner.core.impl.testdata.util.PlannerTestUtils;
import static org.junit.Assert.*;
@@ -83,4 +86,67 @@ public void extendedZigZag() {
assertEquals("3/firstShadow/secondShadow/thirdShadow", c.getThirdShadow());
}
+ @Test
+ public void manyToMany() {
+ EntityDescriptor entityDescriptor = TestdataManyToManyShadowedEntity.buildEntityDescriptor();
+ GenuineVariableDescriptor primaryVariableDescriptor = entityDescriptor
+ .getGenuineVariableDescriptor("primaryValue");
+ GenuineVariableDescriptor secondaryVariableDescriptor = entityDescriptor
+ .getGenuineVariableDescriptor("secondaryValue");
+ InnerScoreDirector scoreDirector = PlannerTestUtils.mockScoreDirector(
+ primaryVariableDescriptor.getEntityDescriptor().getSolutionDescriptor());
+
+ TestdataValue val1 = new TestdataValue("1");
+ TestdataValue val2 = new TestdataValue("2");
+ TestdataValue val3 = new TestdataValue("3");
+ TestdataValue val4 = new TestdataValue("4");
+ TestdataManyToManyShadowedEntity a = new TestdataManyToManyShadowedEntity("a", null, null);
+ TestdataManyToManyShadowedEntity b = new TestdataManyToManyShadowedEntity("b", null, null);
+ TestdataManyToManyShadowedEntity c = new TestdataManyToManyShadowedEntity("c", null, null);
+
+ TestdataManyToManyShadowedSolution solution = new TestdataManyToManyShadowedSolution("solution");
+ solution.setEntityList(Arrays.asList(a, b, c));
+ solution.setValueList(Arrays.asList(val1, val2, val3, val4));
+ scoreDirector.setWorkingSolution(solution);
+
+ scoreDirector.beforeVariableChanged(primaryVariableDescriptor, a);
+ a.setPrimaryValue(val1);
+ scoreDirector.afterVariableChanged(primaryVariableDescriptor, a);
+ assertEquals(null, a.getComposedCode());
+ assertEquals(null, a.getReverseComposedCode());
+
+ scoreDirector.beforeVariableChanged(secondaryVariableDescriptor, a);
+ a.setSecondaryValue(val3);
+ scoreDirector.afterVariableChanged(secondaryVariableDescriptor, a);
+ assertEquals("1-3", a.getComposedCode());
+ assertEquals("3-1", a.getReverseComposedCode());
+
+ scoreDirector.beforeVariableChanged(secondaryVariableDescriptor, a);
+ a.setSecondaryValue(val4);
+ scoreDirector.afterVariableChanged(secondaryVariableDescriptor, a);
+ assertEquals("1-4", a.getComposedCode());
+ assertEquals("4-1", a.getReverseComposedCode());
+
+ scoreDirector.beforeVariableChanged(primaryVariableDescriptor, a);
+ a.setPrimaryValue(val2);
+ scoreDirector.afterVariableChanged(primaryVariableDescriptor, a);
+ assertEquals("2-4", a.getComposedCode());
+ assertEquals("4-2", a.getReverseComposedCode());
+
+ scoreDirector.beforeVariableChanged(primaryVariableDescriptor, a);
+ a.setPrimaryValue(null);
+ scoreDirector.afterVariableChanged(primaryVariableDescriptor, a);
+ assertEquals(null, a.getComposedCode());
+ assertEquals(null, a.getReverseComposedCode());
+
+ scoreDirector.beforeVariableChanged(primaryVariableDescriptor, c);
+ c.setPrimaryValue(val1);
+ scoreDirector.afterVariableChanged(primaryVariableDescriptor, c);
+ scoreDirector.beforeVariableChanged(secondaryVariableDescriptor, c);
+ c.setSecondaryValue(val3);
+ scoreDirector.afterVariableChanged(secondaryVariableDescriptor, c);
+ assertEquals("1-3", c.getComposedCode());
+ assertEquals("3-1", c.getReverseComposedCode());
+ }
+
}
diff --git a/optaplanner-core/src/test/java/org/optaplanner/core/impl/testdata/domain/shadow/cyclic/TestdataCyclicShadowedEntity.java b/optaplanner-core/src/test/java/org/optaplanner/core/impl/testdata/domain/shadow/cyclic/TestdataCyclicShadowedEntity.java
index 5143d3a8d0..4a2b5584fd 100644
--- a/optaplanner-core/src/test/java/org/optaplanner/core/impl/testdata/domain/shadow/cyclic/TestdataCyclicShadowedEntity.java
+++ b/optaplanner-core/src/test/java/org/optaplanner/core/impl/testdata/domain/shadow/cyclic/TestdataCyclicShadowedEntity.java
@@ -109,17 +109,19 @@ public static class RockShadowUpdatingVariableListener extends VariableListenerA
@Override
public void afterEntityAdded(ScoreDirector scoreDirector, TestdataCyclicShadowedEntity entity) {
- updateShadow(entity);
+ updateShadow(entity, scoreDirector);
}
@Override
public void afterVariableChanged(ScoreDirector scoreDirector, TestdataCyclicShadowedEntity entity) {
- updateShadow(entity);
+ updateShadow(entity, scoreDirector);
}
- private void updateShadow(TestdataCyclicShadowedEntity entity) {
+ private void updateShadow(TestdataCyclicShadowedEntity entity, ScoreDirector scoreDirector) {
String scissors = entity.getScissorsShadow();
+ scoreDirector.beforeVariableChanged(entity, "rockShadow");
entity.setRockShadow("Rock beats (" + scissors + ")");
+ scoreDirector.afterVariableChanged(entity, "rockShadow");
}
}
@@ -128,17 +130,19 @@ public static class PaperShadowUpdatingVariableListener extends VariableListener
@Override
public void afterEntityAdded(ScoreDirector scoreDirector, TestdataCyclicShadowedEntity entity) {
- updateShadow(entity);
+ updateShadow(entity, scoreDirector);
}
@Override
public void afterVariableChanged(ScoreDirector scoreDirector, TestdataCyclicShadowedEntity entity) {
- updateShadow(entity);
+ updateShadow(entity, scoreDirector);
}
- private void updateShadow(TestdataCyclicShadowedEntity entity) {
+ private void updateShadow(TestdataCyclicShadowedEntity entity, ScoreDirector scoreDirector) {
String rock = entity.getRockShadow();
+ scoreDirector.beforeVariableChanged(entity, "paperShadow");
entity.setPaperShadow("Paper beats (" + rock + ")");
+ scoreDirector.afterVariableChanged(entity, "paperShadow");
}
}
@@ -147,17 +151,19 @@ public static class ScissorsShadowUpdatingVariableListener extends VariableListe
@Override
public void afterEntityAdded(ScoreDirector scoreDirector, TestdataCyclicShadowedEntity entity) {
- updateShadow(entity);
+ updateShadow(entity, scoreDirector);
}
@Override
public void afterVariableChanged(ScoreDirector scoreDirector, TestdataCyclicShadowedEntity entity) {
- updateShadow(entity);
+ updateShadow(entity, scoreDirector);
}
- private void updateShadow(TestdataCyclicShadowedEntity entity) {
+ private void updateShadow(TestdataCyclicShadowedEntity entity, ScoreDirector scoreDirector) {
String paper = entity.getPaperShadow();
+ scoreDirector.beforeVariableChanged(entity, "scissorsShadow");
entity.setScissorsShadow("Scissors beats (" + paper + ")");
+ scoreDirector.afterVariableChanged(entity, "scissorsShadow");
}
}
diff --git a/optaplanner-core/src/test/java/org/optaplanner/core/impl/testdata/domain/shadow/manytomany/TestdataManyToManyShadowedEntity.java b/optaplanner-core/src/test/java/org/optaplanner/core/impl/testdata/domain/shadow/manytomany/TestdataManyToManyShadowedEntity.java
new file mode 100644
index 0000000000..93a50a1702
--- /dev/null
+++ b/optaplanner-core/src/test/java/org/optaplanner/core/impl/testdata/domain/shadow/manytomany/TestdataManyToManyShadowedEntity.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2015 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.optaplanner.core.impl.testdata.domain.shadow.manytomany;
+
+import org.optaplanner.core.api.domain.entity.PlanningEntity;
+import org.optaplanner.core.api.domain.variable.CustomShadowVariable;
+import org.optaplanner.core.api.domain.variable.PlanningVariable;
+import org.optaplanner.core.api.domain.variable.PlanningVariableReference;
+import org.optaplanner.core.impl.domain.entity.descriptor.EntityDescriptor;
+import org.optaplanner.core.impl.domain.solution.descriptor.SolutionDescriptor;
+import org.optaplanner.core.impl.domain.variable.listener.VariableListenerAdapter;
+import org.optaplanner.core.impl.score.director.ScoreDirector;
+import org.optaplanner.core.impl.testdata.domain.TestdataObject;
+import org.optaplanner.core.impl.testdata.domain.TestdataValue;
+
+@PlanningEntity
+public class TestdataManyToManyShadowedEntity extends TestdataObject {
+
+ public static EntityDescriptor buildEntityDescriptor() {
+ SolutionDescriptor solutionDescriptor = TestdataManyToManyShadowedSolution.buildSolutionDescriptor();
+ return solutionDescriptor.findEntityDescriptorOrFail(TestdataManyToManyShadowedEntity.class);
+ }
+
+ private TestdataValue primaryValue;
+ private TestdataValue secondaryValue;
+ private String composedCode;
+ private String reverseComposedCode;
+
+ public TestdataManyToManyShadowedEntity() {
+ }
+
+ public TestdataManyToManyShadowedEntity(String code) {
+ super(code);
+ }
+
+ public TestdataManyToManyShadowedEntity(String code, TestdataValue primaryValue, TestdataValue secondaryValue) {
+ this(code);
+ this.primaryValue = primaryValue;
+ this.secondaryValue = secondaryValue;
+ }
+
+ @PlanningVariable(valueRangeProviderRefs = "valueRange")
+ public TestdataValue getPrimaryValue() {
+ return primaryValue;
+ }
+
+ public void setPrimaryValue(TestdataValue primaryValue) {
+ this.primaryValue = primaryValue;
+ }
+
+ @PlanningVariable(valueRangeProviderRefs = "valueRange")
+ public TestdataValue getSecondaryValue() {
+ return secondaryValue;
+ }
+
+ public void setSecondaryValue(TestdataValue secondaryValue) {
+ this.secondaryValue = secondaryValue;
+ }
+
+ @CustomShadowVariable(variableListenerClass = ComposedValuesUpdatingVariableListener.class,
+ sources = {@CustomShadowVariable.Source(variableName = "primaryValue"),
+ @CustomShadowVariable.Source(variableName = "secondaryValue")})
+ public String getComposedCode() {
+ return composedCode;
+ }
+
+ public void setComposedCode(String composedCode) {
+ this.composedCode = composedCode;
+ }
+
+ @CustomShadowVariable(variableListenerRef = @PlanningVariableReference(variableName = "composedCode"))
+ public String getReverseComposedCode() {
+ return reverseComposedCode;
+ }
+
+ public void setReverseComposedCode(String reverseComposedCode) {
+ this.reverseComposedCode = reverseComposedCode;
+ }
+
+ // ************************************************************************
+ // Complex methods
+ // ************************************************************************
+
+ // ************************************************************************
+ // Static inner classes
+ // ************************************************************************
+
+ public static class ComposedValuesUpdatingVariableListener extends VariableListenerAdapter {
+
+ @Override
+ public void afterEntityAdded(ScoreDirector scoreDirector, TestdataManyToManyShadowedEntity entity) {
+ updateShadow(entity, scoreDirector);
+ }
+
+ @Override
+ public void afterVariableChanged(ScoreDirector scoreDirector, TestdataManyToManyShadowedEntity entity) {
+ updateShadow(entity, scoreDirector);
+ }
+
+ private void updateShadow(TestdataManyToManyShadowedEntity entity, ScoreDirector scoreDirector) {
+ TestdataValue primaryValue = entity.getPrimaryValue();
+ TestdataValue secondaryValue = entity.getSecondaryValue();
+ String composedValue;
+ String reverseComposedValue;
+ if (primaryValue == null || secondaryValue == null) {
+ composedValue = null;
+ reverseComposedValue = null;
+ } else {
+ composedValue = primaryValue.getCode() + "-" + secondaryValue.getCode();
+ reverseComposedValue = secondaryValue.getCode() + "-" + primaryValue.getCode();
+ }
+ scoreDirector.beforeVariableChanged(entity, "composedCode");
+ entity.setComposedCode(composedValue);
+ scoreDirector.afterVariableChanged(entity, "composedCode");
+ scoreDirector.beforeVariableChanged(entity, "reverseComposedCode");
+ entity.setReverseComposedCode(reverseComposedValue);
+ scoreDirector.afterVariableChanged(entity, "reverseComposedCode");
+ }
+
+ }
+
+}
diff --git a/optaplanner-core/src/test/java/org/optaplanner/core/impl/testdata/domain/shadow/manytomany/TestdataManyToManyShadowedSolution.java b/optaplanner-core/src/test/java/org/optaplanner/core/impl/testdata/domain/shadow/manytomany/TestdataManyToManyShadowedSolution.java
new file mode 100644
index 0000000000..54b383dfe2
--- /dev/null
+++ b/optaplanner-core/src/test/java/org/optaplanner/core/impl/testdata/domain/shadow/manytomany/TestdataManyToManyShadowedSolution.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2015 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.optaplanner.core.impl.testdata.domain.shadow.manytomany;
+
+import java.util.Collection;
+import java.util.List;
+
+import org.optaplanner.core.api.domain.solution.PlanningEntityCollectionProperty;
+import org.optaplanner.core.api.domain.solution.PlanningSolution;
+import org.optaplanner.core.api.domain.solution.Solution;
+import org.optaplanner.core.api.domain.valuerange.ValueRangeProvider;
+import org.optaplanner.core.api.score.buildin.simple.SimpleScore;
+import org.optaplanner.core.impl.domain.solution.descriptor.SolutionDescriptor;
+import org.optaplanner.core.impl.testdata.domain.TestdataObject;
+import org.optaplanner.core.impl.testdata.domain.TestdataValue;
+
+@PlanningSolution
+public class TestdataManyToManyShadowedSolution extends TestdataObject implements Solution {
+
+ public static SolutionDescriptor buildSolutionDescriptor() {
+ return SolutionDescriptor.buildSolutionDescriptor(TestdataManyToManyShadowedSolution.class,
+ TestdataManyToManyShadowedEntity.class);
+ }
+
+ private List valueList;
+ private List entityList;
+
+ private SimpleScore score;
+
+ public TestdataManyToManyShadowedSolution() {
+ }
+
+ public TestdataManyToManyShadowedSolution(String code) {
+ super(code);
+ }
+
+ @ValueRangeProvider(id = "valueRange")
+ 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;
+ }
+
+ public SimpleScore getScore() {
+ return score;
+ }
+
+ public void setScore(SimpleScore score) {
+ this.score = score;
+ }
+
+ // ************************************************************************
+ // Complex methods
+ // ************************************************************************
+
+ public Collection extends Object> getProblemFacts() {
+ return valueList;
+ }
+
+}
diff --git a/optaplanner-distribution/src/main/assembly/filtered-resources/UpgradeFromPreviousVersionRecipe.txt b/optaplanner-distribution/src/main/assembly/filtered-resources/UpgradeFromPreviousVersionRecipe.txt
index 1443606206..5f7e0b588f 100644
--- a/optaplanner-distribution/src/main/assembly/filtered-resources/UpgradeFromPreviousVersionRecipe.txt
+++ b/optaplanner-distribution/src/main/assembly/filtered-resources/UpgradeFromPreviousVersionRecipe.txt
@@ -2540,3 +2540,33 @@ After in *SolverConfig.xml and *BenchmarkConfig.xml:
From 6.3.0.Beta2 to 6.3.0.CR1
-----------------------------
+[MINOR] If a custom VariableListener changes 2 shadow variables, use the new variableListenerRef property accordingly
+to indicate that the VariableListener class of another shadow variable also updates this shadow variable:
+Before in *.java:
+ @PlanningVariable(...)
+ public Standstill getPreviousStandstill() {
+ return previousStandstill;
+ }
+ @CustomShadowVariable(variableListenerClass = TransportTimeAndCapacityUpdatingVariableListener.class,
+ sources = {@CustomShadowVariable.Source(variableName = "previousStandstill")})
+ public Integer getTransportTime() {
+ return transportTime;
+ }
+ @CustomShadowVariable(variableListenerClass = DummyListener.class, sources = ...)
+ public Integer getCapacity() {
+ return capacity;
+ }
+After in *.java:
+ @PlanningVariable(...)
+ public Standstill getPreviousStandstill() {
+ return previousStandstill;
+ }
+ @CustomShadowVariable(variableListenerClass = TransportTimeAndCapacityUpdatingVariableListener.class,
+ sources = {@CustomShadowVariable.Source(variableName = "previousStandstill")})
+ public Integer getTransportTime() {
+ return transportTime;
+ }
+ @CustomShadowVariable(variableListenerRef = @PlanningVariableReference(variableName = "transportTime"))
+ public Integer getCapacity() {
+ return capacity;
+ }
diff --git a/optaplanner-docs/src/main/docbook/en-US/Chapter-Planner_configuration/Chapter-Planner_configuration.xml b/optaplanner-docs/src/main/docbook/en-US/Chapter-Planner_configuration/Chapter-Planner_configuration.xml
index c20d7bdfda..eceddc4702 100755
--- a/optaplanner-docs/src/main/docbook/en-US/Chapter-Planner_configuration/Chapter-Planner_configuration.xml
+++ b/optaplanner-docs/src/main/docbook/en-US/Chapter-Planner_configuration/Chapter-Planner_configuration.xml
@@ -1251,6 +1251,27 @@ public class Customer {
Any change of a shadow variable must be told to the ScoreDirector.
+
+ If one VariableListener changes two shadow variables (because having two separate
+ VariableListeners would be inefficient), then annotate only the first shadow variable with
+ the variableListenerClass and let the other shadow variable(s) reference the first shadow
+ variable:
+
+ @PlanningVariable(...)
+ public Standstill getPreviousStandstill() {
+ return previousStandstill;
+ }
+
+ @CustomShadowVariable(variableListenerClass = TransportTimeAndCapacityUpdatingVariableListener.class,
+ sources = {@CustomShadowVariable.Source(variableName = "previousStandstill")})
+ public Integer getTransportTime() {
+ return transportTime;
+ }
+
+ @CustomShadowVariable(variableListenerRef = @PlanningVariableReference(variableName = "transportTime"))
+ public Integer getCapacity() {
+ return capacity;
+ }