/* * 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.libreplan.web.planner.allocation; import static org.libreplan.business.workingday.EffortDuration.hours; import static org.libreplan.business.workingday.EffortDuration.zero; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; import org.joda.time.LocalDate; import org.libreplan.business.calendars.entities.ThereAreHoursOnWorkHoursCalculator.CapacityResult; import org.libreplan.business.common.Flagged; import org.libreplan.business.orders.entities.HoursGroup; import org.libreplan.business.planner.entities.AssignmentFunction; import org.libreplan.business.planner.entities.CalculatedValue; import org.libreplan.business.planner.entities.DerivedAllocationGenerator.IWorkerFinder; import org.libreplan.business.planner.entities.ResourceAllocation; import org.libreplan.business.planner.entities.ResourceAllocation.AllocationsSpecified.INotFulfilledReceiver; import org.libreplan.business.planner.entities.ResourceAllocation.Direction; import org.libreplan.business.planner.entities.Task; import org.libreplan.business.planner.entities.allocationalgorithms.AllocationModification; import org.libreplan.business.planner.entities.allocationalgorithms.EffortModification; import org.libreplan.business.planner.entities.allocationalgorithms.ResourcesPerDayModification; import org.libreplan.business.resources.entities.Criterion; import org.libreplan.business.resources.entities.Resource; import org.libreplan.business.resources.entities.ResourceEnum; import org.libreplan.business.scenarios.entities.Scenario; import org.libreplan.business.workingday.EffortDuration; import org.libreplan.business.workingday.EffortDuration.IEffortFrom; public class AllocationRowsHandler { public static AllocationRowsHandler create(Task task, List<AllocationRow> initialAllocations, IWorkerFinder workerFinder) { return new AllocationRowsHandler(task, initialAllocations, workerFinder); } private final List<AllocationRow> currentRows; private final Set<ResourceAllocation<?>> requestedToRemove = new HashSet<ResourceAllocation<?>>(); private final Task task; private FormBinder formBinder = null; private CalculatedValue calculatedValue; private final IWorkerFinder workersFinder; private AllocationRowsHandler(Task task, List<AllocationRow> initialRows, IWorkerFinder workersFinder) { this.task = task; this.workersFinder = workersFinder; this.currentRows = new ArrayList<AllocationRow>(initialRows); this.calculatedValue = task.getCalculatedValue(); } public void addSpecificResourceAllocationFor(List<Resource> resource) { List<Resource> alreadyPresent = new ArrayList<Resource>(); for (Resource each : resource) { if (alreadyExistsAllocationFor(each)) { alreadyPresent.add(each); } else { SpecificAllocationRow specificAllocationRow = SpecificAllocationRow .forResource(getCalculatedValue(), each); setupInitialHours(specificAllocationRow); currentRows.add(specificAllocationRow); formBinder.newAllocationAdded(); } } if (!alreadyPresent.isEmpty()) { formBinder.markRepeatedResources(alreadyPresent); } } public void addGeneric(ResourceEnum resourceType, Collection<? extends Criterion> criteria, Collection<? extends Resource> resourcesMatched) { addGeneric(resourceType, criteria, resourcesMatched, null); } public boolean addGeneric(ResourceEnum resourceType, Collection<? extends Criterion> criteria, Collection<? extends Resource> resourcesMatched, Integer hours) { if (resourcesMatched.isEmpty()) { formBinder.markNoResourcesMatchedByCriterions(resourceType, criteria); return false; } else { GenericAllocationRow genericAllocationRow = GenericAllocationRow .create(getCalculatedValue(), resourceType, criteria, resourcesMatched); if (hours != null) { genericAllocationRow.setEffortToInput(hours(hours)); } else { setupInitialHours(genericAllocationRow); } if (alreadyExistsAllocationFor(resourceType, criteria)) { formBinder.markThereisAlreadyAssignmentWith(resourceType, criteria); return false; } else { currentRows.add(genericAllocationRow); formBinder.newAllocationAdded(); return true; } } } private void setupInitialHours(AllocationRow allocationRow) { EffortDuration effortCalculated = calculateEffort(allocationRow); if (effortCalculated != null) { allocationRow.setEffortToInput(effortCalculated); } } private EffortDuration calculateEffort(AllocationRow allocationRow) { ResourceEnum type = allocationRow.getType(); if (notStillExistAnotherAllocation(type)) { return calculateEffortByCalculatedValue(type); } return null; } private boolean notStillExistAnotherAllocation(ResourceEnum type) { for (AllocationRow allocation : getCurrentRows()) { if (allocation.getType().equals(type)) { return false; } } return true; } private EffortDuration calculateEffortByCalculatedValue(ResourceEnum type) { switch (calculatedValue) { case NUMBER_OF_HOURS: break; case END_DATE: case RESOURCES_PER_DAY: return calculateEffortByResourceType(type); } return null; } private EffortDuration calculateEffortByResourceType(final ResourceEnum type) { return EffortDuration.sum(task.getTaskSource().getHoursGroups(), new IEffortFrom<HoursGroup>() { @Override public EffortDuration from(HoursGroup each) { if (type.equals(each.getResourceType())) { return hours(each.getWorkingHours()); } return zero(); } }); } public List<AllocationRow> getCurrentRows() { return currentRows; } private boolean alreadyExistsAllocationFor(Resource resource) { return !getAllocationsFor(resource).isEmpty(); } private boolean alreadyExistsAllocationFor(ResourceEnum resourceType, Collection<? extends Criterion> criterions) { Set<Criterion> criterionsSet = new HashSet<Criterion>(criterions); List<GenericAllocationRow> generic = AllocationRow .getGeneric(getCurrentRows()); for (GenericAllocationRow each : generic) { if (each.hasSameCriterionsAndType(criterionsSet, resourceType)) { return true; } } return false; } private List<SpecificAllocationRow> getAllocationsFor(Resource resource) { List<SpecificAllocationRow> found = SpecificAllocationRow.withResource( SpecificAllocationRow.getSpecific(currentRows), resource); return found; } public void remove(AllocationRow row) { currentRows.remove(row); if (row.isModifying()) { requestedToRemove.add(row.getOrigin()); } formBinder.rowRemoved(); } public Set<ResourceAllocation<?>> getAllocationsRequestedToRemove() { return requestedToRemove; } public void checkInvalidValues() { if (calculatedValue != CalculatedValue.NUMBER_OF_HOURS && !currentRows.isEmpty() && formBinder.getAssignedEffort().compareTo(zero()) <= 0) { formBinder.markAssignedHoursMustBePositive(); } if (calculatedValue != CalculatedValue.RESOURCES_PER_DAY) { List<AllocationRow> rows = getRowsWithEmptyResourcesPerDay(); if (!rows.isEmpty()) { formBinder.markNoEmptyResourcesPerDay(rows); } } } private List<AllocationRow> getRowsWithEmptyResourcesPerDay() { List<AllocationRow> result = new ArrayList<AllocationRow>(); for (AllocationRow each : currentRows) { if (each.isEmptyResourcesPerDay()) { result.add(each); } } return result; } public enum Warnings { SOME_GOALS_NOT_FULFILLED; } public Flagged<AllocationResult, Warnings> doAllocation() { checkInvalidValues(); if ( !currentRows.isEmpty() ) { List<? extends AllocationModification> modificationsDone; modificationsDone = doSuitableAllocation(); AllocationRow.updateUIWithModificationsDone(currentRows, modificationsDone); createDerived(); AllocationResult result = createResult(); if ( AllocationModification.allFullfiled(AllocationModification .ofType(EffortModification.class, modificationsDone)) ) { return Flagged.justValue(result); } else { return Flagged.withFlags(result, Warnings.SOME_GOALS_NOT_FULFILLED); } } return Flagged.justValue(createResult()); } private AllocationResult createResult() { return AllocationResult.create(task, calculatedValue, currentRows, getWorkableDaysIfApplyable()); } private List<? extends AllocationModification> doSuitableAllocation() { List<? extends AllocationModification> allocationModifications; switch (calculatedValue) { case NUMBER_OF_HOURS: allocationModifications = calculateNumberOfHoursAllocation(); break; case END_DATE: allocationModifications = calculateEndDateOrStartDateAllocation(); break; case RESOURCES_PER_DAY: allocationModifications = calculateResourcesPerDayAllocation(); break; default: throw new RuntimeException("cant handle: " + calculatedValue); } AssignmentFunction .applyAssignmentFunctionsIfAny(AllocationModification.getBeingModified(allocationModifications)); return allocationModifications; } private List<ResourcesPerDayModification> calculateNumberOfHoursAllocation() { List<ResourcesPerDayModification> allocations = AllocationRow.createAndAssociate(task, currentRows, requestedToRemove); if ( isForwardsAllocation() ) { ResourceAllocation.allocating(allocations).allocateUntil(formBinder.getAllocationEnd()); } else { ResourceAllocation.allocating(allocations).allocateFromEndUntil(formBinder.getAllocationStart()); } return allocations; } public boolean isForwardsAllocation() { return Direction.FORWARD.equals(task.getAllocationDirection()); } private List<ResourcesPerDayModification> calculateEndDateOrStartDateAllocation() { List<ResourcesPerDayModification> allocations = AllocationRow .createAndAssociate(task, currentRows, requestedToRemove); ResourceAllocation.allocating(allocations).untilAllocating( task.getAllocationDirection(), formBinder.getAssignedEffort(), notFullfiledReceiver()); return allocations; } private INotFulfilledReceiver notFullfiledReceiver() { return new INotFulfilledReceiver() { @Override public void cantFulfill(ResourcesPerDayModification attempt, CapacityResult capacityResult) { final AllocationRow row = findRowFor(attempt.getBeingModified()); row.markNoCapacity(attempt, capacityResult); } }; } private AllocationRow findRowFor(ResourceAllocation<?> resourceAllocation) { return AllocationRow.find(currentRows, resourceAllocation); } private List<EffortModification> calculateResourcesPerDayAllocation() { List<EffortModification> hours = AllocationRow.createHoursModificationsAndAssociate(task, currentRows, requestedToRemove); if ( isForwardsAllocation() ) { ResourceAllocation.allocatingHours(hours).allocateUntil(formBinder.getAllocationEnd()); } else { ResourceAllocation.allocatingHours(hours).allocateFromEndUntil(formBinder.getAllocationStart()); } return hours; } private Integer getWorkableDaysIfApplyable() { switch (calculatedValue) { case NUMBER_OF_HOURS: case RESOURCES_PER_DAY: return formBinder.getWorkableDays(); case END_DATE: return null; default: throw new RuntimeException("unexpected calculatedValue: " + calculatedValue); } } private void createDerived() { List<ResourceAllocation<?>> lastFrom = AllocationRow.getBeingModified(currentRows); for (ResourceAllocation<?> each : lastFrom) { each.createDerived(workersFinder); } } public FormBinder createFormBinder(Scenario currentScenario, IResourceAllocationModel resourceAllocationModel) { if ( formBinder != null ) { throw new IllegalStateException("there is already a binder associated with this object"); } formBinder = new FormBinder(currentScenario, this, resourceAllocationModel); return formBinder; } public CalculatedValue getCalculatedValue() { return this.calculatedValue; } public void setCalculatedValue(CalculatedValue calculatedValue) { this.calculatedValue = calculatedValue; } public AllocationResult getInitialAllocation(Scenario currentScenario) { return AllocationResult.createCurrent(currentScenario, task); } public Task getTask() { return task; } public Set<Resource> getAllocationResources() { Set<Resource> result = new HashSet<Resource>(); for (AllocationRow each : currentRows) { result.addAll(each.getAssociatedResources()); } for (ResourceAllocation<?> each : requestedToRemove) { result.addAll(each.getAssociatedResources()); } return result; } public LocalDate getStartDate() { return new LocalDate(task.getStartDate()); } public void removeAll() { for (AllocationRow each : copyOfCurrentRowsToAvoidConcurrentModification()) { remove(each); } } private ArrayList<AllocationRow> copyOfCurrentRowsToAvoidConcurrentModification() { return new ArrayList<AllocationRow>(currentRows); } public boolean isTaskUpdatedFromTimesheets() { return task.isUpdatedFromTimesheets(); } }