/* * Copyright 2010 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.examples.common.business; import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import javax.swing.SwingUtilities; import org.apache.commons.io.FileUtils; import org.optaplanner.core.api.domain.solution.PlanningSolution; import org.optaplanner.core.api.score.Score; import org.optaplanner.core.api.score.constraint.ConstraintMatchTotal; import org.optaplanner.core.api.score.constraint.Indictment; import org.optaplanner.core.api.solver.Solver; 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.domain.variable.inverserelation.SingletonInverseVariableDemand; import org.optaplanner.core.impl.domain.variable.inverserelation.SingletonInverseVariableSupply; import org.optaplanner.core.impl.domain.variable.supply.SupplyManager; import org.optaplanner.core.impl.heuristic.move.Move; import org.optaplanner.core.impl.heuristic.selector.move.generic.ChangeMove; import org.optaplanner.core.impl.heuristic.selector.move.generic.SwapMove; import org.optaplanner.core.impl.heuristic.selector.move.generic.chained.ChainedChangeMove; import org.optaplanner.core.impl.heuristic.selector.move.generic.chained.ChainedSwapMove; import org.optaplanner.core.impl.score.director.InnerScoreDirector; import org.optaplanner.core.impl.score.director.ScoreDirector; import org.optaplanner.core.impl.score.director.ScoreDirectorFactory; import org.optaplanner.core.impl.solver.ProblemFactChange; import org.optaplanner.examples.common.app.CommonApp; import org.optaplanner.examples.common.persistence.AbstractSolutionExporter; import org.optaplanner.examples.common.persistence.AbstractSolutionImporter; import org.optaplanner.examples.common.persistence.SolutionDao; import org.optaplanner.examples.common.swingui.SolverAndPersistenceFrame; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @param <Solution_> the solution type, the class with the {@link PlanningSolution} annotation */ public class SolutionBusiness<Solution_> { private static final ProblemFileComparator FILE_COMPARATOR = new ProblemFileComparator(); protected final transient Logger logger = LoggerFactory.getLogger(getClass()); private final CommonApp app; private SolutionDao<Solution_> solutionDao; private AbstractSolutionImporter<Solution_>[] importers; private AbstractSolutionExporter<Solution_> exporter; private File importDataDir; private File unsolvedDataDir; private File solvedDataDir; private File exportDataDir; // volatile because the solve method doesn't come from the event thread (like every other method call) private volatile Solver<Solution_> solver; private String solutionFileName = null; private ScoreDirector<Solution_> guiScoreDirector; private final AtomicReference<Solution_> skipToBestSolutionRef = new AtomicReference<>(); public SolutionBusiness(CommonApp app) { this.app = app; } public String getAppName() { return app.getName(); } public String getAppDescription() { return app.getDescription(); } public String getAppIconResource() { return app.getIconResource(); } public void setSolutionDao(SolutionDao<Solution_> solutionDao) { this.solutionDao = solutionDao; } public AbstractSolutionImporter<Solution_>[] getImporters() { return importers; } public void setImporters(AbstractSolutionImporter<Solution_>[] importers) { this.importers = importers; } public void setExporter(AbstractSolutionExporter<Solution_> exporter) { this.exporter = exporter; } public String getDirName() { return solutionDao.getDirName(); } public boolean hasImporter() { return importers.length > 0; } public boolean hasExporter() { return exporter != null; } public void updateDataDirs() { File dataDir = solutionDao.getDataDir(); if (hasImporter()) { importDataDir = new File(dataDir, "import"); if (!importDataDir.exists()) { throw new IllegalStateException("The directory importDataDir (" + importDataDir.getAbsolutePath() + ") does not exist."); } } unsolvedDataDir = new File(dataDir, "unsolved"); if (!unsolvedDataDir.exists()) { throw new IllegalStateException("The directory unsolvedDataDir (" + unsolvedDataDir.getAbsolutePath() + ") does not exist."); } solvedDataDir = new File(dataDir, "solved"); if (!solvedDataDir.exists() && !solvedDataDir.mkdir()) { throw new IllegalStateException("The directory solvedDataDir (" + solvedDataDir.getAbsolutePath() + ") does not exist and could not be created."); } if (hasExporter()) { exportDataDir = new File(dataDir, "export"); if (!exportDataDir.exists() && !exportDataDir.mkdir()) { throw new IllegalStateException("The directory exportDataDir (" + exportDataDir.getAbsolutePath() + ") does not exist and could not be created."); } } } public File getImportDataDir() { return importDataDir; } public File getUnsolvedDataDir() { return unsolvedDataDir; } public File getSolvedDataDir() { return solvedDataDir; } public File getExportDataDir() { return exportDataDir; } public String getExportFileSuffix() { return exporter.getOutputFileSuffix(); } public void setSolver(Solver<Solution_> solver) { this.solver = solver; ScoreDirectorFactory<Solution_> scoreDirectorFactory = solver.getScoreDirectorFactory(); guiScoreDirector = scoreDirectorFactory.buildScoreDirector(); } public List<File> getUnsolvedFileList() { List<File> fileList = new ArrayList<>( FileUtils.listFiles(unsolvedDataDir, new String[]{solutionDao.getFileExtension()}, true)); Collections.sort(fileList, FILE_COMPARATOR); return fileList; } public List<File> getSolvedFileList() { List<File> fileList = new ArrayList<>( FileUtils.listFiles(solvedDataDir, new String[]{solutionDao.getFileExtension()}, true)); Collections.sort(fileList, FILE_COMPARATOR); return fileList; } public Solution_ getSolution() { return guiScoreDirector.getWorkingSolution(); } public void setSolution(Solution_ solution) { guiScoreDirector.setWorkingSolution(solution); } public String getSolutionFileName() { return solutionFileName; } public Score getScore() { return guiScoreDirector.calculateScore(); } public boolean isSolving() { return solver.isSolving(); } public void registerForBestSolutionChanges(final SolverAndPersistenceFrame solverAndPersistenceFrame) { solver.addEventListener(event -> { // Called on the Solver thread, so not on the Swing Event thread /* * Avoid ConcurrentModificationException when there is an unprocessed ProblemFactChange * because the paint method uses the same problem facts instances as the Solver's workingSolution * unlike the planning entities of the bestSolution which are cloned from the Solver's workingSolution */ if (solver.isEveryProblemFactChangeProcessed()) { // The final is also needed for thread visibility final Solution_ newBestSolution = event.getNewBestSolution(); skipToBestSolutionRef.set(newBestSolution); SwingUtilities.invokeLater(() -> { // Called on the Swing Event thread Solution_ skipToBestSolution = skipToBestSolutionRef.get(); // Skip this event if a newer one arrived meanwhile to avoid flooding the Swing Event thread if (newBestSolution != skipToBestSolution) { return; } guiScoreDirector.setWorkingSolution(newBestSolution); solverAndPersistenceFrame.bestSolutionChanged(); }); } }); } public boolean isConstraintMatchEnabled() { return guiScoreDirector.isConstraintMatchEnabled(); } public List<ConstraintMatchTotal> getConstraintMatchTotalList() { List<ConstraintMatchTotal> constraintMatchTotalList = new ArrayList<>( guiScoreDirector.getConstraintMatchTotals()); Collections.sort(constraintMatchTotalList); return constraintMatchTotalList; } public Map<Object, Indictment> getIndictmentMap() { return guiScoreDirector.getIndictmentMap(); } public void importSolution(File file) { AbstractSolutionImporter<Solution_> importer = determineImporter(file); Solution_ solution = importer.readSolution(file); solutionFileName = file.getName(); guiScoreDirector.setWorkingSolution(solution); } private AbstractSolutionImporter<Solution_> determineImporter(File file) { for (AbstractSolutionImporter<Solution_> importer : importers) { if (importer.acceptInputFile(file)) { return importer; } } return importers[0]; } public void openSolution(File file) { Solution_ solution = solutionDao.readSolution(file); solutionFileName = file.getName(); guiScoreDirector.setWorkingSolution(solution); } public void saveSolution(File file) { Solution_ solution = guiScoreDirector.getWorkingSolution(); solutionDao.writeSolution(solution, file); } public void exportSolution(File file) { Solution_ solution = guiScoreDirector.getWorkingSolution(); exporter.writeSolution(solution, file); } public void doMove(Move<Solution_> move) { if (solver.isSolving()) { logger.error("Not doing user move ({}) because the solver is solving.", move); return; } if (!move.isMoveDoable(guiScoreDirector)) { logger.warn("Not doing user move ({}) because it is not doable.", move); return; } logger.info("Doing user move ({}).", move); move.doMove(guiScoreDirector); guiScoreDirector.calculateScore(); } public void doProblemFactChange(ProblemFactChange<Solution_> problemFactChange) { if (solver.isSolving()) { solver.addProblemFactChange(problemFactChange); } else { problemFactChange.doChange(guiScoreDirector); guiScoreDirector.calculateScore(); } } /** * Can be called on any thread. * <p> * Note: This method does not change the guiScoreDirector because that can only be changed on the event thread. * @param planningProblem never null * @return never null */ public Solution_ solve(Solution_ planningProblem) { return solver.solve(planningProblem); } public void terminateSolvingEarly() { solver.terminateEarly(); } public ChangeMove<Solution_> createChangeMove(Object entity, String variableName, Object toPlanningValue) { // TODO Solver should support building a ChangeMove InnerScoreDirector<Solution_> guiInnerScoreDirector = (InnerScoreDirector<Solution_>) this.guiScoreDirector; SolutionDescriptor<Solution_> solutionDescriptor = guiInnerScoreDirector.getSolutionDescriptor(); GenuineVariableDescriptor<Solution_> variableDescriptor = solutionDescriptor.findGenuineVariableDescriptorOrFail( entity, variableName); if (variableDescriptor.isChained()) { SupplyManager supplyManager = guiInnerScoreDirector.getSupplyManager(); SingletonInverseVariableSupply inverseVariableSupply = supplyManager.demand( new SingletonInverseVariableDemand(variableDescriptor)); return new ChainedChangeMove<>(entity, variableDescriptor, inverseVariableSupply, toPlanningValue); } else { return new ChangeMove<>(entity, variableDescriptor, toPlanningValue); } } public void doChangeMove(Object entity, String variableName, Object toPlanningValue) { ChangeMove<Solution_> move = createChangeMove(entity, variableName, toPlanningValue); doMove(move); } public SwapMove<Solution_> createSwapMove(Object leftEntity, Object rightEntity) { // TODO Solver should support building a SwapMove InnerScoreDirector<Solution_> guiInnerScoreDirector = (InnerScoreDirector<Solution_>) this.guiScoreDirector; SolutionDescriptor<Solution_> solutionDescriptor = guiInnerScoreDirector.getSolutionDescriptor(); EntityDescriptor<Solution_> entityDescriptor = solutionDescriptor.findEntityDescriptor(leftEntity.getClass()); List<GenuineVariableDescriptor<Solution_>> variableDescriptorList = entityDescriptor.getGenuineVariableDescriptorList(); if (entityDescriptor.hasAnyChainedGenuineVariables()) { List<SingletonInverseVariableSupply> inverseVariableSupplyList = new ArrayList<>(variableDescriptorList.size()); SupplyManager supplyManager = guiInnerScoreDirector.getSupplyManager(); for (GenuineVariableDescriptor variableDescriptor : variableDescriptorList) { SingletonInverseVariableSupply inverseVariableSupply; if (variableDescriptor.isChained()) { inverseVariableSupply = supplyManager.demand( new SingletonInverseVariableDemand(variableDescriptor)); } else { inverseVariableSupply = null; } inverseVariableSupplyList.add(inverseVariableSupply); } return new ChainedSwapMove<>(variableDescriptorList, inverseVariableSupplyList, leftEntity, rightEntity); } else { return new SwapMove<>(variableDescriptorList, leftEntity, rightEntity); } } public void doSwapMove(Object leftEntity, Object rightEntity) { SwapMove<Solution_> move = createSwapMove(leftEntity, rightEntity); doMove(move); } }