/* * This file is part of LibrePlan * * Copyright (C) 2009-2010 Fundación para o Fomento da Calidade Industrial e * Desenvolvemento Tecnolóxico de Galicia * Copyright (C) 2010-2011 Igalia, S.L. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.zkoss.ganttz.data; import static java.util.Arrays.asList; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.Set; import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jgrapht.DirectedGraph; import org.jgrapht.graph.SimpleDirectedGraph; import org.zkoss.ganttz.data.DependencyType.Point; import org.zkoss.ganttz.data.ITaskFundamentalProperties.IModifications; import org.zkoss.ganttz.data.ITaskFundamentalProperties.IUpdatablePosition; import org.zkoss.ganttz.data.constraint.Constraint; import org.zkoss.ganttz.data.constraint.ConstraintOnComparableValues; import org.zkoss.ganttz.data.constraint.ConstraintOnComparableValues.ComparisonType; import org.zkoss.ganttz.data.criticalpath.ICriticalPathCalculable; import org.zkoss.ganttz.util.IAction; import org.zkoss.ganttz.util.PreAndPostNotReentrantActionsWrapper; import org.zkoss.ganttz.util.ReentranceGuard; import org.zkoss.ganttz.util.ReentranceGuard.IReentranceCases; /** * This class contains a graph with the {@link Task tasks} as vertexes and the * {@link Dependency dependency} as arcs. It enforces the rules embodied in the * dependencies and in the duration of the tasks using listeners. <br/> * * @author Óscar González Fernández <ogonzalez@igalia.com> */ public class GanttDiagramGraph<V, D extends IDependency<V>> implements ICriticalPathCalculable<V> { private static final Log LOG = LogFactory.getLog(GanttDiagramGraph.class); public static IDependenciesEnforcerHook doNothingHook() { return new IDependenciesEnforcerHook() { @Override public void setNewEnd(GanttDate previousEnd, GanttDate newEnd) { } @Override public void setStartDate(GanttDate previousStart, GanttDate previousEnd, GanttDate newStart) { } @Override public void positionPotentiallyModified() { } }; } private static final GanttZKAdapter GANTTZK_ADAPTER = new GanttZKAdapter(); public static IAdapter<Task, Dependency> taskAdapter() { return GANTTZK_ADAPTER; } public interface IAdapter<V, D extends IDependency<V>> { List<V> getChildren(V task); V getOwner(V task); boolean isContainer(V task); void registerDependenciesEnforcerHookOn(V task, IDependenciesEnforcerHookFactory<V> hookFactory); GanttDate getStartDate(V task); void setStartDateFor(V task, GanttDate newStart); GanttDate getEndDateFor(V task); void setEndDateFor(V task, GanttDate newEnd); List<Constraint<GanttDate>> getConstraints(ConstraintCalculator<V> calculator, Set<D> withDependencies, Point point); List<Constraint<GanttDate>> getStartConstraintsFor(V task); List<Constraint<GanttDate>> getEndConstraintsFor(V task); V getSource(D dependency); V getDestination(D dependency); Class<D> getDependencyType(); D createInvisibleDependency(V origin, V destination, DependencyType type); DependencyType getType(D dependency); boolean isVisible(D dependency); boolean isFixed(V task); } public static class GanttZKAdapter implements IAdapter<Task, Dependency> { @Override public List<Task> getChildren(Task task) { return task.getTasks(); } @Override public Task getOwner(Task task) { if ( task instanceof Milestone ) { Milestone milestone = (Milestone) task; return milestone.getOwner(); } return null; } @Override public Task getDestination(Dependency dependency) { return dependency.getDestination(); } @Override public Task getSource(Dependency dependency) { return dependency.getSource(); } @Override public boolean isContainer(Task task) { return task.isContainer(); } @Override public void registerDependenciesEnforcerHookOn(Task task, IDependenciesEnforcerHookFactory<Task> hookFactory) { task.registerDependenciesEnforcerHook(hookFactory); } @Override public Dependency createInvisibleDependency(Task origin, Task destination, DependencyType type) { return new Dependency(origin, destination, type, false); } @Override public Class<Dependency> getDependencyType() { return Dependency.class; } @Override public DependencyType getType(Dependency dependency) { return dependency.getType(); } @Override public boolean isVisible(Dependency dependency) { return dependency.isVisible(); } @Override public GanttDate getEndDateFor(Task task) { return task.getEndDate(); } @Override public void setEndDateFor(Task task, final GanttDate newEnd) { task.doPositionModifications(new IModifications() { @Override public void doIt(IUpdatablePosition position) { position.setEndDate(newEnd); } }); } @Override public GanttDate getStartDate(Task task) { return task.getBeginDate(); } @Override public void setStartDateFor(Task task, final GanttDate newStart) { task.doPositionModifications(new IModifications() { @Override public void doIt(IUpdatablePosition position) { position.setBeginDate(newStart); } }); } @Override public List<Constraint<GanttDate>> getConstraints(ConstraintCalculator<Task> calculator, Set<Dependency> withDependencies, Point pointBeingModified) { return Dependency.getConstraintsFor(calculator, withDependencies, pointBeingModified); } @Override public List<Constraint<GanttDate>> getStartConstraintsFor(Task task) { return task.getStartConstraints(); } @Override public List<Constraint<GanttDate>> getEndConstraintsFor(Task task) { return task.getEndConstraints(); } @Override public boolean isFixed(Task task) { return task.isFixed(); } } public static class GanttZKDiagramGraph extends GanttDiagramGraph<Task, Dependency> { private GanttZKDiagramGraph( boolean scheduleBackwards, List<Constraint<GanttDate>> globalStartConstraints, List<Constraint<GanttDate>> globalEndConstraints, boolean dependenciesConstraintsHavePriority) { super(scheduleBackwards, GANTTZK_ADAPTER, globalStartConstraints, globalEndConstraints, dependenciesConstraintsHavePriority); } } public interface IGraphChangeListener { void execute(); } public static GanttZKDiagramGraph create(boolean scheduleBackwards, List<Constraint<GanttDate>> globalStartConstraints, List<Constraint<GanttDate>> globalEndConstraints, boolean dependenciesConstraintsHavePriority) { return new GanttZKDiagramGraph(scheduleBackwards, globalStartConstraints, globalEndConstraints, dependenciesConstraintsHavePriority); } private final IAdapter<V, D> adapter; private final DirectedGraph<V, D> graph; private final TopologicalSorter topologicalSorter; private List<V> topLevelTasks = new ArrayList<>(); private Map<V, V> fromChildToParent = new HashMap<>(); private final List<Constraint<GanttDate>> globalStartConstraints; private final List<Constraint<GanttDate>> globalEndConstraints; private final boolean scheduleBackwards; private DependenciesEnforcer enforcer = new DependenciesEnforcer(); private final boolean dependenciesConstraintsHavePriority; private final ReentranceGuard positionsUpdatingGuard = new ReentranceGuard(); private final PreAndPostNotReentrantActionsWrapper preAndPostActions = new PreAndPostNotReentrantActionsWrapper() { @Override protected void postAction() { executeGraphChangeListeners(new ArrayList<>(postGraphChangeListeners)); } @Override protected void preAction() { executeGraphChangeListeners(new ArrayList<>(preGraphChangeListeners)); } private void executeGraphChangeListeners(List<IGraphChangeListener> graphChangeListeners) { for (IGraphChangeListener each : graphChangeListeners) { try { each.execute(); } catch (Exception e) { LOG.error("error executing execution listener", e); } } } }; private List<IGraphChangeListener> preGraphChangeListeners = new ArrayList<>(); private List<IGraphChangeListener> postGraphChangeListeners = new ArrayList<>(); public void addPreGraphChangeListener(IGraphChangeListener preGraphChangeListener) { preGraphChangeListeners.add(preGraphChangeListener); } public void removePreGraphChangeListener(IGraphChangeListener preGraphChangeListener) { preGraphChangeListeners.remove(preGraphChangeListener); } public void addPostGraphChangeListener(IGraphChangeListener postGraphChangeListener) { postGraphChangeListeners.add(postGraphChangeListener); } public void removePostGraphChangeListener(IGraphChangeListener postGraphChangeListener) { postGraphChangeListeners.remove(postGraphChangeListener); } public void addPreChangeListeners(Collection<? extends IGraphChangeListener> preChangeListeners) { for (IGraphChangeListener each : preChangeListeners) { addPreGraphChangeListener(each); } } public void addPostChangeListeners(Collection<? extends IGraphChangeListener> postChangeListeners) { for (IGraphChangeListener each : postChangeListeners) { addPostGraphChangeListener(each); } } public static <V, D extends IDependency<V>> GanttDiagramGraph<V, D> create( boolean scheduleBackwards, IAdapter<V, D> adapter, List<Constraint<GanttDate>> globalStartConstraints, List<Constraint<GanttDate>> globalEndConstraints, boolean dependenciesConstraintsHavePriority) { return new GanttDiagramGraph<>(scheduleBackwards, adapter, globalStartConstraints, globalEndConstraints, dependenciesConstraintsHavePriority); } protected GanttDiagramGraph(boolean scheduleBackwards, IAdapter<V, D> adapter, List<Constraint<GanttDate>> globalStartConstraints, List<Constraint<GanttDate>> globalEndConstraints, boolean dependenciesConstraintsHavePriority) { this.scheduleBackwards = scheduleBackwards; this.adapter = adapter; this.globalStartConstraints = globalStartConstraints; this.globalEndConstraints = globalEndConstraints; this.dependenciesConstraintsHavePriority = dependenciesConstraintsHavePriority; this.graph = new SimpleDirectedGraph<>(adapter.getDependencyType()); this.topologicalSorter = new TopologicalSorter(); } public void enforceAllRestrictions() { enforcer.enforceRestrictionsOn(withoutVisibleIncomingDependencies(getTopLevelTasks())); } private List<V> withoutVisibleIncomingDependencies(Collection<? extends V> tasks) { List<V> result = new ArrayList<>(); for (V each : tasks) { boolean condition = noVisibleDependencies(isScheduleForward() ? graph.incomingEdgesOf(each) : graph.outgoingEdgesOf(each)); if ( condition ) { result.add(each); } } return result; } private boolean noVisibleDependencies(Collection<? extends D> dependencies) { for (D each : dependencies) { if ( adapter.isVisible(each) ) { return false; } } return true; } public void addTopLevel(V task) { topLevelTasks.add(task); addTask(task); } public void addTopLevel(Collection<? extends V> tasks) { for (V task : tasks) { addTopLevel(task); } } public void addTasks(Collection<? extends V> tasks) { for (V t : tasks) { addTask(t); } } /** * This class is designed for performing topological sorting of nodes. * Topological sorting is used to make graph nodes not to be mixed, because * parent and child nodes ({@link TaskPoint}) must be placed in the correct order. * Also during topological sorting nodes are placed on appropriate levels. * Topological sorting can be done using different algorithms, but here is used Khan's algorithm. */ class TopologicalSorter { private Map<TaskPoint, Integer> taskPointsByDepthCached = null; /** * This method is used to place each node on appropriate level. * * @return map of TaskPoints with appropriate levels */ private Map<TaskPoint, Integer> taskPointsByDepth() { if ( taskPointsByDepthCached != null ) { return taskPointsByDepthCached; } Map<TaskPoint, Integer> result = new HashMap<>(); Map<TaskPoint, Set<TaskPoint>> visitedBy = new HashMap<>(); /* * Here are stored taskpoints that we have visited. * This need to be done because we need to check have we visited this taskpoint, or not. * Described above need to be done to avoid loop between taskpoints. */ Set<TaskPoint> visitedTaskPoints = new HashSet (); Queue<TaskPoint> withoutIncoming = getInitial(withoutVisibleIncomingDependencies(getTopLevelTasks())); for (TaskPoint each : withoutIncoming) { initializeIfNeededForKey(result, each, 0); } while (!withoutIncoming.isEmpty()) { TaskPoint current = withoutIncoming.poll(); visitedTaskPoints.add(current); // Marking taskpoint as visited // Taking all child elements for (TaskPoint each : current.getImmediateSuccessors()) { if (!visitedTaskPoints.contains(each)) { initializeIfNeededForKey(visitedBy, each, new HashSet<TaskPoint>()); Set<TaskPoint> visitors = visitedBy.get(each); visitors.add(current); // Taking parent elements Set<TaskPoint> predecessorsRequired = each.getImmediatePredecessors(); if ( visitors.containsAll(predecessorsRequired) ) { initializeIfNeededForKey(result, each, result.get(current) + 1); withoutIncoming.offer(each); } } } } return taskPointsByDepthCached = Collections.unmodifiableMap(result); } private <K, T> void initializeIfNeededForKey(Map<K, T> map, K key, T initialValue) { if ( !map.containsKey(key) ) { map.put(key, initialValue); } } private LinkedList<TaskPoint> getInitial(List<V> initial) { LinkedList<TaskPoint> result = new LinkedList<>(); for (V each : initial) { result.add(allPointsPotentiallyModified(each)); } return result; } public void recalculationNeeded() { taskPointsByDepthCached = null; } public List<Recalculation> sort(Collection<? extends Recalculation> recalculationsToBeSorted) { List<Recalculation> result = new ArrayList<>(recalculationsToBeSorted); final Map<TaskPoint, Integer> taskPointsByDepth = taskPointsByDepth(); Collections.sort(result, new Comparator<Recalculation>() { @Override public int compare(Recalculation o1, Recalculation o2) { int o1Depth = onNullDefault( taskPointsByDepth.get(o1.taskPoint), Integer.MAX_VALUE, "no depth value for " + o1.taskPoint); int o2Depth = onNullDefault( taskPointsByDepth.get(o2.taskPoint), Integer.MAX_VALUE, "no depth value for " + o2.taskPoint); int result = o1Depth - o2Depth; if ( result == 0 ) { return asInt(o1.parentRecalculation) - asInt(o2.parentRecalculation); } return result; } private int asInt(boolean b) { return b ? 1 : 0; } }); return result; } } private static <T> T onNullDefault(T value, T defaultValue, String warnMessage) { if ( value == null ) { if ( warnMessage != null ) { LOG.warn(warnMessage); } return defaultValue; } return value; } public void addTask(V original) { List<V> stack = new LinkedList<>(); stack.add(original); List<D> dependenciesToAdd = new ArrayList<>(); while (!stack.isEmpty()){ V task = stack.remove(0); graph.addVertex(task); topologicalSorter.recalculationNeeded(); adapter.registerDependenciesEnforcerHookOn(task, enforcer); if ( adapter.isContainer(task) ) { for (V child : adapter.getChildren(task)) { fromChildToParent.put(child, task); stack.add(0, child); dependenciesToAdd.add(adapter.createInvisibleDependency(child, task, DependencyType.END_END)); dependenciesToAdd.add(adapter.createInvisibleDependency(task, child, DependencyType.START_START)); } } else { V owner = adapter.getOwner(task); if( owner != null ) { dependenciesToAdd.add(adapter.createInvisibleDependency(task, owner, DependencyType.END_END)); dependenciesToAdd.add(adapter.createInvisibleDependency(owner, task, DependencyType.START_START)); } } } for (D each : dependenciesToAdd) { add(each, false); } } private interface IDependenciesEnforcer { void setStartDate(GanttDate previousStart, GanttDate previousEnd, GanttDate newStart); void setNewEnd(GanttDate previousEnd, GanttDate newEnd); } /** * <p> * When a {@link Task}'s dates are modified methods of this interface must * be called. This can potentially trigger the dependencies enforcement * algorithm. * </p> * <p> * If the date modification happens outside the dependencies enforcement * algorithm, it's always executed. Through the algorithm execution other * tasks' dates are modified. When this happens we don't want to trigger the * algorithm, instead we want to record that the change has happened and * when the algorithm ends all the tasks are notified at once. * </p> * <p> * For example imagine a Gantt with three tasks: T1 -> T2 -> T3. Imagine * that T1 position is modified due to being moved by the user. In that case * the scheduling algorithm triggers and the {@link Recalculation} * recalculations needed are done. T2 position would be recalculated and T3 * position too. When the recalculation happens their dates are modified, * but in that case we don't want to trigger the dependencies enforcement * algorithm again. What we want is to record the changes that have happened * due to the algorithm. When the algorithm ends all notifications are fired * at once. These notifications are notified to the registered * {@link INotificationAfterDependenciesEnforcement}. * </p> */ public interface IDependenciesEnforcerHook extends IDependenciesEnforcer { void positionPotentiallyModified(); } public interface IDependenciesEnforcerHookFactory<T> { /** * Creates a {@link IDependenciesEnforcerHook} that uses the provided * {@link INotificationAfterDependenciesEnforcement notifier} to notify * the changes that have happened due to the algorithm. */ IDependenciesEnforcerHook create(T task, INotificationAfterDependenciesEnforcement notifier); IDependenciesEnforcerHook create(T task); } public interface INotificationAfterDependenciesEnforcement { void onStartDateChange(GanttDate previousStart, GanttDate previousEnd, GanttDate newStart); void onEndDateChange(GanttDate previousEnd, GanttDate newEnd); } private static final INotificationAfterDependenciesEnforcement EMPTY_NOTIFICATOR = new INotificationAfterDependenciesEnforcement() { @Override public void onStartDateChange(GanttDate previousStart, GanttDate previousEnd, GanttDate newStart) { } @Override public void onEndDateChange(GanttDate previousEnd, GanttDate newEnd) { } }; /** * Tracks all modifications to dates that have happened inside the * dependencies enforcement algorithm. At the end of the algorithm they're * executed via {@link DeferedNotifier#doNotifications()}. */ public class DeferedNotifier { private Map<V, NotificationPendingForTask> notificationsPending = new LinkedHashMap<>(); public void add(V task, StartDateNofitication notification) { retrieveOrCreateFor(task).setStartDateNofitication(notification); } private NotificationPendingForTask retrieveOrCreateFor(V task) { NotificationPendingForTask result = notificationsPending.get(task); if ( result == null ) { result = new NotificationPendingForTask(); notificationsPending.put(task, result); } return result; } void add(V task, LengthNotification notification) { retrieveOrCreateFor(task).setLengthNofitication(notification); } public void doNotifications() { for (NotificationPendingForTask each : notificationsPending.values()) { each.doNotification(); } notificationsPending.clear(); } } private class NotificationPendingForTask { private StartDateNofitication startDateNofitication; private LengthNotification lengthNofitication; void setStartDateNofitication(StartDateNofitication startDateNofitication) { this.startDateNofitication = (this.startDateNofitication == null) ? startDateNofitication : this.startDateNofitication.coalesce(startDateNofitication); } void setLengthNofitication(LengthNotification lengthNofitication) { this.lengthNofitication = (this.lengthNofitication == null) ? lengthNofitication : this.lengthNofitication.coalesce(lengthNofitication); } void doNotification() { if ( startDateNofitication != null ) { startDateNofitication.doNotification(); } if ( lengthNofitication != null ) { lengthNofitication.doNotification(); } } } private class StartDateNofitication { private final INotificationAfterDependenciesEnforcement notification; private final GanttDate previousStart; private final GanttDate previousEnd; private final GanttDate newStart; public StartDateNofitication(INotificationAfterDependenciesEnforcement notification, GanttDate previousStart, GanttDate previousEnd, GanttDate newStart) { this.notification = notification; this.previousStart = previousStart; this.previousEnd = previousEnd; this.newStart = newStart; } public StartDateNofitication coalesce(StartDateNofitication startDateNofitication) { return new StartDateNofitication(notification, previousStart, previousEnd, startDateNofitication.newStart); } void doNotification() { notification.onStartDateChange(previousStart, previousEnd, newStart); } } private class LengthNotification { private final INotificationAfterDependenciesEnforcement notification; private final GanttDate previousEnd; private final GanttDate newEnd; public LengthNotification(INotificationAfterDependenciesEnforcement notification, GanttDate previousEnd, GanttDate newEnd) { this.notification = notification; this.previousEnd = previousEnd; this.newEnd = newEnd; } public LengthNotification coalesce(LengthNotification lengthNofitication) { return new LengthNotification(notification, previousEnd, lengthNofitication.newEnd); } void doNotification() { notification.onEndDateChange(previousEnd, newEnd); } } private class DependenciesEnforcer implements IDependenciesEnforcerHookFactory<V> { private ThreadLocal<DeferedNotifier> deferedNotifier = new ThreadLocal<>(); /** * It creates a {@link IDependenciesEnforcerHook} that starts the * algorithm <em>onEntrance</em> and in subsequent tasks position * modifications records the changes <em>onNotification</em>. */ @Override public IDependenciesEnforcerHook create(V task, INotificationAfterDependenciesEnforcement notificator) { return withPositionPotentiallyModified( task, onlyEnforceDependenciesOnEntrance(onEntrance(task), onNotification(task, notificator))); } @Override public IDependenciesEnforcerHook create(V task) { return create(task, EMPTY_NOTIFICATOR); } /** * What to do when a task's position is modified not inside the * dependencies enforcement algorithm. */ private IDependenciesEnforcer onEntrance(final V task) { return new IDependenciesEnforcer() { public void setStartDate(GanttDate previousStart, GanttDate previousEnd, GanttDate newStart) { taskPositionModified(task); } @Override public void setNewEnd(GanttDate previousEnd, GanttDate newEnd) { taskPositionModified(task); } }; } /** * What to do when a task's position is modified from inside the * dependencies enforcement algorithm. */ private IDependenciesEnforcer onNotification(final V task, final INotificationAfterDependenciesEnforcement notification) { return new IDependenciesEnforcer() { @Override public void setStartDate(GanttDate previousStart, GanttDate previousEnd, GanttDate newStart) { StartDateNofitication startDateNotification = new StartDateNofitication(notification, previousStart, previousEnd, newStart); deferedNotifier.get().add(task, startDateNotification); } @Override public void setNewEnd(GanttDate previousEnd, GanttDate newEnd) { LengthNotification lengthNotification = new LengthNotification(notification, previousEnd, newEnd); deferedNotifier.get().add(task, lengthNotification); } }; } /** * Enrich {@link IDependenciesEnforcer} with * {@link IDependenciesEnforcerHook#positionPotentiallyModified()}. */ private IDependenciesEnforcerHook withPositionPotentiallyModified(final V task, final IDependenciesEnforcer enforcer) { return new IDependenciesEnforcerHook() { @Override public void setStartDate(GanttDate previousStart, GanttDate previousEnd, GanttDate newStart) { enforcer.setStartDate(previousStart, previousEnd, newStart); } @Override public void setNewEnd(GanttDate previousEnd, GanttDate newEnd) { enforcer.setNewEnd(previousEnd, newEnd); } @Override public void positionPotentiallyModified() { taskPositionModified(task); } }; } /** * Creates a {@link IDependenciesEnforcer} that detects if a position * change comes from the dependencies algorithm or comes from outside. * For that a {@link ReentranceGuard} is used. If the dependencies * enforcement algorithm isn't being executed the * {@link IDependenciesEnforcer} created delegates to * <code>onEntrance</code>. Otherwise it delegates to * <code>notifier</code>. */ private IDependenciesEnforcer onlyEnforceDependenciesOnEntrance(final IDependenciesEnforcer onEntrance, final IDependenciesEnforcer notifier) { return new IDependenciesEnforcer() { @Override public void setStartDate(final GanttDate previousStart, final GanttDate previousEnd, final GanttDate newStart) { positionsUpdatingGuard.entranceRequested(new IReentranceCases() { @Override public void ifNewEntrance() { onNewEntrance(new IAction() { @Override public void doAction() { notifier.setStartDate(previousStart, previousEnd, newStart); onEntrance.setStartDate(previousStart, previousEnd, newStart); } }); } @Override public void ifAlreadyInside() { notifier.setStartDate(previousStart, previousEnd, newStart); } }); } @Override public void setNewEnd(final GanttDate previousEnd, final GanttDate newEnd) { positionsUpdatingGuard.entranceRequested(new IReentranceCases() { @Override public void ifNewEntrance() { onNewEntrance(new IAction() { @Override public void doAction() { notifier.setNewEnd(previousEnd, newEnd); onEntrance.setNewEnd(previousEnd, newEnd); } }); } @Override public void ifAlreadyInside() { notifier.setNewEnd(previousEnd, newEnd); } }); } }; } void enforceRestrictionsOn(Collection<? extends V> tasks) { List<Recalculation> allRecalculations = new ArrayList<>(); for (V each : tasks) { allRecalculations.addAll(getRecalculationsNeededFrom(each)); } enforceRestrictionsOn(allRecalculations, tasks); } void enforceRestrictionsOn(V task) { enforceRestrictionsOn(getRecalculationsNeededFrom(task), Collections.singleton(task)); } void enforceRestrictionsOn(final List<Recalculation> recalculations, final Collection<? extends V> initiallyModified) { executeWithPreAndPostActionsOnlyIfNewEntrance(new IAction() { @Override public void doAction() { doRecalculations(recalculations, initiallyModified); } }); } private void executeWithPreAndPostActionsOnlyIfNewEntrance(final IAction action) { positionsUpdatingGuard.entranceRequested(new IReentranceCases() { @Override public void ifAlreadyInside() { action.doAction(); } @Override public void ifNewEntrance() { onNewEntrance(action); } }); } /** * When entering and exiting the dependencies enforcement algorithm some * listeners must be notified. */ private void onNewEntrance(final IAction action) { preAndPostActions.doAction(decorateWithNotifications(action)); } /** * Attach the {@link DeferedNotifier} for the current execution. * {@link DeferedNotifier#doNotifications()} is called once the * execution has finished, telling all listeners the task positions * modifications that have happened. */ private IAction decorateWithNotifications(final IAction action) { return new IAction() { @Override public void doAction() { deferedNotifier.set(new DeferedNotifier()); try { action.doAction(); } finally { DeferedNotifier notifier = deferedNotifier.get(); notifier.doNotifications(); deferedNotifier.set(null); } } }; } DeferedNotifier manualNotification(final IAction action) { final DeferedNotifier result = new DeferedNotifier(); positionsUpdatingGuard.entranceRequested(new IReentranceCases() { @Override public void ifAlreadyInside() { throw new RuntimeException("it cannot do a manual notification if it's already inside"); } @Override public void ifNewEntrance() { preAndPostActions.doAction(new IAction() { @Override public void doAction() { deferedNotifier.set(result); try { action.doAction(); } finally { deferedNotifier.set(null); } } }); } }); return result; } private void taskPositionModified(final V task) { executeWithPreAndPostActionsOnlyIfNewEntrance(new IAction() { @Override public void doAction() { List<Recalculation> recalculationsNeededFrom = getRecalculationsNeededFrom(task); doRecalculations(recalculationsNeededFrom, Collections.singletonList(task)); } }); } private void doRecalculations(List<Recalculation> recalculationsNeeded, Collection<? extends V> initiallyModified) { Set<V> allModified = new HashSet<>(); allModified.addAll(initiallyModified); for (Recalculation each : recalculationsNeeded) { boolean modified = each.doRecalculation(); if ( modified ) { allModified.add(each.taskPoint.task); } } List<V> shrunkContainers = shrunkContainersOfModified(allModified); for (V each : getTaskAffectedByShrinking(shrunkContainers)) { doRecalculations(getRecalculationsNeededFrom(each), Collections.singletonList(each)); } } private List<V> getTaskAffectedByShrinking(List<V> shrunkContainers) { List<V> tasksAffectedByShrinking = new ArrayList<>(); for (V each : shrunkContainers) { for (D eachDependency : graph.outgoingEdgesOf(each)) { boolean condition = adapter.getType(eachDependency) == DependencyType.START_START && adapter.isVisible(eachDependency); if ( condition ) { tasksAffectedByShrinking.add(adapter.getDestination(eachDependency)); } } } return tasksAffectedByShrinking; } private List<V> shrunkContainersOfModified(Set<V> allModified) { Set<V> topmostToShrink = getTopMostThatCouldPotentiallyNeedShrinking(allModified); List<V> allToShrink = new ArrayList<>(); for (V each : topmostToShrink) { allToShrink.addAll(getContainersBottomUp(each)); } List<V> result = new ArrayList<>(); for (V each : allToShrink) { boolean modified = enforceParentShrinkage(each); if ( modified ) { result.add(each); } } return result; } private Set<V> getTopMostThatCouldPotentiallyNeedShrinking(Collection<V> modified) { Set<V> result = new HashSet<>(); for (V each : modified) { V t = getTopmostFor(each); if ( adapter.isContainer(t) ) { result.add(t); } } return result; } private Collection<? extends V> getContainersBottomUp(V container) { List<V> result = new ArrayList<V>(); List<V> tasks = adapter.getChildren(container); for (V each : tasks) { if ( adapter.isContainer(each) ) { result.addAll(getContainersBottomUp(each)); result.add(each); } } result.add(container); return result; } boolean enforceParentShrinkage(V container) { GanttDate oldBeginDate = adapter.getStartDate(container); GanttDate firstStart = getSmallestBeginDateFromChildrenFor(container); GanttDate lastEnd = getBiggestEndDateFromChildrenFor(container); GanttDate previousEnd = adapter.getEndDateFor(container); if ( firstStart.after(oldBeginDate) || previousEnd.after(lastEnd) ) { adapter.setStartDateFor(container, GanttDate.max(firstStart, oldBeginDate)); adapter.setEndDateFor(container, GanttDate.min(lastEnd, previousEnd)); return true; } return false; } } private GanttDate getSmallestBeginDateFromChildrenFor(V container) { return Collections.min(getChildrenDates(container, Point.START)); } private GanttDate getBiggestEndDateFromChildrenFor(V container) { return Collections.max(getChildrenDates(container, Point.END)); } private List<GanttDate> getChildrenDates(V container, Point point) { List<V> children = adapter.getChildren(container); List<GanttDate> result = new ArrayList<>(); if ( children.isEmpty() ) { result.add(getDateFor(container, point)); } for (V each : children) { result.add(getDateFor(each, point)); } return result; } GanttDate getDateFor(V task, Point point) { if ( point.equals(Point.START) ) { return adapter.getStartDate(task); } else { return adapter.getEndDateFor(task); } } List<Recalculation> getRecalculationsNeededFrom(V task) { List<Recalculation> result = new ArrayList<>(); Set<Recalculation> parentRecalculationsAlreadyDone = new HashSet<>(); Recalculation first = recalculationFor(allPointsPotentiallyModified(task)); first.couldHaveBeenModifiedBeforehand(); result.addAll(getParentsRecalculations(parentRecalculationsAlreadyDone, first.taskPoint)); result.add(first); Queue<Recalculation> pendingOfVisit = new LinkedList<>(); pendingOfVisit.offer(first); Map<Recalculation, Recalculation> alreadyVisited = new HashMap<>(); alreadyVisited.put(first, first); while (!pendingOfVisit.isEmpty()) { Recalculation current = pendingOfVisit.poll(); for (TaskPoint each : current.taskPoint.getImmediateSuccessors()) { if ( each.isImmediatelyDerivedFrom(current.taskPoint) ) { continue; } Recalculation recalculationToAdd = getRecalcualtionToAdd(each, alreadyVisited); recalculationToAdd.comesFromPredecessor(current); if ( !alreadyVisited.containsKey(recalculationToAdd) ) { result.addAll(getParentsRecalculations(parentRecalculationsAlreadyDone, each)); result.add(recalculationToAdd); pendingOfVisit.offer(recalculationToAdd); alreadyVisited.put(recalculationToAdd, recalculationToAdd); } } } return topologicalSorter.sort(result); } private Recalculation getRecalcualtionToAdd(TaskPoint taskPoint, Map<Recalculation, Recalculation> alreadyVisited) { Recalculation result = recalculationFor(taskPoint); if ( alreadyVisited.containsKey(result) ) { return alreadyVisited.get(result); } else { return result; } } private List<Recalculation> getParentsRecalculations(Set<Recalculation> parentRecalculationsAlreadyDone, TaskPoint taskPoint) { List<Recalculation> result = new ArrayList<>(); for (TaskPoint eachParent : parentsRecalculationsNeededFor(taskPoint)) { Recalculation parentRecalculation = parentRecalculation(eachParent.task); if ( !parentRecalculationsAlreadyDone.contains(parentRecalculation) ) { parentRecalculationsAlreadyDone.add(parentRecalculation); result.add(parentRecalculation); } } return result; } private Set<TaskPoint> parentsRecalculationsNeededFor(TaskPoint current) { Set<TaskPoint> result = new LinkedHashSet<>(); if ( current.areAllPointsPotentiallyModified() ) { List<V> path = fromTaskToTop(current.task); if ( path.size() > 1 ) { path = path.subList(1, path.size()); Collections.reverse(path); result.addAll(asBothPoints(path)); } } return result; } private Collection<? extends TaskPoint> asBothPoints(List<V> parents) { List<TaskPoint> result = new ArrayList<>(); for (V each : parents) { result.add(allPointsPotentiallyModified(each)); } return result; } private List<V> fromTaskToTop(V task) { List<V> result = new ArrayList<>(); V current = task; while ( current != null ) { result.add(current); current = fromChildToParent.get(current); } return result; } private Recalculation parentRecalculation(V task) { return new Recalculation(allPointsPotentiallyModified(task), true); } private Recalculation recalculationFor(TaskPoint taskPoint) { return new Recalculation(taskPoint, false); } private class Recalculation { private final boolean parentRecalculation; private final TaskPoint taskPoint; private Set<Recalculation> recalculationsCouldAffectThis = new HashSet<>(); private boolean recalculationCalled = false; private boolean dataPointModified = false; private boolean couldHaveBeenModifiedBeforehand = false; Recalculation(TaskPoint taskPoint, boolean isParentRecalculation) { Validate.notNull(taskPoint); this.taskPoint = taskPoint; this.parentRecalculation = isParentRecalculation; } public void couldHaveBeenModifiedBeforehand() { couldHaveBeenModifiedBeforehand = true; } public void comesFromPredecessor(Recalculation predecessor) { recalculationsCouldAffectThis.add(predecessor); } boolean doRecalculation() { recalculationCalled = true; dataPointModified = haveToDoCalculation() && taskChangesPosition(); return dataPointModified; } private boolean haveToDoCalculation() { return recalculationsCouldAffectThis.isEmpty() || predecessorsHaveBeenModified(); } private boolean predecessorsHaveBeenModified() { for (Recalculation each : recalculationsCouldAffectThis) { if ( !each.recalculationCalled ) { // FIXME this need to be not commented, but some imported projects not working with it //throw new RuntimeException("the predecessor must be called first"); } if ( each.dataPointModified || each.couldHaveBeenModifiedBeforehand ) { return true; } } return false; } private boolean taskChangesPosition() { ChangeTracker tracker = trackTaskChanges(); Constraint.initialValue(noRestrictions()).withConstraints(getConstraintsToApply()).apply(); return tracker.taskHasChanged(); } @SuppressWarnings("unchecked") private List<Constraint<PositionRestrictions>> getConstraintsToApply() { Constraint<PositionRestrictions> weakForces = scheduleBackwards ? new WeakForwardForces() : new WeakBackwardsForces(); Constraint<PositionRestrictions> dominatingForces = scheduleBackwards ? new DominatingBackwardForces() : new DominatingForwardForces(); if ( dependenciesConstraintsHavePriority ) { return asList(weakForces, dominatingForces); } else { return asList(weakForces, dominatingForces, weakForces); } } abstract class PositionRestrictions { private final GanttDate start; private final GanttDate end; PositionRestrictions(GanttDate start, GanttDate end) { this.start = start; this.end = end; } GanttDate getStart() { return start; } GanttDate getEnd() { return end; } abstract List<Constraint<GanttDate>> getStartConstraints(); abstract List<Constraint<GanttDate>> getEndConstraints(); abstract boolean satisfies(PositionRestrictions other); } private final class NoRestrictions extends PositionRestrictions { public NoRestrictions(TaskPoint taskPoint) { super(adapter.getStartDate(taskPoint.task), adapter.getEndDateFor(taskPoint.task)); } @Override List<Constraint<GanttDate>> getStartConstraints() { return Collections.emptyList(); } @Override List<Constraint<GanttDate>> getEndConstraints() { return Collections.emptyList(); } @Override boolean satisfies(PositionRestrictions restrictions) { return true; } } PositionRestrictions noRestrictions() { return new NoRestrictions(taskPoint); } DatesBasedPositionRestrictions biggerThan(GanttDate start, GanttDate end) { ComparisonType type = isScheduleForward() ? ComparisonType.BIGGER_OR_EQUAL_THAN : ComparisonType.BIGGER_OR_EQUAL_THAN_LEFT_FLOATING; return new DatesBasedPositionRestrictions(type, start, end); } DatesBasedPositionRestrictions lessThan(GanttDate start, GanttDate end) { ComparisonType type = isScheduleForward() ? ComparisonType.LESS_OR_EQUAL_THAN_RIGHT_FLOATING : ComparisonType.LESS_OR_EQUAL_THAN; return new DatesBasedPositionRestrictions(type, start, end); } class DatesBasedPositionRestrictions extends PositionRestrictions { private Constraint<GanttDate> startConstraint; private Constraint<GanttDate> endConstraint; public DatesBasedPositionRestrictions(ComparisonType comparisonType, GanttDate start, GanttDate end) { super(start, end); this.startConstraint = ConstraintOnComparableValues.instantiate(comparisonType, start); this.endConstraint = ConstraintOnComparableValues.instantiate(comparisonType, end); } boolean satisfies(PositionRestrictions other) { if ( DatesBasedPositionRestrictions.class.isInstance(other) ) { return satisfies(DatesBasedPositionRestrictions.class.cast(other)); } return false; } private boolean satisfies(DatesBasedPositionRestrictions other) { return startConstraint.isSatisfiedBy(other.getStart()) && endConstraint.isSatisfiedBy(other.getEnd()); } @Override List<Constraint<GanttDate>> getStartConstraints() { return Collections.singletonList(startConstraint); } @Override List<Constraint<GanttDate>> getEndConstraints() { return Collections.singletonList(endConstraint); } } class ChangeTracker { private GanttDate start; private GanttDate end; private final V task; public ChangeTracker(V task) { this.task = task; this.start = adapter.getStartDate(task); this.end = adapter.getEndDateFor(task); } public boolean taskHasChanged() { return areNotEqual(adapter.getStartDate(task), this.start) || areNotEqual(adapter.getEndDateFor(task), this.end); } } boolean areNotEqual(GanttDate a, GanttDate b) { return a != b && a.compareTo(b) != 0; } protected ChangeTracker trackTaskChanges() { return new ChangeTracker(taskPoint.task); } abstract class Forces extends Constraint<PositionRestrictions> { protected final V task; public Forces() { this.task = taskPoint.task; } private PositionRestrictions resultingRestrictions = noRestrictions(); protected PositionRestrictions applyConstraintTo(PositionRestrictions restrictions) { if ( adapter.isFixed(task) ) { return restrictions; } resultingRestrictions = enforceUsingPreviousRestrictions(restrictions); return resultingRestrictions; } public boolean isSatisfiedBy(PositionRestrictions value) { return resultingRestrictions.satisfies(value); } public void checkSatisfiesResult(PositionRestrictions finalResult) { super.checkSatisfiesResult(finalResult); checkStartConstraints(finalResult.getStart()); checkEndConstraints(finalResult.getEnd()); } private void checkStartConstraints(GanttDate finalStart) { Constraint.checkSatisfyResult(getStartConstraints(), finalStart); } private void checkEndConstraints(GanttDate finalEnd) { Constraint.checkSatisfyResult(getEndConstraints(), finalEnd); } abstract List<Constraint<GanttDate>> getStartConstraints(); abstract List<Constraint<GanttDate>> getEndConstraints(); abstract PositionRestrictions enforceUsingPreviousRestrictions(PositionRestrictions restrictions); } abstract class Dominating extends Forces { private final Point primary; private final Point secondary; public Dominating(Point primary, Point secondary) { Validate.isTrue(isSupportedPoint(primary)); Validate.isTrue(isSupportedPoint(secondary)); Validate.isTrue(!primary.equals(secondary)); this.primary = primary; this.secondary = secondary; } private boolean isSupportedPoint(Point point) { EnumSet<Point> validPoints = EnumSet.of(Point.START, Point.END); return validPoints.contains(point); } private Point getPrimaryPoint() { return primary; } private Point getSecondaryPoint() { return secondary; } @Override PositionRestrictions enforceUsingPreviousRestrictions(PositionRestrictions restrictions) { if ( parentRecalculation ) { // avoid interference from task containers shrinking return enforcePrimaryPoint(restrictions); } else if ( taskPoint.areAllPointsPotentiallyModified() ) { return enforceBoth(restrictions); } else if ( taskPoint.somePointPotentiallyModified() ) { return enforceSecondaryPoint(restrictions); } return restrictions; } private PositionRestrictions enforceBoth(PositionRestrictions restrictions) { ChangeTracker changeTracker = trackTaskChanges(); PositionRestrictions currentRestrictions = enforcePrimaryPoint(restrictions); if ( changeTracker.taskHasChanged() || parentRecalculation || couldHaveBeenModifiedBeforehand ) { return enforceSecondaryPoint(currentRestrictions); } return currentRestrictions; } private PositionRestrictions enforcePrimaryPoint(PositionRestrictions originalRestrictions) { GanttDate newDominatingPointDate = calculatePrimaryPointDate(originalRestrictions); return enforceRestrictionsFor(primary, newDominatingPointDate); } /** * Calculates the new date for the primary point based on the * present constraints. If there are no constraints this method will * return the existent commanding point date * @param originalRestrictions */ private GanttDate calculatePrimaryPointDate(PositionRestrictions originalRestrictions) { GanttDate newDate = Constraint.<GanttDate> initialValue(null) .withConstraints(getConstraintsFrom(originalRestrictions, getPrimaryPoint())) .withConstraints(getConstraintsFor(getPrimaryPoint())) .applyWithoutFinalCheck(); if ( newDate == null ) { return getTaskDateFor(getPrimaryPoint()); } return newDate; } private List<Constraint<GanttDate>> getConstraintsFor(Point point) { Validate.isTrue(isSupportedPoint(point)); switch (point) { case START: return getStartConstraints(); case END: return getEndConstraints(); default: throw new RuntimeException("shouldn't happen"); } } private PositionRestrictions enforceSecondaryPoint(PositionRestrictions restrictions) { GanttDate newSecondaryPointDate = calculateSecondaryPointDate(restrictions); if ( newSecondaryPointDate == null ) { return restrictions; } restrictions = enforceRestrictionsFor(getSecondaryPoint(), newSecondaryPointDate); if ( taskPoint.onlyModifies(getSecondaryPoint()) ) { // primary point constraints could be the ones "commanding" now GanttDate potentialPrimaryDate = calculatePrimaryPointDate(restrictions); if ( !doSatisfyOrderCondition(potentialPrimaryDate, getTaskDateFor(getPrimaryPoint())) ) { return enforceRestrictionsFor(getPrimaryPoint(), potentialPrimaryDate); } } return restrictions; } private GanttDate calculateSecondaryPointDate(PositionRestrictions restrictions) { GanttDate newEnd = Constraint.<GanttDate> initialValue(null) .withConstraints(getConstraintsFrom(restrictions, getSecondaryPoint())) .withConstraints(getConstraintsFor(getSecondaryPoint())) .applyWithoutFinalCheck(); return newEnd; } protected abstract boolean doSatisfyOrderCondition(GanttDate supposedlyBefore, GanttDate supposedlyAfter); private PositionRestrictions enforceRestrictionsFor(Point point, GanttDate newDate) { GanttDate old = getTaskDateFor(point); if ( areNotEqual(old, newDate) ) { setTaskDateFor(point, newDate); } return createRestrictionsFor(getTaskDateFor(Point.START), getTaskDateFor(Point.END)); } GanttDate getTaskDateFor(Point point) { Validate.isTrue(isSupportedPoint(point)); return getDateFor(task, point); } protected abstract PositionRestrictions createRestrictionsFor(GanttDate start, GanttDate end); private void setTaskDateFor(Point point, GanttDate date) { Validate.isTrue(isSupportedPoint(point)); switch (point) { case START: adapter.setStartDateFor(task, date); break; case END: adapter.setEndDateFor(task, date); } } private List<Constraint<GanttDate>> getConstraintsFrom(PositionRestrictions restrictions, Point point) { Validate.isTrue(isSupportedPoint(point)); switch (point) { case START: return restrictions.getStartConstraints(); case END: return restrictions.getEndConstraints(); default: throw new RuntimeException("shouldn't happen"); } } protected List<Constraint<GanttDate>> getConstraintsForPrimaryPoint() { List<Constraint<GanttDate>> result = new ArrayList<>(); if ( dependenciesConstraintsHavePriority ) { result.addAll(getTaskConstraints(getPrimaryPoint())); result.addAll(getDependenciesConstraintsFor(getPrimaryPoint())); } else { result.addAll(getDependenciesConstraintsFor(getPrimaryPoint())); result.addAll(getTaskConstraints(getPrimaryPoint())); } result.addAll(getGlobalConstraintsToApply(getPrimaryPoint())); return result; } private Collection<Constraint<GanttDate>> getGlobalConstraintsToApply(Point point) { Validate.isTrue(isSupportedPoint(point)); switch (point) { case START: return globalStartConstraints; case END: return globalEndConstraints; default: throw new RuntimeException("shouldn't happen"); } } protected List<Constraint<GanttDate>> getConstraintsForSecondaryPoint() { return getDependenciesConstraintsFor(getSecondaryPoint()); } private List<Constraint<GanttDate>> getDependenciesConstraintsFor(Point point) { final Set<D> withDependencies = getDependenciesAffectingThisTask(); return adapter.getConstraints(getCalculator(), withDependencies, point); } protected abstract Set<D> getDependenciesAffectingThisTask(); private List<Constraint<GanttDate>> getTaskConstraints(Point point) { Validate.isTrue(isSupportedPoint(point)); switch (point) { case START: return adapter.getStartConstraintsFor(task); case END: return adapter.getEndConstraintsFor(task); default: throw new RuntimeException("shouldn't happen"); } } protected abstract ConstraintCalculator<V> getCalculator(); protected ConstraintCalculator<V> createNormalCalculator() { return createCalculator(false); } protected ConstraintCalculator<V> createBackwardsCalculator() { return createCalculator(true); } private ConstraintCalculator<V> createCalculator(boolean inverse) { return new ConstraintCalculator<V>(inverse) { @Override protected GanttDate getStartDate(V vertex) { return adapter.getStartDate(vertex); } @Override protected GanttDate getEndDate(V vertex) { return adapter.getEndDateFor(vertex); } }; } } class DominatingForwardForces extends Dominating { public DominatingForwardForces() { super(Point.START, Point.END); } @Override List<Constraint<GanttDate>> getStartConstraints() { return getConstraintsForPrimaryPoint(); } @Override List<Constraint<GanttDate>> getEndConstraints() { return getConstraintsForSecondaryPoint(); } @Override protected Set<D> getDependenciesAffectingThisTask() { return graph.incomingEdgesOf(task); } @Override protected ConstraintCalculator<V> getCalculator() { return createNormalCalculator(); } @Override protected PositionRestrictions createRestrictionsFor(GanttDate start, GanttDate end) { return biggerThan(start, end); } @Override protected boolean doSatisfyOrderCondition(GanttDate supposedlyBefore, GanttDate supposedlyAfter) { return supposedlyBefore.compareTo(supposedlyAfter) <= 0; } } class DominatingBackwardForces extends Dominating { public DominatingBackwardForces() { super(Point.END, Point.START); } @Override List<Constraint<GanttDate>> getStartConstraints() { return getConstraintsForSecondaryPoint(); } @Override List<Constraint<GanttDate>> getEndConstraints() { return getConstraintsForPrimaryPoint(); } @Override protected Set<D> getDependenciesAffectingThisTask() { return graph.outgoingEdgesOf(task); } @Override protected ConstraintCalculator<V> getCalculator() { return createBackwardsCalculator(); } @Override protected PositionRestrictions createRestrictionsFor(GanttDate start, GanttDate end) { return lessThan(start, end); } @Override protected boolean doSatisfyOrderCondition(GanttDate supposedlyBefore, GanttDate supposedlyAfter) { return supposedlyBefore.compareTo(supposedlyAfter) >= 0; } } class WeakForwardForces extends Forces { @Override List<Constraint<GanttDate>> getStartConstraints() { return adapter.getStartConstraintsFor(task); } @Override List<Constraint<GanttDate>> getEndConstraints() { return Collections.emptyList(); } @Override PositionRestrictions enforceUsingPreviousRestrictions(PositionRestrictions restrictions) { GanttDate result = Constraint.<GanttDate> initialValue(null) .withConstraints(restrictions.getStartConstraints()) .withConstraints(getStartConstraints()) .applyWithoutFinalCheck(); if ( result != null ) { enforceRestrictions(result); return biggerThan(result, adapter.getEndDateFor(task)); } return restrictions; } private void enforceRestrictions(GanttDate result) { if ( !result.equals(getStartDate(task)) ) { adapter.setStartDateFor(task, result); } } } class WeakBackwardsForces extends Forces { @Override PositionRestrictions enforceUsingPreviousRestrictions(PositionRestrictions restrictions) { GanttDate result = Constraint.<GanttDate> initialValue(null) .withConstraints(restrictions.getEndConstraints()) .withConstraints(getEndConstraints()) .applyWithoutFinalCheck(); if ( result != null ) { enforceRestrictions(result); return lessThan(adapter.getStartDate(task), result); } return restrictions; } @Override List<Constraint<GanttDate>> getStartConstraints() { return Collections.emptyList(); } @Override List<Constraint<GanttDate>> getEndConstraints() { return adapter.getEndConstraintsFor(task); } private void enforceRestrictions(GanttDate newEnd) { if ( !newEnd.equals(getEndDateFor(task)) ) { adapter.setEndDateFor(task, newEnd); } } } @Override public int hashCode() { return new HashCodeBuilder() .append(parentRecalculation) .append(taskPoint) .toHashCode(); } @Override public String toString() { return String.format( "%s, parentRecalculation: %s, predecessors: %s", taskPoint, parentRecalculation, asSimpleString(recalculationsCouldAffectThis)); } private String asSimpleString(Collection<? extends Recalculation> recalculations) { StringBuilder result = new StringBuilder(); result.append("["); for (Recalculation each : recalculations) { result.append(each.taskPoint).append(", "); } result.append("]"); return result.toString(); } @Override public boolean equals(Object obj) { if ( Recalculation.class.isInstance(obj) ) { Recalculation other = Recalculation.class.cast(obj); return new EqualsBuilder().append(parentRecalculation, other.parentRecalculation) .append(taskPoint, other.taskPoint) .isEquals(); } return false; } } public void remove(final V task) { Set<V> needingEnforcing = getOutgoingTasksFor(task); graph.removeVertex(task); topLevelTasks.remove(task); fromChildToParent.remove(task); if ( adapter.isContainer(task) ) { for (V t : adapter.getChildren(task)) { remove(t); } } topologicalSorter.recalculationNeeded(); enforcer.enforceRestrictionsOn(needingEnforcing); } public void removeDependency(D dependency) { graph.removeEdge(dependency); topologicalSorter.recalculationNeeded(); V destination = adapter.getDestination(dependency); V source = adapter.getSource(dependency); enforcer.enforceRestrictionsOn(destination); enforcer.enforceRestrictionsOn(source); } public boolean canAddDependency(D dependency) { return !isForbidden(dependency) && doesNotProvokeLoop(dependency); } private boolean isForbidden(D dependency) { if ( !adapter.isVisible(dependency) ) { // the invisible dependencies, the ones used to implement container behavior are not forbidden return false; } boolean endEndDependency = DependencyType.END_END == dependency.getType(); boolean startStartDependency = DependencyType.START_START == dependency.getType(); V source = adapter.getSource(dependency); V destination = adapter.getDestination(dependency); boolean destinationIsContainer = adapter.isContainer(destination); boolean sourceIsContainer = adapter.isContainer(source); return (destinationIsContainer && endEndDependency) || (sourceIsContainer && startStartDependency); } public void add(D dependency) { add(dependency, true); } public void addWithoutEnforcingConstraints(D dependency) { add(dependency, false); } private void add(D dependency, boolean enforceRestrictions) { if ( isForbidden(dependency) ) { return; } V source = adapter.getSource(dependency); V destination = adapter.getDestination(dependency); graph.addEdge(source, destination, dependency); topologicalSorter.recalculationNeeded(); if ( enforceRestrictions ) { enforceRestrictions(destination); } } public void enforceRestrictions(final V task) { enforcer.taskPositionModified(task); } public DeferedNotifier manualNotificationOn(IAction action) { return enforcer.manualNotification(action); } public boolean contains(D dependency) { return graph.containsEdge(dependency); } public List<V> getTasks() { return new ArrayList<>(graph.vertexSet()); } public List<D> getVisibleDependencies() { ArrayList<D> result = new ArrayList<>(); for (D dependency : graph.edgeSet()) { if ( adapter.isVisible(dependency) ) { result.add(dependency); } } return result; } public List<V> getTopLevelTasks() { return Collections.unmodifiableList(topLevelTasks); } public void childrenAddedTo(V task) { enforcer.enforceRestrictionsOn(task); } public List<V> getInitialTasks() { List<V> result = new ArrayList<>(); for (V task : graph.vertexSet()) { int dependencies = graph.inDegreeOf(task); if ( (dependencies == 0) || (dependencies == getNumberOfIncomingDependenciesByType(task, DependencyType.END_END)) ) { result.add(task); } } return result; } public IDependency<V> getDependencyFrom(V from, V to) { return graph.getEdge(from, to); } public Set<V> getOutgoingTasksFor(V task) { Set<V> result = new HashSet<>(); for (D dependency : graph.outgoingEdgesOf(task)) { result.add(adapter.getDestination(dependency)); } return result; } public Set<V> getIncomingTasksFor(V task) { Set<V> result = new HashSet<>(); for (D dependency : graph.incomingEdgesOf(task)) { result.add(adapter.getSource(dependency)); } return result; } public boolean hasVisibleIncomingDependencies(V task) { return isSomeVisibleAndNotEndEnd(graph.incomingEdgesOf(task)); } private boolean isSomeVisibleAndNotEndEnd(Set<D> dependencies) { for (D each : dependencies) { if ( !each.getType().equals(DependencyType.END_END) && adapter.isVisible(each) ) { return true; } } return false; } public boolean hasVisibleOutcomingDependencies(V task) { return isSomeVisibleAndNotStartStart(graph.outgoingEdgesOf(task)); } private boolean isSomeVisibleAndNotStartStart(Set<D> dependencies) { for (D each : dependencies) { if ( !each.getType().equals(DependencyType.START_START) && adapter.isVisible(each) ) { return true; } } return false; } public List<V> getLatestTasks() { List<V> tasks = new ArrayList<>(); for (V task : graph.vertexSet()) { int dependencies = graph.outDegreeOf(task); if ( (dependencies == 0) || (dependencies == getNumberOfOutgoingDependenciesByType(task, DependencyType.START_START)) ) { tasks.add(task); } } return tasks; } private int getNumberOfIncomingDependenciesByType(V task, DependencyType dependencyType) { int count = 0; for (D dependency : graph.incomingEdgesOf(task)) { if ( adapter.getType(dependency).equals(dependencyType) ) { count++; } } return count; } private int getNumberOfOutgoingDependenciesByType(V task, DependencyType dependencyType) { int count = 0; for (D dependency : graph.outgoingEdgesOf(task)) { if ( adapter.getType(dependency).equals(dependencyType) ) { count++; } } return count; } public boolean isContainer(V task) { if ( task == null ) { return false; } return adapter.isContainer(task); } public boolean contains(V container, V task) { if ( (container == null) || (task == null) ) { return false; } if ( adapter.isContainer(container) ) { return adapter.getChildren(container).contains(task); } return false; } public boolean doesNotProvokeLoop(D dependency) { Set<TaskPoint> reachableFromDestination = destinationPoint(dependency).getReachable(); for (TaskPoint each : reachableFromDestination) { if ( each.sendsModificationsThrough(dependency) ) { return false; } } return true; } TaskPoint destinationPoint(D dependency) { V destination = getDependencyDestination(dependency); return new TaskPoint(destination, getDestinationPoint(dependency.getType())); } private Point getDestinationPoint(DependencyType type) { return type.getSourceAndDestination()[isScheduleForward() ? 1 : 0]; } TaskPoint sourcePoint(D dependency) { V source = getDependencySource(dependency); return new TaskPoint(source, getSourcePoint(dependency.getType())); } /** * The dominating point is the one that causes the other point to be * modified; e.g. when doing forward scheduling the dominating point is the * start. */ private boolean isDominatingPoint(Point point) { return point == getDominatingPoint(); } private Point getDominatingPoint() { return isScheduleForward() ? Point.START : Point.END; } private Point getSourcePoint(DependencyType type) { return type.getSourceAndDestination()[isScheduleForward() ? 0 : 1]; } private V getDependencySource(D dependency) { return isScheduleForward() ? adapter.getSource(dependency) : adapter.getDestination(dependency); } private V getDependencyDestination(D dependency) { return isScheduleForward() ? adapter.getDestination(dependency) : adapter.getSource(dependency); } TaskPoint allPointsPotentiallyModified(V task) { return new TaskPoint(task, getDominatingPoint()); } private class TaskPoint { private final V task; private final boolean isContainer; private final Set<Point> pointsModified; private final Point entryPoint; TaskPoint(V task, Point entryPoint) { Validate.notNull(task); Validate.notNull(entryPoint); this.task = task; this.entryPoint = entryPoint; this.pointsModified = isDominatingPoint(entryPoint) ? EnumSet.of(Point.START, Point.END) : EnumSet.of(entryPoint); this.isContainer = adapter.isContainer(task); } @Override public String toString() { return String.format("%s(%s)", task, pointsModified); } @Override public boolean equals(Object obj) { if ( TaskPoint.class.isInstance(obj) ) { TaskPoint other = TaskPoint.class.cast(obj); return new EqualsBuilder() .append(task, other.task) .append(pointsModified, other.pointsModified) .isEquals(); } return false; } @Override public int hashCode() { return new HashCodeBuilder().append(task).append(pointsModified).toHashCode(); } public boolean areAllPointsPotentiallyModified() { return pointsModified.size() > 1; } public boolean somePointPotentiallyModified() { return pointsModified.contains(Point.START) || pointsModified.contains(Point.END); } public boolean onlyModifies(Point point) { return pointsModified.size() == 1 && pointsModified.contains(point); } Set<TaskPoint> getReachable() { Set<TaskPoint> result = new HashSet<>(); Queue<TaskPoint> pending = new LinkedList<>(); result.add(this); pending.offer(this); while (!pending.isEmpty()) { TaskPoint current = pending.poll(); Set<TaskPoint> immendiate = current.getImmediateSuccessors(); for (TaskPoint each : immendiate) { if ( !result.contains(each) ) { result.add(each); pending.offer(each); } } } return result; } public boolean isImmediatelyDerivedFrom(TaskPoint other) { return this.task.equals(other.task) && other.pointsModified.containsAll(this.pointsModified); } private Set<TaskPoint> cachedInmmediateSuccesors = null; public Set<TaskPoint> getImmediateSuccessors() { if ( cachedInmmediateSuccesors != null ) { return cachedInmmediateSuccesors; } Set<TaskPoint> result = new HashSet<>(); result.addAll(getImmediatelyDerivedOnSameTask()); Set<D> candidates = immediateDependencies(); for (D each : candidates) { if ( this.sendsModificationsThrough(each) ) { result.add(destinationPoint(each)); } } return cachedInmmediateSuccesors = Collections.unmodifiableSet(result); } private Set<TaskPoint> cachedImmediatePredecessors = null; public Set<TaskPoint> getImmediatePredecessors() { if ( cachedImmediatePredecessors != null ) { return cachedImmediatePredecessors; } Set<TaskPoint> result = new HashSet<>(); if ( !isDominatingPoint(entryPoint) ) { TaskPoint dominating = allPointsPotentiallyModified(task); assert isDominatingPoint(dominating.entryPoint); assert this.isImmediatelyDerivedFrom(dominating); result.add(dominating); } for (D each : immediateIncomingDependencies()) { if ( this.receivesModificationsThrough(each) ) { TaskPoint sourcePoint = sourcePoint(each); result.add(sourcePoint); } } return cachedImmediatePredecessors = Collections.unmodifiableSet(result); } private Collection<TaskPoint> getImmediatelyDerivedOnSameTask() { for (Point each : pointsModified) { if ( isDominatingPoint(each) ) { return Collections.singletonList(new TaskPoint(task, each.getOther())); } } return Collections.emptyList(); } private Set<D> immediateDependencies() { return isScheduleForward() ? graph.outgoingEdgesOf(this.task) : graph.incomingEdgesOf(this.task); } private Set<D> immediateIncomingDependencies() { return isScheduleForward() ? graph.incomingEdgesOf(this.task) : graph.outgoingEdgesOf(this.task); } public boolean sendsModificationsThrough(D dependency) { V source = getDependencySource(dependency); Point dependencySourcePoint = getSourcePoint(adapter.getType(dependency)); return source.equals(task) && (!isContainer || pointsModified.contains(dependencySourcePoint)); } private Point getSourcePoint(DependencyType type) { Point[] sourceAndDestination = type.getSourceAndDestination(); return sourceAndDestination[isScheduleForward() ? 0 : 1]; } private boolean receivesModificationsThrough(D dependency) { V destination = getDependencyDestination(dependency); Point destinationPoint = getDestinationPoint(adapter.getType(dependency)); return destination.equals(task) && entryPoint == destinationPoint; } } private V getTopmostFor(V task) { V result = task; while (fromChildToParent.containsKey(result)) { result = fromChildToParent.get(result); } return result; } public boolean isScheduleForward() { return !isScheduleBackwards(); } public boolean isScheduleBackwards() { return scheduleBackwards; } @Override public GanttDate getEndDateFor(V task) { return adapter.getEndDateFor(task); } @Override public List<Constraint<GanttDate>> getStartConstraintsFor(V task) { return adapter.getStartConstraintsFor(task); } @Override public List<Constraint<GanttDate>> getEndConstraintsFor(V task) { return adapter.getEndConstraintsFor(task); } @Override public GanttDate getStartDate(V task) { return adapter.getStartDate(task); } @Override public List<V> getChildren(V task) { if ( !isContainer(task) ) { return Collections.emptyList(); } return adapter.getChildren(task); } }