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 variableListenerClass(); + Class 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 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 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; + }