Skip to content

Commit

Permalink
PLANNER-315 PLANNER-319 Two shadow vars updated by the same variable …
Browse files Browse the repository at this point in the history
…listener should not require 2 variable listener instances
  • Loading branch information
ge0ffrey committed Aug 3, 2015
1 parent 2451669 commit 9431918
Show file tree
Hide file tree
Showing 12 changed files with 511 additions and 42 deletions.
Expand Up @@ -18,6 +18,7 @@


import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.Target; import java.lang.annotation.Target;
import java.util.Comparator;


import org.optaplanner.core.api.domain.entity.PlanningEntity; import org.optaplanner.core.api.domain.entity.PlanningEntity;
import org.optaplanner.core.impl.domain.variable.listener.VariableListener; import org.optaplanner.core.impl.domain.variable.listener.VariableListener;
Expand All @@ -34,26 +35,36 @@
@Retention(RUNTIME) @Retention(RUNTIME)
public @interface CustomShadowVariable { 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. * 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, * That listener changes the shadow variable (often recursively on multiple planning entities) accordingly,
* Those shadow variables should make the score calculation more natural to write. * Those shadow variables should make the score calculation more natural to write.
* <p> * <p>
* For example: VRP with time windows uses a {@link VariableListener} to update the arrival times * 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. * 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). * 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. * 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 { public static @interface Source {


/** /**
Expand Down
Expand Up @@ -43,7 +43,7 @@
* Any {@link ValueRangeProvider} annotation on a {@link PlanningSolution} or {@link PlanningEntity} * Any {@link ValueRangeProvider} annotation on a {@link PlanningSolution} or {@link PlanningEntity}
* will automatically be registered with it's {@link ValueRangeProvider#id()}. * will automatically be registered with it's {@link ValueRangeProvider#id()}.
* <p> * <p>
* 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()} * @return 1 (or more) registered {@link ValueRangeProvider#id()}
*/ */
String[] valueRangeProviderRefs() default {}; String[] valueRangeProviderRefs() default {};
Expand Down
@@ -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.
* <p>
* 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();

}
Expand Up @@ -41,7 +41,6 @@
import org.optaplanner.core.api.domain.solution.cloner.PlanningCloneable; import org.optaplanner.core.api.domain.solution.cloner.PlanningCloneable;
import org.optaplanner.core.api.domain.solution.cloner.SolutionCloner; import org.optaplanner.core.api.domain.solution.cloner.SolutionCloner;
import org.optaplanner.core.api.domain.valuerange.ValueRangeProvider; 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.api.score.Score;
import org.optaplanner.core.config.util.ConfigUtils; import org.optaplanner.core.config.util.ConfigUtils;
import org.optaplanner.core.impl.domain.common.AlphabeticMemberComparator; import org.optaplanner.core.impl.domain.common.AlphabeticMemberComparator;
Expand Down
Expand Up @@ -21,11 +21,11 @@
import java.util.List; import java.util.List;


import org.optaplanner.core.api.domain.variable.CustomShadowVariable; 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.config.util.ConfigUtils;
import org.optaplanner.core.impl.domain.common.accessor.MemberAccessor; import org.optaplanner.core.impl.domain.common.accessor.MemberAccessor;
import org.optaplanner.core.impl.domain.entity.descriptor.EntityDescriptor; import org.optaplanner.core.impl.domain.entity.descriptor.EntityDescriptor;
import org.optaplanner.core.impl.domain.policy.DescriptorPolicy; 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.ShadowVariableDescriptor;
import org.optaplanner.core.impl.domain.variable.descriptor.VariableDescriptor; import org.optaplanner.core.impl.domain.variable.descriptor.VariableDescriptor;
import org.optaplanner.core.impl.domain.variable.listener.VariableListener; import org.optaplanner.core.impl.domain.variable.listener.VariableListener;
Expand All @@ -34,6 +34,8 @@


public class CustomShadowVariableDescriptor extends ShadowVariableDescriptor { public class CustomShadowVariableDescriptor extends ShadowVariableDescriptor {


protected ShadowVariableDescriptor refVariableDescriptor;

protected Class<? extends VariableListener> variableListenerClass; protected Class<? extends VariableListener> variableListenerClass;
protected List<VariableDescriptor> sourceVariableDescriptorList; protected List<VariableDescriptor> sourceVariableDescriptorList;


Expand All @@ -49,57 +51,117 @@ public void processAnnotations(DescriptorPolicy descriptorPolicy) {
private void processPropertyAnnotations(DescriptorPolicy descriptorPolicy) { private void processPropertyAnnotations(DescriptorPolicy descriptorPolicy) {
CustomShadowVariable shadowVariableAnnotation = variableMemberAccessor CustomShadowVariable shadowVariableAnnotation = variableMemberAccessor
.getAnnotation(CustomShadowVariable.class); .getAnnotation(CustomShadowVariable.class);
PlanningVariableReference variableListenerRef = shadowVariableAnnotation.variableListenerRef();
if (variableListenerRef.variableName().equals("")) {
variableListenerRef = null;
}
variableListenerClass = shadowVariableAnnotation.variableListenerClass(); variableListenerClass = shadowVariableAnnotation.variableListenerClass();
if (variableListenerClass == CustomShadowVariable.NullVariableListener.class) {
variableListenerClass = null;
}
CustomShadowVariable.Source[] sources = shadowVariableAnnotation.sources(); CustomShadowVariable.Source[] sources = shadowVariableAnnotation.sources();
if (sources.length < 1) { if (variableListenerRef != null) {
throw new IllegalArgumentException("The entityClass (" + entityDescriptor.getEntityClass() if (variableListenerClass != null || sources.length > 0) {
+ ") has a " + CustomShadowVariable.class.getSimpleName() throw new IllegalArgumentException("The entityClass (" + entityDescriptor.getEntityClass()
+ " annotated property (" + variableMemberAccessor.getName() + ") has a " + CustomShadowVariable.class.getSimpleName()
+ ") with sources (" + Arrays.toString(sources) + " annotated property (" + variableMemberAccessor.getName()
+ ") which is empty."); + ") 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) { public void linkShadowSources(DescriptorPolicy descriptorPolicy) {
CustomShadowVariable shadowVariableAnnotation = variableMemberAccessor CustomShadowVariable shadowVariableAnnotation = variableMemberAccessor
.getAnnotation(CustomShadowVariable.class); .getAnnotation(CustomShadowVariable.class);
SolutionDescriptor solutionDescriptor = entityDescriptor.getSolutionDescriptor(); PlanningVariableReference variableListenerRef = shadowVariableAnnotation.variableListenerRef();
CustomShadowVariable.Source[] sources = shadowVariableAnnotation.sources(); if (variableListenerRef.variableName().equals("")) {
sourceVariableDescriptorList = new ArrayList<VariableDescriptor>(sources.length); variableListenerRef = null;
for (CustomShadowVariable.Source source : sources) { }
EntityDescriptor sourceEntityDescriptor; if (variableListenerRef != null) {
Class<?> sourceEntityClass = source.entityClass(); EntityDescriptor refEntityDescriptor;
if (sourceEntityClass.equals(CustomShadowVariable.Source.NullEntityClass.class)) { Class<?> refEntityClass = variableListenerRef.entityClass();
sourceEntityDescriptor = entityDescriptor; if (refEntityClass.equals(PlanningVariableReference.NullEntityClass.class)) {
refEntityDescriptor = entityDescriptor;
} else { } else {
sourceEntityDescriptor = solutionDescriptor.findEntityDescriptor(sourceEntityClass); refEntityDescriptor = entityDescriptor.getSolutionDescriptor().findEntityDescriptor(refEntityClass);
if (sourceEntityDescriptor == null) { if (refEntityDescriptor == null) {
throw new IllegalArgumentException("The entityClass (" + entityDescriptor.getEntityClass() throw new IllegalArgumentException("The entityClass (" + entityDescriptor.getEntityClass()
+ ") has a " + CustomShadowVariable.class.getSimpleName() + ") has a " + CustomShadowVariable.class.getSimpleName()
+ " annotated property (" + variableMemberAccessor.getName() + " annotated property (" + variableMemberAccessor.getName()
+ ") with a sourceEntityClass (" + sourceEntityClass + ") with a refEntityClass (" + refEntityClass
+ ") which is not a valid planning entity."); + ") which is not a valid planning entity.");
} }
} }
String sourceVariableName = source.variableName(); String refVariableName = variableListenerRef.variableName();
VariableDescriptor sourceVariableDescriptor = sourceEntityDescriptor.getVariableDescriptor( refVariableDescriptor = refEntityDescriptor.getShadowVariableDescriptor(refVariableName);
sourceVariableName); if (refVariableDescriptor == null) {
if (sourceVariableDescriptor == null) {
throw new IllegalArgumentException("The entityClass (" + entityDescriptor.getEntityClass() throw new IllegalArgumentException("The entityClass (" + entityDescriptor.getEntityClass()
+ ") has a " + CustomShadowVariable.class.getSimpleName() + ") has a " + CustomShadowVariable.class.getSimpleName()
+ " annotated property (" + variableMemberAccessor.getName() + " annotated property (" + variableMemberAccessor.getName()
+ ") with sourceVariableName (" + sourceVariableName + ") with refVariableName (" + refVariableName
+ ") which is not a valid planning variable on entityClass (" + ") which is not a valid planning variable on entityClass ("
+ sourceEntityDescriptor.getEntityClass() + ").\n" + refEntityDescriptor.getEntityClass() + ").\n"
+ entityDescriptor.buildInvalidVariableNameExceptionMessage(sourceVariableName)); + entityDescriptor.buildInvalidVariableNameExceptionMessage(refVariableName));
}
} else {
CustomShadowVariable.Source[] sources = shadowVariableAnnotation.sources();
sourceVariableDescriptorList = new ArrayList<VariableDescriptor>(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 @Override
public List<VariableDescriptor> getSourceVariableDescriptorList() { public List<VariableDescriptor> getSourceVariableDescriptorList() {
if (refVariableDescriptor != null) {
return refVariableDescriptor.getSourceVariableDescriptorList();
}
return sourceVariableDescriptorList; return sourceVariableDescriptorList;
} }


Expand All @@ -114,6 +176,11 @@ public Demand getProvidedDemand() {


@Override @Override
public VariableListener buildVariableListener(InnerScoreDirector scoreDirector) { 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); return ConfigUtils.newInstance(this, "variableListenerClass", variableListenerClass);
} }


Expand Down
Expand Up @@ -65,6 +65,9 @@ public void registerShadowVariableDescriptor(ShadowVariableDescriptor shadowVari
shadowVariableDescriptorList.add(shadowVariableDescriptor); shadowVariableDescriptorList.add(shadowVariableDescriptor);
} }


/**
* @return never null, only direct, non-referencing shadow variables
*/
public List<ShadowVariableDescriptor> getShadowVariableDescriptorList() { public List<ShadowVariableDescriptor> getShadowVariableDescriptorList() {
return shadowVariableDescriptorList; return shadowVariableDescriptorList;
} }
Expand Down

0 comments on commit 9431918

Please sign in to comment.