/* * 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-2012 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.libreplan.business.planner.entities; import static java.util.Arrays.asList; import static org.libreplan.business.workingday.EffortDuration.zero; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.EnumSet; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.Validate; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import javax.validation.constraints.NotNull; import org.joda.time.LocalDate; import org.libreplan.business.calendars.entities.BaseCalendar; import org.libreplan.business.common.BaseEntity; import org.libreplan.business.common.entities.ProgressType; import org.libreplan.business.externalcompanies.entities.ExternalCompany; import org.libreplan.business.orders.entities.Order; import org.libreplan.business.orders.entities.OrderElement; import org.libreplan.business.orders.entities.OrderStatusEnum; import org.libreplan.business.orders.entities.TaskSource; import org.libreplan.business.planner.entities.DayAssignment.FilterType; import org.libreplan.business.planner.entities.Dependency.Type; import org.libreplan.business.resources.daos.IResourcesSearcher; import org.libreplan.business.scenarios.entities.Scenario; import org.libreplan.business.util.TaskElementVisitor; import org.libreplan.business.util.deepcopy.OnCopy; import org.libreplan.business.util.deepcopy.Strategy; import org.libreplan.business.workingday.EffortDuration; import org.libreplan.business.workingday.IntraDayDate; import org.libreplan.business.workingday.ResourcesPerDay; /** * @author Óscar González Fernández <ogonzalez@igalia.com> * @author Manuel Rego Casasnovas <rego@igalia.com> */ public abstract class TaskElement extends BaseEntity { private static final Log LOG = LogFactory.getLog(TaskElement.class); private static final IDatesInterceptor EMPTY_INTERCEPTOR = new IDatesInterceptor() { @Override public void setStartDate(IntraDayDate previousStart, IntraDayDate previousEnd, IntraDayDate newStart) {} @Override public void setNewEnd(IntraDayDate previousEnd, IntraDayDate newEnd) {} }; @OnCopy(Strategy.SHARE) private IDatesInterceptor datesInterceptor = EMPTY_INTERCEPTOR; @OnCopy(Strategy.SHARE) private BaseCalendar calendar; private IntraDayDate startDate; private IntraDayDate endDate; private LocalDate deadline; private String name; private String notes; private BigDecimal advancePercentage = BigDecimal.ZERO; private Boolean simplifiedAssignedStatusCalculationEnabled = false; private Boolean updatedFromTimesheets = false; private EffortDuration sumOfAssignedEffort = EffortDuration.zero(); private TaskGroup parent; private Set<Dependency> dependenciesWithThisOrigin = new HashSet<>(); private Set<Dependency> dependenciesWithThisDestination = new HashSet<>(); private TaskSource taskSource; public static List<Task> justTasks(Collection<? extends TaskElement> tasks) { List<Task> result = new ArrayList<>(); for (TaskElement taskElement : tasks) { if ( taskElement instanceof Task ) { result.add((Task) taskElement); } } return result; } public interface IDatesInterceptor { void setStartDate(IntraDayDate previousStart, IntraDayDate previousEnd, IntraDayDate newStart); void setNewEnd(IntraDayDate previousEnd, IntraDayDate newEnd); } public static Comparator<TaskElement> getByStartDateComparator() { Comparator<TaskElement> result = new Comparator<TaskElement>() { @Override public int compare(TaskElement o1, TaskElement o2) { return o1.getStartDate().compareTo(o2.getStartDate()); } }; return result; } public static Comparator<? super TaskElement> getByEndAndDeadlineDateComparator() { return new Comparator<TaskElement>() { @Override public int compare(TaskElement o1, TaskElement o2) { return o1.getBiggestAmongEndOrDeadline().compareTo(o2.getBiggestAmongEndOrDeadline()); } }; } /** * @returns the biggest one among the deadline (if exists) or the end date. */ @SuppressWarnings("unchecked") public LocalDate getBiggestAmongEndOrDeadline() { return this.getDeadline() != null ? Collections.max(asList(this.getDeadline(), this.getEndAsLocalDate())) : this.getEndAsLocalDate(); } protected static <T extends TaskElement> T create(T taskElement, TaskSource taskSource) { taskElement.setTaskSource(taskSource); taskElement.updateDeadlineFromOrderElement(); taskElement.setName(taskElement.getOrderElement().getName()); taskElement.updateAdvancePercentageFromOrderElement(); Order order = taskElement.getOrderElement().getOrder(); if ( order.isScheduleBackwards() ) { taskElement.setEndDate(order.getDeadline()); } else { taskElement.setStartDate(order.getInitDate()); } return create(taskElement); } protected static <T extends TaskElement> T createWithoutTaskSource(T taskElement) { return create(taskElement); } public void initializeDatesIfNeeded() { if ( getIntraDayEndDate() == null || getIntraDayStartDate() == null ) { initializeDates(); } } protected abstract void initializeDates(); public void updateDeadlineFromOrderElement() { Date newDeadline = this.taskSource.getOrderElement().getDeadline(); setDeadline(newDeadline == null ? null : new LocalDate(newDeadline)); } public void setDatesInterceptor(IDatesInterceptor datesIntercerptor) { Validate.notNull(datesIntercerptor); this.datesInterceptor = datesIntercerptor; } public Integer getWorkHours() { return taskSource == null ? 0 : taskSource.getTotalHours(); } protected void copyPropertiesFrom(TaskElement task) { this.name = task.getName(); this.notes = task.getNotes(); this.startDate = task.startDate; this.taskSource = task.getTaskSource(); } public TaskSource getTaskSource() { return taskSource; } protected void setTaskSource(TaskSource taskSource) { this.taskSource = taskSource; } protected void copyDependenciesTo(TaskElement result) { for (Dependency dependency : getDependenciesWithThisOrigin()) { Dependency.create(result, dependency.getDestination(), dependency.getType()); } for (Dependency dependency : getDependenciesWithThisDestination()) { Dependency.create(dependency.getOrigin(), result, dependency.getType()); } } protected void copyParenTo(TaskElement result) { if ( this.getParent() != null ) { this.getParent().addTaskElement(result); } } public TaskGroup getParent() { return parent; } public String getName() { return name; } public String getCode() { return getOrderElement().getCode(); } public String getProjectCode() { return getOrderElement().getOrder().getCode(); } public void setName(String name) { this.name = name; if ( taskSource != null && taskSource.getOrderElement() != null ) { taskSource.getOrderElement().setName(name); } } public String getNotes() { return notes; } public void setNotes(String notes) { this.notes = notes; } public OrderElement getOrderElement() { return getTaskSource() == null ? null : getTaskSource().getOrderElement(); } public Set<Dependency> getDependenciesWithThisOrigin() { return Collections.unmodifiableSet(dependenciesWithThisOrigin); } public Set<Dependency> getDependenciesWithThisDestination() { return Collections.unmodifiableSet(dependenciesWithThisDestination); } public Set<Dependency> getDependenciesWithThisDestinationAndAllParents() { Set<Dependency> result = new HashSet<>(getDependenciesWithThisDestination()); if ( parent != null ) { result.addAll(parent.getDependenciesWithThisDestinationAndAllParents()); } return result; } public Date getStartDate() { return startDate != null ? startDate.getDate().toDateTimeAtStartOfDay().toDate() : null; } @NotNull public IntraDayDate getIntraDayStartDate() { return startDate; } public LocalDate getStartAsLocalDate() { return startDate == null ? null : startDate.getDate(); } public LocalDate getEndAsLocalDate() { return endDate == null ? null : endDate.getDate(); } public void setStartDate(Date startDate) { setIntraDayStartDate(IntraDayDate.startOfDay(LocalDate.fromDateFields(startDate))); } public void setIntraDayStartDate(IntraDayDate startDate) { if ( startDate == null ) { LOG.error(doNotProvideNullsDiscouragingMessage()); } IntraDayDate previousStart = getIntraDayStartDate(); IntraDayDate previousEnd = getIntraDayEndDate(); this.startDate = startDate; datesInterceptor.setStartDate(previousStart, previousEnd, getIntraDayStartDate()); } @NotNull public Date getEndDate() { return (endDate != null) ? endDate.toDateTimeAtStartOfDay().toDate() : null; } public void setEndDate(Date endDate) { setIntraDayEndDate( (endDate != null) ? IntraDayDate.create(LocalDate.fromDateFields(endDate), EffortDuration.zero()) : null); } public void setIntraDayEndDate(IntraDayDate endDate) { if ( endDate == null ) { LOG.error(doNotProvideNullsDiscouragingMessage()); } IntraDayDate previousEnd = getIntraDayEndDate(); this.endDate = endDate; datesInterceptor.setNewEnd(previousEnd, this.endDate); } private String doNotProvideNullsDiscouragingMessage() { return "The provided date shouldn't be null.\n" + "Providing null values to start or end dates is not safe.\n" + "In a near future an exception will be thrown if you provide a null value to a start or end date.\n" + "Please detect the caller and fix it"; } @NotNull public IntraDayDate getIntraDayEndDate() { return endDate; } public IDatesHandler getDatesHandler(Scenario scenario, IResourcesSearcher resourcesSearcher) { return noNullDates(createDatesHandler(scenario, resourcesSearcher)); } private IDatesHandler noNullDates(final IDatesHandler decorated) { return new IDatesHandler() { @Override public void resizeTo(IntraDayDate endDate) { Validate.notNull(endDate); decorated.resizeTo(endDate); } @Override public void moveTo(IntraDayDate newStartDate) { Validate.notNull(newStartDate); decorated.moveTo(newStartDate); } @Override public void moveEndTo(IntraDayDate newEnd) { Validate.notNull(newEnd); decorated.moveEndTo(newEnd); } }; } protected abstract IDatesHandler createDatesHandler(Scenario scenario, IResourcesSearcher resourcesSearcher); public interface IDatesHandler { /** * Sets the startDate to newStartDate. It can update the endDate. * * @param newStartDate */ void moveTo(IntraDayDate newStartDate); void moveEndTo(IntraDayDate newEnd); void resizeTo(IntraDayDate endDate); } protected abstract boolean canBeResized(); /** * @return if this task can be resized by an explicit action */ public abstract boolean canBeExplicitlyResized(); public LocalDate getDeadline() { return deadline; } public void setDeadline(LocalDate deadline) { this.deadline = deadline; if ( taskSource != null && taskSource.getOrderElement() != null ) { taskSource.getOrderElement().setDeadline((deadline == null) ? null : deadline.toDateTimeAtStartOfDay().toDate()); } } public void add(Dependency dependency) { if ( this.equals(dependency.getOrigin()) ) { dependenciesWithThisOrigin.add(dependency); } if ( this.equals(dependency.getDestination()) ) { dependenciesWithThisDestination.add(dependency); } } private void removeDependenciesWithThisOrigin(TaskElement origin, Type type) { ArrayList<Dependency> toBeRemoved = new ArrayList<>(); for (Dependency dependency : dependenciesWithThisDestination) { if ( dependency.getOrigin().equals(origin) && dependency.getType().equals(type) ) { toBeRemoved.add(dependency); } } dependenciesWithThisDestination.removeAll(toBeRemoved); } public void removeDependencyWithDestination(TaskElement destination, Type type) { ArrayList<Dependency> toBeRemoved = new ArrayList<>(); for (Dependency dependency : dependenciesWithThisOrigin) { if ( dependency.getDestination().equals(destination) && dependency.getType().equals(type) ) { toBeRemoved.add(dependency); } } destination.removeDependenciesWithThisOrigin(this, type); dependenciesWithThisOrigin.removeAll(toBeRemoved); } public abstract boolean isLeaf(); public abstract List<TaskElement> getChildren(); protected void setParent(TaskGroup taskGroup) { this.parent = taskGroup; } public void detach() { detachDependencies(); detachFromParent(); } public void detachFromParent() { if ( parent != null ) { parent.remove(this); } } private void removeDependenciesWithOrigin(TaskElement t) { List<Dependency> dependenciesToRemove = getDependenciesWithOrigin(t); dependenciesWithThisDestination.removeAll(dependenciesToRemove); } private void removeDependenciesWithDestination(TaskElement t) { List<Dependency> dependenciesToRemove = getDependenciesWithDestination(t); dependenciesWithThisOrigin.removeAll(dependenciesToRemove); } private List<Dependency> getDependenciesWithDestination(TaskElement t) { ArrayList<Dependency> result = new ArrayList<>(); for (Dependency dependency : dependenciesWithThisOrigin) { if ( dependency.getDestination().equals(t) ) { result.add(dependency); } } return result; } private List<Dependency> getDependenciesWithOrigin(TaskElement t) { ArrayList<Dependency> result = new ArrayList<>(); for (Dependency dependency : dependenciesWithThisDestination) { if ( dependency.getOrigin().equals(t) ) { result.add(dependency); } } return result; } public void detachDependencies() { detachOutcomingDependencies(); detachIncomingDependencies(); } private void detachIncomingDependencies() { Set<TaskElement> tasksToNotify = new HashSet<>(); for (Dependency dependency : dependenciesWithThisDestination) { TaskElement origin = dependency.getOrigin(); if ( origin != null ) { tasksToNotify.add(origin); } } for (TaskElement taskElement : tasksToNotify) { this.removeDependenciesWithOrigin(taskElement); taskElement.removeDependenciesWithDestination(this); } } private void detachOutcomingDependencies() { Set<TaskElement> tasksToNotify = new HashSet<>(); for (Dependency dependency : dependenciesWithThisOrigin) { TaskElement destination = dependency.getDestination(); if ( destination != null ) { tasksToNotify.add(destination); } } for (TaskElement taskElement : tasksToNotify) { this.removeDependenciesWithDestination(taskElement); taskElement.removeDependenciesWithOrigin(this); } } public void setCalendar(BaseCalendar calendar) { this.calendar = calendar; } public BaseCalendar getOwnCalendar() { return calendar; } public BaseCalendar getCalendar() { if ( calendar == null ) { OrderElement orderElement = getOrderElement(); return orderElement != null ? orderElement.getOrder().getCalendar() : null; } return calendar; } public abstract Set<ResourceAllocation<?>> getSatisfiedResourceAllocations(); public abstract Set<ResourceAllocation<?>> getAllResourceAllocations(); public SortedMap<LocalDate, EffortDuration> getDurationsAssignedByDay() { SortedMap<LocalDate, EffortDuration> result = new TreeMap<>(); for (ResourceAllocation<?> resourceAllocation : getSatisfiedResourceAllocations()) { for (DayAssignment each : resourceAllocation.getAssignments()) { addToResult(result, each.getDay(), each.getDuration()); } } return result; } private void addToResult(SortedMap<LocalDate, EffortDuration> result, LocalDate date, EffortDuration duration) { EffortDuration current = result.get(date) != null ? result.get(date) : zero(); result.put(date, current.plus(duration)); } public List<DayAssignment> getDayAssignments(DayAssignment.FilterType filter) { List<DayAssignment> dayAssignments = new ArrayList<>(); Set<ResourceAllocation<?>> resourceAllocations = getSatisfiedResourceAllocations(); for (ResourceAllocation<?> resourceAllocation : resourceAllocations) { dayAssignments.addAll(resourceAllocation.getAssignments()); Set<DerivedAllocation> derivedAllocations = resourceAllocation.getDerivedAllocations(); for (DerivedAllocation each : derivedAllocations) { dayAssignments.addAll(each.getAssignments()); } } return DayAssignment.filter(dayAssignments, filter); } /** * Just Task could be subcontracted. */ public boolean isSubcontracted() { return false; } public String getSubcontractionName() { return ""; } /** * Just Task could be subcontracted. */ public boolean isSubcontractedAndWasAlreadySent() { return false; } public boolean isLimiting() { return false; } public boolean isLimitingAndHasDayAssignments() { return false; } /** * Just Task could be consolidated. */ public boolean hasConsolidations() { return false; } public TaskElement getTopMost() { TaskElement result = this; while (result.getParent() != null) { result = result.getParent(); } return result; } public abstract boolean isMilestone(); public Boolean isSimplifiedAssignedStatusCalculationEnabled() { return simplifiedAssignedStatusCalculationEnabled; } public void setSimplifiedAssignedStatusCalculationEnabled(Boolean enabled) { this.simplifiedAssignedStatusCalculationEnabled = enabled; } public String getAssignedStatus() { if (isSimplifiedAssignedStatusCalculationEnabled()) { // Simplified calculation has only two states: // 1. Unassigned, when hours allocated is zero. // 2. Assigned otherwise. return getSumOfAssignedEffort().isZero() ? "unassigned" : "assigned"; } Set<ResourceAllocation<?>> resourceAllocations = getSatisfiedResourceAllocations(); if ( resourceAllocations.isEmpty() ) { return "unassigned"; } for (ResourceAllocation<?> resourceAllocation : resourceAllocations) { final ResourcesPerDay resourcesPerDay = resourceAllocation.getResourcesPerDay(); if ( resourcesPerDay != null && resourcesPerDay.isZero() ) { return "partially-assigned"; } } return "assigned"; } public Boolean belongsClosedProject() { EnumSet<OrderStatusEnum> CLOSED = EnumSet.of(OrderStatusEnum.CANCELLED, OrderStatusEnum.FINISHED, OrderStatusEnum.STORED); return CLOSED.contains(getOrderElement().getOrder().getState()); } public abstract boolean hasLimitedResourceAllocation(); public void removePredecessorsDayAssignmentsFor(Scenario scenario) { for (ResourceAllocation<?> each : getAllResourceAllocations()) { each.removePredecessorsDayAssignmentsFor(scenario); } } public void removeDayAssignmentsFor(Scenario scenario) { for (ResourceAllocation<?> each : getAllResourceAllocations()) { each.removeDayAssignmentsFor(scenario); } } public BigDecimal getAdvancePercentage() { return (advancePercentage == null) ? BigDecimal.ZERO : advancePercentage; } /** * For common tasks it just return the spread progress. * * It's overridden in {@link TaskGroup} to return different progresses depending on parameter. */ public BigDecimal getAdvancePercentage(ProgressType progressType) { return progressType != null && progressType.equals(ProgressType.SPREAD_PROGRESS) ? advancePercentage : BigDecimal.ZERO; } public void setAdvancePercentage(BigDecimal advancePercentage) { this.advancePercentage = advancePercentage; this.resetStatus(); } public void setSumOfAssignedEffort(EffortDuration sumOfAssignedEffort) { this.sumOfAssignedEffort = sumOfAssignedEffort; } public EffortDuration getSumOfAssignedEffort() { if ( this.getParent() == null ) { // It's an order, we use the cached value return sumOfAssignedEffort; } else { return getSumOfAssignedEffortCalculated(); } } private EffortDuration getSumOfAssignedEffortCalculated() { EffortDuration result = EffortDuration.zero(); for (ResourceAllocation<?> allocation : getAllResourceAllocations()) { result = result.plus(allocation.getAssignedEffort()); } return result; } public String toString() { return super.toString() + " :: " + getName(); } public abstract boolean isTask(); public List<TaskElement> getAllChildren() { List<TaskElement> children = getChildren(); List<TaskElement> result = new ArrayList<>(); for (TaskElement child : children) { result.add(child); result.addAll(child.getAllChildren()); } return result; } public abstract EffortDuration getTheoreticalCompletedTimeUntilDate(Date date); public BigDecimal getTheoreticalAdvancePercentageUntilDate(Date date) { EffortDuration totalAllocatedTime = AggregateOfDayAssignments.create(this.getDayAssignments(FilterType.KEEP_ALL)).getTotalTime(); EffortDuration totalTheoreticalCompletedTime = this.getTheoreticalCompletedTimeUntilDate(date); if ( totalAllocatedTime.isZero() || totalTheoreticalCompletedTime.isZero() ) { return BigDecimal.ZERO; } Validate.isTrue(totalTheoreticalCompletedTime.getSeconds() <= totalAllocatedTime.getSeconds()); return totalTheoreticalCompletedTime.dividedByAndResultAsBigDecimal(totalAllocatedTime); } public abstract boolean isFinished(); public abstract boolean isInProgress(); public abstract void acceptVisitor(TaskElementVisitor visitor); public abstract void resetStatus(); public void updateAdvancePercentageFromOrderElement() { setAdvancePercentage(getOrderElement().getAdvancePercentage()); } public Boolean isRoot() { return this.getParent() == null; } public BigDecimal getBudget() { return (taskSource != null) && (taskSource.getOrderElement() != null) ? taskSource.getOrderElement().getBudget() : null; } public BigDecimal getResourcesBudget() { return (taskSource != null) && (taskSource.getOrderElement() != null) ? taskSource.getOrderElement().getResourcesBudget() : null; } public ExternalCompany getSubcontractedCompany() { return null; } public abstract boolean isAnyTaskWithConstraint(PositionConstraintType type); public TaskDeadlineViolationStatusEnum getDeadlineViolationStatus() { LocalDate deadline = this.getDeadline(); if ( deadline == null ) { return TaskDeadlineViolationStatusEnum.NO_DEADLINE; } else if ( this.getEndAsLocalDate().isAfter(deadline) ) { return TaskDeadlineViolationStatusEnum.DEADLINE_VIOLATED; } else { return TaskDeadlineViolationStatusEnum.ON_SCHEDULE; } } public static IntraDayDate maxDate(Collection<? extends TaskElement> tasksToSave) { List<IntraDayDate> endDates = toEndDates(tasksToSave); return endDates.isEmpty() ? null : Collections.max(endDates); } private static List<IntraDayDate> toEndDates(Collection<? extends TaskElement> tasksToSave) { List<IntraDayDate> result = new ArrayList<>(); for (TaskElement taskElement : tasksToSave) { IntraDayDate endDate = taskElement.getIntraDayEndDate(); if ( endDate != null ) { result.add(endDate); } else { LOG.warn("the task" + taskElement + " has null end date"); } } return result; } public static IntraDayDate minDate(Collection<? extends TaskElement> tasksToSave) { List<IntraDayDate> startDates = toStartDates(tasksToSave); return startDates.isEmpty() ? null : Collections.min(startDates); } private static List<IntraDayDate> toStartDates(Collection<? extends TaskElement> tasksToSave) { List<IntraDayDate> result = new ArrayList<>(); for (TaskElement taskElement : tasksToSave) { IntraDayDate startDate = taskElement.getIntraDayStartDate(); if ( startDate != null ) { result.add(startDate); } else { LOG.warn("the task" + taskElement + " has null start date"); } } return result; } public Boolean isUpdatedFromTimesheets() { return updatedFromTimesheets; } public void setUpdatedFromTimesheets(Boolean updatedFromTimesheets) { this.updatedFromTimesheets = BooleanUtils.isTrue(updatedFromTimesheets); } }