/* * Copyright 2012 Red Hat, Inc. and/or its affiliates. * * 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.score.director; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.IdentityHashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import org.optaplanner.core.api.domain.solution.cloner.SolutionCloner; import org.optaplanner.core.api.score.Score; import org.optaplanner.core.api.score.constraint.ConstraintMatch; import org.optaplanner.core.api.score.constraint.ConstraintMatchTotal; import org.optaplanner.core.impl.domain.entity.descriptor.EntityDescriptor; import org.optaplanner.core.impl.domain.lookup.ClassAndPlanningIdComparator; import org.optaplanner.core.impl.domain.lookup.LookUpManager; 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; import org.optaplanner.core.impl.domain.variable.listener.support.VariableListenerSupport; import org.optaplanner.core.impl.domain.variable.supply.SupplyManager; import org.optaplanner.core.impl.score.definition.ScoreDefinition; import org.optaplanner.core.impl.solver.ChildThreadType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Abstract superclass for {@link ScoreDirector}. * <p> * Implementation note: Extending classes should follow these guidelines: * <ul> * <li>before* method: last statement should be a call to the super method</li> * <li>after* method: first statement should be a call to the super method</li> * </ul> * @see ScoreDirector */ public abstract class AbstractScoreDirector<Solution_, Factory_ extends AbstractScoreDirectorFactory<Solution_>> implements InnerScoreDirector<Solution_>, Cloneable { protected final transient Logger logger = LoggerFactory.getLogger(getClass()); protected final Factory_ scoreDirectorFactory; protected final boolean lookUpEnabled; protected final LookUpManager lookUpManager; protected boolean constraintMatchEnabledPreference; protected final VariableListenerSupport<Solution_> variableListenerSupport; protected Solution_ workingSolution; protected long workingEntityListRevision = 0L; protected Integer workingInitScore = null; protected boolean allChangesWillBeUndoneBeforeStepEnds = false; protected long calculationCount = 0L; protected AbstractScoreDirector(Factory_ scoreDirectorFactory, boolean lookUpEnabled, boolean constraintMatchEnabledPreference) { this.scoreDirectorFactory = scoreDirectorFactory; this.lookUpEnabled = lookUpEnabled; lookUpManager = lookUpEnabled ? new LookUpManager(scoreDirectorFactory.getSolutionDescriptor().getLookUpStrategyResolver()) : null; this.constraintMatchEnabledPreference = constraintMatchEnabledPreference; variableListenerSupport = new VariableListenerSupport<>(this); variableListenerSupport.linkVariableListeners(); } @Override public Factory_ getScoreDirectorFactory() { return scoreDirectorFactory; } @Override public SolutionDescriptor<Solution_> getSolutionDescriptor() { return scoreDirectorFactory.getSolutionDescriptor(); } @Override public ScoreDefinition getScoreDefinition() { return scoreDirectorFactory.getScoreDefinition(); } public boolean isLookUpEnabled() { return lookUpEnabled; } public boolean isConstraintMatchEnabledPreference() { return constraintMatchEnabledPreference; } @Override public void overwriteConstraintMatchEnabledPreference(boolean constraintMatchEnabledPreference) { this.constraintMatchEnabledPreference = constraintMatchEnabledPreference; } @Override public Solution_ getWorkingSolution() { return workingSolution; } @Override public long getWorkingEntityListRevision() { return workingEntityListRevision; } public boolean isAllChangesWillBeUndoneBeforeStepEnds() { return allChangesWillBeUndoneBeforeStepEnds; } @Override public void setAllChangesWillBeUndoneBeforeStepEnds(boolean allChangesWillBeUndoneBeforeStepEnds) { this.allChangesWillBeUndoneBeforeStepEnds = allChangesWillBeUndoneBeforeStepEnds; } @Override public long getCalculationCount() { return calculationCount; } @Override public void resetCalculationCount() { this.calculationCount = 0L; } @Override public SupplyManager getSupplyManager() { return variableListenerSupport; } // ************************************************************************ // Complex methods // ************************************************************************ @Override public void setWorkingSolution(Solution_ workingSolution) { this.workingSolution = workingSolution; SolutionDescriptor<Solution_> solutionDescriptor = getSolutionDescriptor(); workingInitScore = - solutionDescriptor.countUninitializedVariables(workingSolution); if (lookUpEnabled) { lookUpManager.resetWorkingObjects(solutionDescriptor.getAllFacts(workingSolution)); } variableListenerSupport.resetWorkingSolution(); setWorkingEntityListDirty(); } @Override public boolean isWorkingEntityListDirty(long expectedWorkingEntityListRevision) { return workingEntityListRevision != expectedWorkingEntityListRevision; } protected void setWorkingEntityListDirty() { workingEntityListRevision++; } @Override public Solution_ cloneWorkingSolution() { return cloneSolution(workingSolution); } @Override public Solution_ cloneSolution(Solution_ originalSolution) { SolutionDescriptor<Solution_> solutionDescriptor = getSolutionDescriptor(); Score originalScore = solutionDescriptor.getScore(originalSolution); Solution_ cloneSolution = solutionDescriptor.getSolutionCloner().cloneSolution(originalSolution); Score cloneScore = solutionDescriptor.getScore(cloneSolution); if (scoreDirectorFactory.isAssertClonedSolution()) { if (!Objects.equals(originalScore, cloneScore)) { throw new IllegalStateException("Cloning corruption: " + "the original's score (" + originalScore + ") is different from the clone's score (" + cloneScore + ").\n" + "Check the " + SolutionCloner.class.getSimpleName() + "."); } List<Object> originalEntityList = solutionDescriptor.getEntityList(originalSolution); Map<Object, Object> originalEntityMap = new IdentityHashMap<>(originalEntityList.size()); for (Object originalEntity : originalEntityList) { originalEntityMap.put(originalEntity, null); } for (Object cloneEntity : solutionDescriptor.getEntityList(cloneSolution)) { if (originalEntityMap.containsKey(cloneEntity)) { throw new IllegalStateException("Cloning corruption: " + "the same entity (" + cloneEntity + ") is present in both the original and the clone.\n" + "So when a planning variable in the original solution changes, " + "the cloned solution will change too.\n" + "Check the " + SolutionCloner.class.getSimpleName() + "."); } } } return cloneSolution; } @Override public int getWorkingEntityCount() { return getSolutionDescriptor().getEntityCount(workingSolution); } @Override public List<Object> getWorkingEntityList() { return getSolutionDescriptor().getEntityList(workingSolution); } @Override public int getWorkingValueCount() { return getSolutionDescriptor().getValueCount(workingSolution); } @Override public void triggerVariableListeners() { variableListenerSupport.triggerVariableListenersInNotificationQueues(); } protected void setCalculatedScore(Score score) { getSolutionDescriptor().setScore(workingSolution, score); calculationCount++; } @Override public AbstractScoreDirector<Solution_, Factory_> clone() { // Breaks incremental score calculation. // Subclasses should overwrite this method to avoid breaking it if possible. AbstractScoreDirector<Solution_, Factory_> clone = (AbstractScoreDirector<Solution_, Factory_>) scoreDirectorFactory.buildScoreDirector(isLookUpEnabled(), constraintMatchEnabledPreference); clone.setWorkingSolution(cloneWorkingSolution()); return clone; } @Override public InnerScoreDirector<Solution_> createChildThreadScoreDirector(ChildThreadType childThreadType) { AbstractScoreDirector<Solution_, Factory_> childThreadScoreDirector = (AbstractScoreDirector<Solution_, Factory_>) scoreDirectorFactory.buildScoreDirector(false, constraintMatchEnabledPreference); if (childThreadType == ChildThreadType.PART_THREAD) { // ScoreCalculationCountTermination takes into account previous phases // but the calculationCount of partitions is maxed, not summed. childThreadScoreDirector.calculationCount = calculationCount; } else { throw new IllegalStateException("The childThreadType (" + childThreadType + ") is not implemented."); } return childThreadScoreDirector; } @Override public void dispose() { workingSolution = null; workingInitScore = null; if (lookUpEnabled) { lookUpManager.clearWorkingObjects(); } variableListenerSupport.clearWorkingSolution(); } // ************************************************************************ // Entity/variable add/change/remove methods // ************************************************************************ @Override public final void beforeEntityAdded(Object entity) { beforeEntityAdded(getSolutionDescriptor().findEntityDescriptorOrFail(entity.getClass()), entity); } @Override public final void afterEntityAdded(Object entity) { afterEntityAdded(getSolutionDescriptor().findEntityDescriptorOrFail(entity.getClass()), entity); } @Override public final void beforeVariableChanged(Object entity, String variableName) { VariableDescriptor variableDescriptor = getSolutionDescriptor() .findVariableDescriptorOrFail(entity, variableName); beforeVariableChanged(variableDescriptor, entity); } @Override public final void afterVariableChanged(Object entity, String variableName) { VariableDescriptor variableDescriptor = getSolutionDescriptor() .findVariableDescriptorOrFail(entity, variableName); afterVariableChanged(variableDescriptor, entity); } @Override public final void beforeEntityRemoved(Object entity) { beforeEntityRemoved(getSolutionDescriptor().findEntityDescriptorOrFail(entity.getClass()), entity); } @Override public final void afterEntityRemoved(Object entity) { afterEntityRemoved(getSolutionDescriptor().findEntityDescriptorOrFail(entity.getClass()), entity); } public void beforeEntityAdded(EntityDescriptor<Solution_> entityDescriptor, Object entity) { variableListenerSupport.beforeEntityAdded(entityDescriptor, entity); } public void afterEntityAdded(EntityDescriptor<Solution_> entityDescriptor, Object entity) { workingInitScore -= entityDescriptor.countUninitializedVariables(entity); if (lookUpEnabled) { lookUpManager.addWorkingObject(entity); } variableListenerSupport.afterEntityAdded(entityDescriptor, entity); if (!allChangesWillBeUndoneBeforeStepEnds) { setWorkingEntityListDirty(); } } @Override public void beforeVariableChanged(VariableDescriptor variableDescriptor, Object entity) { if (variableDescriptor.isGenuineAndUninitialized(entity)) { workingInitScore++; } variableListenerSupport.beforeVariableChanged(variableDescriptor, entity); } @Override public void afterVariableChanged(VariableDescriptor variableDescriptor, Object entity) { if (variableDescriptor.isGenuineAndUninitialized(entity)) { workingInitScore--; } variableListenerSupport.afterVariableChanged(variableDescriptor, entity); } @Override public void changeVariableFacade(VariableDescriptor variableDescriptor, Object entity, Object newValue) { beforeVariableChanged(variableDescriptor, entity); variableDescriptor.setValue(entity, newValue); afterVariableChanged(variableDescriptor, entity); } public void beforeEntityRemoved(EntityDescriptor<Solution_> entityDescriptor, Object entity) { workingInitScore += entityDescriptor.countUninitializedVariables(entity); variableListenerSupport.beforeEntityRemoved(entityDescriptor, entity); } public void afterEntityRemoved(EntityDescriptor<Solution_> entityDescriptor, Object entity) { if (lookUpEnabled) { lookUpManager.removeWorkingObject(entity); } variableListenerSupport.afterEntityRemoved(entityDescriptor, entity); if (!allChangesWillBeUndoneBeforeStepEnds) { setWorkingEntityListDirty(); } } // ************************************************************************ // Problem fact add/change/remove methods // ************************************************************************ @Override public void beforeProblemFactAdded(Object problemFact) { // Do nothing } @Override public void afterProblemFactAdded(Object problemFact) { if (lookUpEnabled) { lookUpManager.addWorkingObject(problemFact); } variableListenerSupport.resetWorkingSolution(); // TODO do not nuke it } @Override public void beforeProblemPropertyChanged(Object problemFactOrEntity) { // Do nothing } @Override public void afterProblemPropertyChanged(Object problemFactOrEntity) { variableListenerSupport.resetWorkingSolution(); // TODO do not nuke it } @Override public void beforeProblemFactRemoved(Object problemFact) { // Do nothing } @Override public void afterProblemFactRemoved(Object problemFact) { if (lookUpEnabled) { lookUpManager.removeWorkingObject(problemFact); } variableListenerSupport.resetWorkingSolution(); // TODO do not nuke it } @Override public <E> E lookUpWorkingObject(E externalObject) { if (!lookUpEnabled) { throw new IllegalStateException("When lookUpEnabled (" + lookUpEnabled + ") is disabled in the constructor, this method should not be called."); } return lookUpManager.lookUpWorkingObject(externalObject); } // ************************************************************************ // Assert methods // ************************************************************************ @Override public void assertExpectedWorkingScore(Score expectedWorkingScore, Object completedAction) { Score workingScore = calculateScore(); if (!expectedWorkingScore.equals(workingScore)) { throw new IllegalStateException( "Score corruption: the expectedWorkingScore (" + expectedWorkingScore + ") is not the workingScore (" + workingScore + ") after completedAction (" + completedAction + ")."); } } @Override public void assertShadowVariablesAreNotStale(Score expectedWorkingScore, Object completedAction) { SolutionDescriptor<Solution_> solutionDescriptor = getSolutionDescriptor(); Map<Object, Map<ShadowVariableDescriptor, Object>> entityToShadowVariableValuesMap = new IdentityHashMap<>(); for (Iterator<Object> it = solutionDescriptor.extractAllEntitiesIterator(workingSolution); it.hasNext();) { Object entity = it.next(); EntityDescriptor<Solution_> entityDescriptor = solutionDescriptor.findEntityDescriptorOrFail(entity.getClass()); Collection<ShadowVariableDescriptor<Solution_>> shadowVariableDescriptors = entityDescriptor.getShadowVariableDescriptors(); Map<ShadowVariableDescriptor, Object> shadowVariableValuesMap = new HashMap<>(shadowVariableDescriptors.size()); for (ShadowVariableDescriptor shadowVariableDescriptor : shadowVariableDescriptors) { Object value = shadowVariableDescriptor.getValue(entity); shadowVariableValuesMap.put(shadowVariableDescriptor, value); } entityToShadowVariableValuesMap.put(entity, shadowVariableValuesMap); } variableListenerSupport.triggerAllVariableListeners(); for (Iterator<Object> it = solutionDescriptor.extractAllEntitiesIterator(workingSolution); it.hasNext();) { Object entity = it.next(); EntityDescriptor<Solution_> entityDescriptor = solutionDescriptor.findEntityDescriptorOrFail(entity.getClass()); Collection<ShadowVariableDescriptor<Solution_>> shadowVariableDescriptors = entityDescriptor.getShadowVariableDescriptors(); Map<ShadowVariableDescriptor, Object> shadowVariableValuesMap = entityToShadowVariableValuesMap.get(entity); for (ShadowVariableDescriptor shadowVariableDescriptor : shadowVariableDescriptors) { Object newValue = shadowVariableDescriptor.getValue(entity); Object originalValue = shadowVariableValuesMap.get(shadowVariableDescriptor); if (!Objects.equals(originalValue, newValue)) { throw new IllegalStateException(VariableListener.class.getSimpleName() + " corruption:" + " the entity (" + entity + ")'s shadow variable (" + shadowVariableDescriptor.getSimpleEntityAndVariableName() + ")'s corrupted value (" + originalValue + ") changed to uncorrupted value (" + newValue + ") after all " + VariableListener.class.getSimpleName() + "s were triggered without changes to the genuine variables.\n" + "Maybe the " + VariableListener.class.getSimpleName() + " class (" + shadowVariableDescriptor.getVariableListenerClass().getSimpleName() + ") for that shadow variable (" + shadowVariableDescriptor.getSimpleEntityAndVariableName() + ") forgot to update it when one of its sources changed" + " after completedAction (" + completedAction + ")."); } } } Score workingScore = calculateScore(); if (!expectedWorkingScore.equals(workingScore)) { throw new IllegalStateException("Impossible " + VariableListener.class.getSimpleName() + " corruption:" + " the expectedWorkingScore (" + expectedWorkingScore + ") is not the workingScore (" + workingScore + ") after all " + VariableListener.class.getSimpleName() + "s were triggered without changes to the genuine variables.\n" + "But all the shadow variable values are still the same, so this is impossible."); } } @Override public void assertWorkingScoreFromScratch(Score workingScore, Object completedAction) { InnerScoreDirectorFactory<Solution_> assertionScoreDirectorFactory = scoreDirectorFactory.getAssertionScoreDirectorFactory(); if (assertionScoreDirectorFactory == null) { assertionScoreDirectorFactory = scoreDirectorFactory; } InnerScoreDirector<Solution_> uncorruptedScoreDirector = assertionScoreDirectorFactory.buildScoreDirector(false, true); uncorruptedScoreDirector.setWorkingSolution(workingSolution); Score uncorruptedScore = uncorruptedScoreDirector.calculateScore(); if (!workingScore.equals(uncorruptedScore)) { String scoreCorruptionAnalysis = buildScoreCorruptionAnalysis(uncorruptedScoreDirector); uncorruptedScoreDirector.dispose(); throw new IllegalStateException( "Score corruption: the workingScore (" + workingScore + ") is not the uncorruptedScore (" + uncorruptedScore + ") after completedAction (" + completedAction + "):\n" + scoreCorruptionAnalysis); } else { uncorruptedScoreDirector.dispose(); } } /** * @param uncorruptedScoreDirector never null * @return never null */ protected String buildScoreCorruptionAnalysis(ScoreDirector<Solution_> uncorruptedScoreDirector) { if (!isConstraintMatchEnabled() || !uncorruptedScoreDirector.isConstraintMatchEnabled()) { return " Score corruption analysis could not be generated because" + " either corrupted constraintMatchEnabled (" + isConstraintMatchEnabled() + ") or uncorrupted constraintMatchEnabled (" + uncorruptedScoreDirector.isConstraintMatchEnabled() + ") is disabled.\n" + " Check your score constraints manually."; } Collection<ConstraintMatchTotal> corruptedConstraintMatchTotals = getConstraintMatchTotals(); Collection<ConstraintMatchTotal> uncorruptedConstraintMatchTotals = uncorruptedScoreDirector.getConstraintMatchTotals(); // The order of justificationLists for score rules that include accumulates isn't stable, so we make it stable. ClassAndPlanningIdComparator comparator = new ClassAndPlanningIdComparator(false); for (ConstraintMatchTotal constraintMatchTotal : corruptedConstraintMatchTotals) { for (ConstraintMatch constraintMatch : constraintMatchTotal.getConstraintMatchSet()) { constraintMatch.getJustificationList().sort(comparator); } } for (ConstraintMatchTotal constraintMatchTotal : uncorruptedConstraintMatchTotals) { for (ConstraintMatch constraintMatch : constraintMatchTotal.getConstraintMatchSet()) { constraintMatch.getJustificationList().sort(comparator); } } Map<List<Object>, ConstraintMatch> corruptedMap = createConstraintMatchMap(corruptedConstraintMatchTotals); Map<List<Object>, ConstraintMatch> excessMap = new LinkedHashMap<>(corruptedMap); Map<List<Object>, ConstraintMatch> missingMap = createConstraintMatchMap(uncorruptedConstraintMatchTotals); excessMap.keySet().removeAll(missingMap.keySet()); // missingMap == uncorruptedMap missingMap.keySet().removeAll(corruptedMap.keySet()); final int CONSTRAINT_MATCH_DISPLAY_LIMIT = 8; StringBuilder analysis = new StringBuilder(); if (excessMap.isEmpty()) { analysis.append(" The corrupted scoreDirector has no ConstraintMatch(s) which are in excess.\n"); } else { analysis.append(" The corrupted scoreDirector has ").append(excessMap.size()) .append(" ConstraintMatch(s) which are in excess (and should not be there):\n"); int count = 0; for (ConstraintMatch constraintMatch : excessMap.values()) { if (count >= CONSTRAINT_MATCH_DISPLAY_LIMIT) { analysis.append(" ... ").append(excessMap.size() - CONSTRAINT_MATCH_DISPLAY_LIMIT) .append(" more\n"); break; } analysis.append(" ").append(constraintMatch).append("\n"); count++; } } if (missingMap.isEmpty()) { analysis.append(" The corrupted scoreDirector has no ConstraintMatch(s) which are missing.\n"); } else { analysis.append(" The corrupted scoreDirector has ").append(missingMap.size()) .append(" ConstraintMatch(s) which are missing:\n"); int count = 0; for (ConstraintMatch constraintMatch : missingMap.values()) { if (count >= CONSTRAINT_MATCH_DISPLAY_LIMIT) { analysis.append(" ... ").append(missingMap.size() - CONSTRAINT_MATCH_DISPLAY_LIMIT) .append(" more\n"); break; } analysis.append(" ").append(constraintMatch).append("\n"); count++; } } if (excessMap.isEmpty() && missingMap.isEmpty()) { analysis.append(" The corrupted scoreDirector has no ConstraintMatch(s) in excess or missing." + " That could be a bug in this class (").append(getClass()).append(").\n"); } analysis.append(" Check your score constraints."); return analysis.toString(); } private Map<List<Object>, ConstraintMatch> createConstraintMatchMap( Collection<ConstraintMatchTotal> constraintMatchTotals) { Map<List<Object>, ConstraintMatch> constraintMatchMap = new LinkedHashMap<>(constraintMatchTotals.size() * 16); for (ConstraintMatchTotal constraintMatchTotal : constraintMatchTotals) { for (ConstraintMatch constraintMatch : constraintMatchTotal.getConstraintMatchSet()) { ConstraintMatch previousConstraintMatch = constraintMatchMap.put( Arrays.<Object>asList( constraintMatchTotal.getConstraintPackage(), constraintMatchTotal.getConstraintName(), constraintMatch.getJustificationList(), constraintMatch.getScore()), constraintMatch); if (previousConstraintMatch != null) { throw new IllegalStateException("Score corruption because the constraintMatch (" + constraintMatch + ") was added twice for constraintMatchTotal (" + constraintMatchTotal + ") without removal."); } } } return constraintMatchMap; } @Override public String toString() { return getClass().getSimpleName() + "(" + calculationCount + ")"; } }