/* * 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.business.calendars.entities; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.Objects; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; import javax.validation.constraints.AssertTrue; import org.hibernate.validator.constraints.NotEmpty; import javax.validation.constraints.NotNull; import javax.validation.Valid; import org.joda.time.LocalDate; import org.libreplan.business.calendars.daos.IBaseCalendarDAO; import org.libreplan.business.calendars.entities.AvailabilityTimeLine.IVetoer; import org.libreplan.business.calendars.entities.CalendarData.Days; import org.libreplan.business.common.IHumanIdentifiable; import org.libreplan.business.common.IntegrationEntity; import org.libreplan.business.common.entities.EntitySequence; import org.libreplan.business.common.exceptions.InstanceNotFoundException; import org.libreplan.business.resources.entities.VirtualWorker; import org.libreplan.business.workingday.EffortDuration; import org.libreplan.business.workingday.EffortDuration.IEffortFrom; import org.libreplan.business.workingday.IntraDayDate; import org.libreplan.business.workingday.IntraDayDate.PartialDay; import org.libreplan.business.workingday.ResourcesPerDay; /** * Represents a calendar with some exception days. * A calendar is valid till the expiring date, when the next calendar starts to be valid. * On the other hand, a calendar could be derived, and * the derived calendar could add or overwrite some exceptions of its parent calendar. * * @author Manuel Rego Casasnovas <mrego@igalia.com> */ public class BaseCalendar extends IntegrationEntity implements ICalendar, IHumanIdentifiable, Comparable<BaseCalendar> { private static final Capacity DEFAULT_VALUE = Capacity.zero().overAssignableWithoutLimit(); private String name; @Valid private Set<CalendarException> exceptions = new HashSet<>(); @Valid private List<CalendarData> calendarDataVersions = new ArrayList<>(); @Valid private List<CalendarAvailability> calendarAvailabilities = new ArrayList<>(); private Integer lastSequenceCode = 0; /** * Constructor for hibernate. Do not use! */ public BaseCalendar() { } protected BaseCalendar(CalendarData calendarData) { calendarDataVersions.add(calendarData); Collections.sort(calendarDataVersions, CalendarData.BY_EXPIRING_DATE_COMPARATOR); } public static BaseCalendar create() { return create(new BaseCalendar(CalendarData.create())); } public static BaseCalendar create(String code) { return create(new BaseCalendar(CalendarData.create()), code); } public static BaseCalendar createBasicCalendar() { BaseCalendar calendar = create(); resetDefaultCapacities(calendar); return calendar; } public static BaseCalendar createBasicCalendar(String code) { BaseCalendar calendar = create(code); resetDefaultCapacities(calendar); return calendar; } private static void resetDefaultCapacities(BaseCalendar calendar) { CalendarData calendarData = calendar.getLastCalendarData(); if ( calendarData != null ) { CalendarData.resetDefaultCapacities(calendarData); } } public static BaseCalendar createUnvalidated(String code, String name, BaseCalendar parent, Set<CalendarException> exceptions, List<CalendarData> calendarDataVersions) { BaseCalendar baseCalendar = create(new BaseCalendar(CalendarData.create()), code); baseCalendar.name = name; if ((exceptions != null) && (!exceptions.isEmpty())) { for (CalendarException exception : exceptions) { baseCalendar.addExceptionDay(exception); } } if ((calendarDataVersions != null) && (!calendarDataVersions.isEmpty())) { baseCalendar.calendarDataVersions = calendarDataVersions; } if (parent != null) { baseCalendar.setParent(parent); } return baseCalendar; } public void updateUnvalidated(String name, BaseCalendar parent) { if (!StringUtils.isBlank(name)) { this.name = name; } if (parent != null) { setParent(parent); } } public void setName(String name) { this.name = name; } @NotEmpty(message = "name not specified") public String getName() { return name; } public BaseCalendar getParent() { return getLastCalendarData().getParent(); } public BaseCalendar getParent(LocalDate date) { return getCalendarData(date).getParent(); } public void setParent(BaseCalendar parent) { getLastCalendarData().setParent(parent); } public void setParent(BaseCalendar parent, LocalDate date) { getCalendarData(date).setParent(parent); } public boolean isDerived() { return getParent() != null; } public boolean isDerived(LocalDate date) { return getParent(date) != null; } public Set<CalendarException> getOwnExceptions() { return Collections.unmodifiableSet(exceptions); } public Set<CalendarException> getExceptions() { Set<CalendarException> exceptionDays = new HashSet<>(); exceptionDays.addAll(exceptions); if (getParent() != null) { for (CalendarException exceptionDay : getParent().getExceptions()) { if (!isExceptionDayAlreadyInExceptions(exceptionDay)) { exceptionDays.add(exceptionDay); } } } return Collections.unmodifiableSet(exceptionDays); } public Set<CalendarException> getExceptions(LocalDate date) { Set<CalendarException> exceptionDays = new HashSet<>(); exceptionDays.addAll(exceptions); if (getParent(date) != null) { for (CalendarException exceptionDay : getParent(date).getExceptions()) { if (!isExceptionDayAlreadyInExceptions(exceptionDay)) { exceptionDays.add(exceptionDay); } } } return Collections.unmodifiableSet(exceptionDays); } private boolean isExceptionDayAlreadyInExceptions(CalendarException exceptionDay) { for (CalendarException day : exceptions) { if (day.getDate().equals(exceptionDay.getDate())) { return true; } } return false; } public void addExceptionDay(CalendarException day) { if (day.getDate() == null) { throw new IllegalArgumentException("This exception day has a incorrect date"); } if (isExceptionDayAlreadyInExceptions(day)) { throw new IllegalArgumentException("This day is already in the exception days"); } exceptions.add(day); } public void removeExceptionDay(LocalDate date) { CalendarException day = getOwnExceptionDay(date); if (day == null) { throw new IllegalArgumentException("There is not an exception day on that date"); } exceptions.remove(day); } public void updateExceptionDay(LocalDate date, Capacity capacity, CalendarExceptionType type) { removeExceptionDay(date); CalendarException day = CalendarException.create("", date, capacity, type); addExceptionDay(day); } public CalendarException getOwnExceptionDay(LocalDate date) { for (CalendarException exceptionDay : exceptions) { if (exceptionDay.getDate().equals(date)) { return exceptionDay; } } return null; } public CalendarException getExceptionDay(LocalDate date) { for (CalendarException exceptionDay : getExceptions(date)) { if (exceptionDay.getDate().equals(date)) { return exceptionDay; } } return null; } @Override public EffortDuration getCapacityOn(PartialDay date) { return date.limitWorkingDay(getCapacityWithOvertime(date.getDate()).getStandardEffort()); } @Override public Capacity getCapacityWithOvertime(LocalDate day) { Validate.notNull(day); return multiplyByCalendarUnits(findCapacityAt(day)); } private Capacity findCapacityAt(LocalDate date) { if (!isActive(date)) { return Capacity.zero(); } CalendarException exceptionDay = getExceptionDay(date); if (exceptionDay != null) { return exceptionDay.getCapacity(); } return getCapacityConsideringCalendarDataOn(date, getDayFrom(date)); } private Days getDayFrom(LocalDate date) { return Days.values()[date.getDayOfWeek() - 1]; } public Capacity getCapacityConsideringCalendarDataOn(LocalDate date, Days day) { CalendarData calendarData = getCalendarData(date); Capacity capacity = calendarData.getCapacityOn(day); BaseCalendar parent = getParent(date); if (capacity == null && parent != null) { return parent.getCapacityConsideringCalendarDataOn(date, day); } return valueIfNotNullElseDefaultValue(capacity); } private Capacity valueIfNotNullElseDefaultValue(Capacity capacity) { if (capacity == null) { return DEFAULT_VALUE; } return capacity; } /** * Returns the number of workable hours for a specific period depending on the calendar restrictions. * * @param init * @param end * @return Workable hours */ public Integer getWorkableHours(LocalDate init, LocalDate end) { return getWorkableDuration(init, end).roundToHours(); } /** * Returns the workable duration for a specific period depending on the calendar restrictions. * * @param init * @param endInclusive * @return Duration of work */ public EffortDuration getWorkableDuration(LocalDate init, LocalDate endInclusive) { Iterable<PartialDay> daysBetween = IntraDayDate.startOfDay(init).daysUntil(IntraDayDate.startOfDay(endInclusive).nextDayAtStart()); return EffortDuration.sum(daysBetween, new IEffortFrom<PartialDay>() { @Override public EffortDuration from(PartialDay each) { return getCapacityOn(each); } }); } /** * Returns the number of workable hours for a specific week depending on the calendar restrictions. * * @param date * @return Workable hours per week */ public Integer getWorkableHoursPerWeek(LocalDate date) { LocalDate init = date.dayOfWeek().withMinimumValue(); LocalDate end = date.dayOfWeek().withMaximumValue(); return getWorkableHours(init, end); } /** * Creates a new {@link BaseCalendar} derived from the current calendar. * The new calendar will be the child of the current calendar. * * @return The derived calendar */ public BaseCalendar newDerivedCalendar() { BaseCalendar derivedCalendar = create(); derivedCalendar.setParent(this); return derivedCalendar; } public ResourceCalendar newDerivedResourceCalendar() { ResourceCalendar derivedCalendar = ResourceCalendar.create(); derivedCalendar.setParent(this); return derivedCalendar; } /** * Creates a new version this {@link BaseCalendar} from the specific date. * It makes that the current calendar expires in the specific date. * And the new calendar will be used from that date onwards. * * @param date */ public void newVersion(LocalDate date) { BaseCalendar lastParent = null; if (getLastCalendarData() != null) { lastParent = getLastCalendarData().getParent(); } CalendarData newCalendarData = createLastVersion(date); newCalendarData.setParent(lastParent); } public CalendarData createNewVersionInsideIntersection(LocalDate startDate, LocalDate expiringDate) { for (CalendarData nextVersion : calendarDataVersions) { if ((nextVersion.getExpiringDate() == null) || (expiringDate.compareTo(nextVersion.getExpiringDate()) <= 0)) { int index = calendarDataVersions.indexOf(nextVersion); if (index > 0) { CalendarData prevVersion = calendarDataVersions.get(index - 1); if (newIntervalIncludeAnotherWorkWeek(startDate, expiringDate, prevVersion, nextVersion)) { throw new IllegalArgumentException("the new work week includes a whole work week already exists"); } else { LocalDate prevExpiringDate = prevVersion.getExpiringDate(); LocalDate nextExpiringDate = nextVersion.getExpiringDate(); BaseCalendar oldParent = nextVersion.getParent(); if ((prevExpiringDate == null) || (startDate.compareTo(prevExpiringDate) > 0)) { CalendarData prevCalendarData = CalendarData.create(); prevCalendarData.setExpiringDate(startDate); prevCalendarData.setParent(oldParent); resetDefaultCapacities(prevCalendarData); calendarDataVersions.add(prevCalendarData); } else { prevVersion.setExpiringDate(startDate); } CalendarData newCalendarData = CalendarData.create(); newCalendarData.setExpiringDate(expiringDate); calendarDataVersions.add(newCalendarData); if ((nextExpiringDate != null) && (expiringDate.compareTo(nextExpiringDate) >= 0)) { calendarDataVersions.remove(nextVersion); } Collections.sort(calendarDataVersions, CalendarData.BY_EXPIRING_DATE_COMPARATOR); return newCalendarData; } } else { throw new IllegalArgumentException( "Wrong start date : the new work week will be the first one, and the start date must be empty"); } } } throw new IllegalArgumentException( "Wrong expiring date : the new work week will be the last one, and the expiring date must be empty"); } public boolean newIntervalIncludeAnotherWorkWeek(LocalDate startDate, LocalDate expiringDate, CalendarData prevVersion, CalendarData nextVersion) { if ((startDate.compareTo(prevVersion.getExpiringDate()) <= 0) && (nextVersion.getExpiringDate() != null) && (expiringDate.compareTo(nextVersion.getExpiringDate()) >= 0)) { return true; } int indexPrevOfPrev = calendarDataVersions.indexOf(prevVersion); if (indexPrevOfPrev > 0) { CalendarData prevOfPrev = calendarDataVersions.get(indexPrevOfPrev - 1); if (startDate.compareTo(prevOfPrev.getExpiringDate()) <= 0) { return true; } } return false; } public CalendarData createLastVersion(LocalDate startDate) { CalendarData calendarData = getCalendarDataBeforeTheLastIfAny(); if ((calendarData.getExpiringDate() != null) && (startDate.compareTo(calendarData.getExpiringDate()) <= 0)) { throw new IllegalArgumentException( "Wrong start date : the new work week includes a whole work week already exists"); } getLastCalendarData().setExpiringDate(startDate); CalendarData newCalendarData = CalendarData.create(); calendarDataVersions.add(newCalendarData); Collections.sort(calendarDataVersions, CalendarData.BY_EXPIRING_DATE_COMPARATOR); return newCalendarData; } public CalendarData createFirstVersion(LocalDate expiringDate) { CalendarData firstVersion = getFirstCalendarData(); if ((firstVersion.getExpiringDate() != null) && (expiringDate.compareTo(firstVersion.getExpiringDate()) >= 0)) { throw new IllegalArgumentException( "Wrong expiring date : Work week expiring date must be lower than expiring date for " + "all work weeks of this calendar"); } CalendarData newCalendarData = CalendarData.create(); newCalendarData.setExpiringDate(expiringDate); calendarDataVersions.add(newCalendarData); Collections.sort(calendarDataVersions, CalendarData.BY_EXPIRING_DATE_COMPARATOR); return newCalendarData; } public void newVersion(LocalDate startDate, LocalDate expiringDate, BaseCalendar parent) { CalendarData newCalendarData; if (startDate != null && expiringDate != null) { if (startDate.compareTo(expiringDate) > 0) { throw new IllegalArgumentException("the start date must be lower than expiring date"); } if (calendarDataVersions.size() == 1) { BaseCalendar lastParent = getLastCalendarData().getParent(); newCalendarData = createLastVersion(startDate); CalendarData newLastVersion = createLastVersion(expiringDate); newLastVersion.setParent(lastParent); resetDefaultCapacities(newLastVersion); } else { newCalendarData = createNewVersionInsideIntersection(startDate, expiringDate); } } else if (startDate != null) { newCalendarData = createLastVersion(startDate); } else if (expiringDate != null) { newCalendarData = createFirstVersion(expiringDate); } else { throw new IllegalArgumentException("At least the start date must be specified"); } if (parent != null) { newCalendarData.setParent(parent); } else { newCalendarData.setParent(getLastCalendarData().getParent()); } resetDefaultCapacities(newCalendarData); } private void resetDefaultCapacities(CalendarData version){ if(version.getParent() == null){ CalendarData.resetDefaultCapacities(version); } } public void addNewVersion(CalendarData version){ if (version.getExpiringDate() == null) { if (getLastCalendarData().getExpiringDate() == null) { throw new IllegalArgumentException("the date is null and overlaps with the last work week."); } else{ calendarDataVersions.add(version); Collections.sort(calendarDataVersions, CalendarData.BY_EXPIRING_DATE_COMPARATOR); return; } } if (version.getExpiringDate().compareTo(new LocalDate()) <= 0) { throw new IllegalArgumentException( "You can not add a work week with previous date than current date"); } for (int i = 0; i < calendarDataVersions.size(); i++) { if ( (calendarDataVersions.get(i).getExpiringDate() == null) || (calendarDataVersions.get(i).getExpiringDate().compareTo(version.getExpiringDate()) > 0) ) { if ( (i - 1 >= 0) && (calendarDataVersions.get(i - 1).getExpiringDate() != null) && (calendarDataVersions.get(i - 1).getExpiringDate().compareTo(version.getExpiringDate()) >= 0) ) { throw new IllegalArgumentException("the date is null and overlap with the other work week."); } calendarDataVersions.add(i, version); return; } } calendarDataVersions.add(version); Collections.sort(calendarDataVersions, CalendarData.BY_EXPIRING_DATE_COMPARATOR); } public BaseCalendar newCopy() { BaseCalendar copy = create(); copyFields(copy); return copy; } private void copyFields(BaseCalendar copy) { copy.name = this.name; copy.setCodeAutogenerated(this.isCodeAutogenerated()); copy.calendarDataVersions = new ArrayList<>(); for (CalendarData calendarData : this.calendarDataVersions) { copy.calendarDataVersions.add(calendarData.copy()); } copy.exceptions = new HashSet<>(this.exceptions); } public BaseCalendar newCopyResourceCalendar() { BaseCalendar copy = ResourceCalendar.create(); copyFields(copy); return copy; } public List<CalendarData> getCalendarDataVersions() { return Collections.unmodifiableList(calendarDataVersions); } public CalendarData getCalendarData(LocalDate date) { for (CalendarData calendarData : calendarDataVersions) { if (calendarData.getExpiringDate() == null) { return calendarData; } else { if (date.compareTo(calendarData.getExpiringDate()) < 0) { return calendarData; } } } throw new RuntimeException("Some work week should not be expired"); } public CalendarData getLastCalendarData() { if (calendarDataVersions.isEmpty()) { return null; } return calendarDataVersions.get(calendarDataVersions.size() - 1); } public CalendarData getFirstCalendarData() { if (calendarDataVersions.isEmpty()) { return null; } return calendarDataVersions.get(0); } public void setCapacityAt(Days day, Capacity capacity) { CalendarData calendarData = getLastCalendarData(); calendarData.setCapacityAt(day, capacity); } public void setCapacityAt(Days day, Capacity capacity, LocalDate date) { CalendarData calendarData = getCalendarData(date); calendarData.setCapacityAt(day, capacity); } private CalendarData getCalendarDataBeforeTheLastIfAny() { if (calendarDataVersions.size() <= 1) { return getLastCalendarData(); } return calendarDataVersions.get(calendarDataVersions.size() - 2); } public boolean isDefault(Days day) { CalendarData calendarData = getLastCalendarData(); return calendarData.isDefault(day); } public boolean isDefault(Days day, LocalDate date) { CalendarData calendarData = getCalendarData(date); return calendarData.isDefault(day); } public void setDefault(Days day) { CalendarData calendarData = getLastCalendarData(); calendarData.setDefault(day); } public void setDefault(Days day, LocalDate date) { CalendarData calendarData = getCalendarData(date); calendarData.setDefault(day); } public LocalDate getExpiringDate() { return getLastCalendarData().getExpiringDate(); } public LocalDate getExpiringDate(LocalDate date) { return getCalendarData(date).getExpiringDate(); } public void setExpiringDate(LocalDate expiringDate) { setExpiringDate(expiringDate, new LocalDate()); } public void setExpiringDate(LocalDate expiringDate, LocalDate date) { CalendarData calendarData = getCalendarData(date); setExpiringDate(calendarData, expiringDate); } private void setExpiringDate(CalendarData calendarData, LocalDate expiringDate) { if (calendarData.getExpiringDate() == null) { throw new IllegalArgumentException("Can not set the expiring date because of this is the last work week"); } Integer index = calendarDataVersions.indexOf(calendarData); if (index > 0) { CalendarData previousCalendarData = calendarDataVersions.get(index - 1); if (expiringDate.compareTo(previousCalendarData.getExpiringDate()) <= 0) { throw new IllegalArgumentException("This date must be greater than expiring date of previous calendars"); } } calendarData.setExpiringDate(expiringDate); } private CalendarData getPreviousCalendarData(LocalDate date) { CalendarData calendarData = getCalendarData(date); return getPrevious(calendarData); } public CalendarData getPrevious(CalendarData calendarData) { Integer index = calendarDataVersions.indexOf(calendarData) - 1; if (index < 0) { return null; } return calendarDataVersions.get(index); } public LocalDate getValidFrom(LocalDate date) { CalendarData calendarData = getPreviousCalendarData(date); if (calendarData == null) { return null; } return calendarData.getExpiringDate(); } public void setValidFrom(LocalDate validFromDate, LocalDate date) { CalendarData calendarData = getPreviousCalendarData(date); if (calendarData == null) { throw new IllegalArgumentException("You can not set this date for the first work week"); } setExpiringDate(calendarData, validFromDate); } public boolean isLastVersion(LocalDate date) { CalendarData calendarData = getCalendarData(date); Integer index = calendarDataVersions.indexOf(calendarData); return index == (calendarDataVersions.size() - 1); } public boolean isFirstVersion(LocalDate date) { CalendarData calendarData = getCalendarData(date); Integer index = calendarDataVersions.indexOf(calendarData); return index == 0; } /** * Returns a set of non workable days (0 hours) for a specific period depending on the calendar restrictions. * * @param init * @param end * * @return Set of locate date */ public Set<LocalDate> getNonWorkableDays(LocalDate init, LocalDate end) { Set<LocalDate> result = new HashSet<>(); for (LocalDate current = init; current.compareTo(end) <= 0; current = current.plusDays(1)) { if (getCapacityOn(PartialDay.wholeDay(current)).isZero()) { result.add(current); } } return result; } public CalendarExceptionType getExceptionType(LocalDate date) { CalendarException exceptionDay = getExceptionDay(date); if (exceptionDay == null) { return null; } return exceptionDay.getType(); } public void removeCalendarData(CalendarData calendarData) { if (this.getCalendarDataVersions().size() <= 1) { throw new IllegalArgumentException("You can not remove the last calendar data"); } CalendarData lastCalendarData = getLastCalendarData(); if (calendarData.equals(lastCalendarData)) { calendarDataVersions.remove(calendarData); getLastCalendarData().removeExpiringDate(); } else { calendarDataVersions.remove(calendarData); } } public LocalDate getValidFrom(CalendarData calendarData) { Integer index = calendarDataVersions.indexOf(calendarData); if (index > 0) { return calendarDataVersions.get(index - 1).getExpiringDate(); } return null; } public List<CalendarAvailability> getCalendarAvailabilities() { return Collections.unmodifiableList(calendarAvailabilities); } /** * Returns a a copy of calendar availabilities sorted by start date. * calendarAvailabilities should already be sorted by start date, this method is just for extra safety. * * @return List of calendar availability */ private List<CalendarAvailability> getCalendarAvailabilitiesSortedByStartDate() { List<CalendarAvailability> result = new ArrayList<>(calendarAvailabilities); Collections.sort(result, CalendarAvailability.BY_START_DATE_COMPARATOR); return result; } public void addNewCalendarAvailability(CalendarAvailability calendarAvailability) { if (this instanceof ResourceCalendar) { if (!calendarAvailabilities.isEmpty()) { CalendarAvailability lastCalendarAvailability = getLastCalendarAvailability(); if (lastCalendarAvailability != null) { if (lastCalendarAvailability.getEndDate() == null) { if (lastCalendarAvailability.getStartDate().compareTo(calendarAvailability.getStartDate()) >= 0) { throw new IllegalArgumentException( "New calendar availability should start after the last calendar availability"); } } else { if (lastCalendarAvailability.getEndDate().compareTo(calendarAvailability.getStartDate()) >= 0) { throw new IllegalArgumentException( "New calendar availability should start after the last calendar availability"); } } lastCalendarAvailability.setEndDate(calendarAvailability.getStartDate().minusDays(1)); } } calendarAvailabilities.add(calendarAvailability); } } public void removeCalendarAvailability(CalendarAvailability calendarAvailability) { calendarAvailabilities.remove(calendarAvailability); } public boolean isActive(LocalDate date) { if (getCalendarAvailabilities().isEmpty()) { return true; } for (CalendarAvailability calendarAvailability : getCalendarAvailabilities()) { if (calendarAvailability.isActive(date)) { return true; } } return false; } public boolean isActiveBetween(LocalDate startDate, LocalDate endDate) { if (getCalendarAvailabilities().isEmpty()) { return true; } for (CalendarAvailability calendarAvailability : getCalendarAvailabilities()) { if (calendarAvailability.isActiveBetween(startDate, endDate)) { return true; } } return false; } public boolean canWorkOn(LocalDate date) { Capacity capacity = findCapacityAt(date); return capacity.allowsWorking(); } public CalendarAvailability getLastCalendarAvailability() { if (calendarAvailabilities.isEmpty()) { return null; } // Sorting for ensuring the last one is picked. // In theory sorting would not be necessary, doing it for safety. List<CalendarAvailability> sorted = getCalendarAvailabilitiesSortedByStartDate(); return sorted.get(sorted.size() - 1); } public CalendarAvailability getFistCalendarAvailability() { if (calendarAvailabilities.isEmpty()) { return null; } // Sorting for ensuring the first one is picked. // In theory sorting would not be necessary, doing it for safety. List<CalendarAvailability> sorted = getCalendarAvailabilitiesSortedByStartDate(); return sorted.get(0); } public boolean isLastCalendarAvailability(CalendarAvailability calendarAvailability) { if (getLastCalendarAvailability() == null || calendarAvailability == null) { return false; } if (getLastCalendarAvailability().getId() == null && calendarAvailability.getId() == null) { return getLastCalendarAvailability() == calendarAvailability; } return Objects.equals(getLastCalendarAvailability().getId(), calendarAvailability.getId()); } public void setStartDate(CalendarAvailability calendarAvailability, LocalDate startDate) { int index = calendarAvailabilities.indexOf(calendarAvailability); if (index > 0 && calendarAvailabilities.get(index - 1).getEndDate().compareTo(startDate) >= 0) { throw new IllegalArgumentException("Start date could not overlap previous calendar availability"); } calendarAvailability.setStartDate(startDate); } public void setEndDate(CalendarAvailability calendarAvailability, LocalDate endDate) { int index = calendarAvailabilities.indexOf(calendarAvailability); if (index < (calendarAvailabilities.size() - 1) && calendarAvailabilities.get(index + 1).getStartDate().compareTo(endDate) <= 0) { throw new IllegalArgumentException("End date could not overlap next calendar availability"); } calendarAvailability.setEndDate(endDate); } @Override public EffortDuration asDurationOn(PartialDay day, ResourcesPerDay amount) { Capacity capacity = findCapacityAt(day.getDate()); EffortDuration oneResourcePerDayWorkingDuration = day.limitWorkingDay(capacity.getStandardEffort()); EffortDuration amountRequestedDuration = amount.asDurationGivenWorkingDayOf(oneResourcePerDayWorkingDuration); EffortDuration duration = multiplyByCalendarUnits(capacity).limitDuration(amountRequestedDuration); return duration.atNearestMinute(); } /** * <p> * Calendar units are the number of units this calendar is applied to. * For example a {@link VirtualWorker} composed of ten workers would multiply the capacity by ten. * </p> * <p> * This method is intended to be overridden. * </p> * */ protected Capacity multiplyByCalendarUnits(Capacity capacity) { return capacity; } @Override public boolean thereAreCapacityFor(AvailabilityTimeLine availability, ResourcesPerDay resourcesPerDay, EffortDuration durationToAllocate) { return ThereAreHoursOnWorkHoursCalculator .thereIsAvailableCapacityFor(this, availability, resourcesPerDay, durationToAllocate) .thereIsCapacityAvailable(); } public boolean onlyGivesZeroHours() { return lastDataDoesNotGiveOnlyZeros(); } public boolean lastDataDoesNotGiveOnlyZeros() { CalendarData last = lastCalendarData(); return last.isEmpty(); } private CalendarData lastCalendarData() { return calendarDataVersions.get(calendarDataVersions.size() - 1); } public boolean onlyGivesZeroHours(Days day) { for (CalendarData each : calendarDataVersions) { if (!each.isEmptyFor(day)) { return false; } } return true; } @Override public AvailabilityTimeLine getAvailability() { AvailabilityTimeLine result = AvailabilityTimeLine.allValid(); addInvaliditiesDerivedFromCalendar(result); return result; } private void addInvaliditiesDerivedFromCalendar(AvailabilityTimeLine result) { addInvaliditiesFromAvailabilities(result); addInvaliditiesFromExceptions(result); addInvaliditiesFromEmptyCalendarData(result); addInvaliditiesFromEmptyDaysInCalendarData(result); } private void addInvaliditiesFromEmptyDaysInCalendarData(AvailabilityTimeLine result) { result.setVetoer(new IVetoer() { @Override public boolean isValid(LocalDate date) { return canWorkOn(date); } }); } private void addInvaliditiesFromEmptyCalendarData(AvailabilityTimeLine result) { LocalDate previous = null; for (CalendarData each : calendarDataVersions) { addInvalidityIfDataEmpty(result, previous, each); previous = each.getExpiringDate(); } } private void addInvalidityIfDataEmpty(AvailabilityTimeLine result, LocalDate previous, CalendarData each) { if (!each.isEmpty()) { return; } final boolean hasExpiringDate = each.getExpiringDate() != null; if (previous == null && hasExpiringDate) { result.invalidUntil(each.getExpiringDate()); } else if (previous == null) { result.allInvalid(); } else if (hasExpiringDate) { result.invalidAt(previous, each.getExpiringDate()); } else { result.invalidFrom(previous); } } private void addInvaliditiesFromAvailabilities(AvailabilityTimeLine timeLine) { if (calendarAvailabilities.isEmpty()) { return; } List<CalendarAvailability> availabilities = getCalendarAvailabilitiesSortedByStartDate(); CalendarAvailability previous = null; for (CalendarAvailability each : availabilities) { final boolean isFirstOne = previous == null; if (isFirstOne) { timeLine.invalidUntil(each.getStartDate()); } else { // CalendarAvailability's end is inclusive LocalDate startOfInvalidPeriod = previous.getEndDate().plusDays(1); timeLine.invalidAt(startOfInvalidPeriod, each.getStartDate()); } previous = each; } final CalendarAvailability last = previous; if (last != null && last.getEndDate() != null) { // CalendarAvailability's end is inclusive timeLine.invalidFrom(last.getEndDate().plusDays(1)); } } private void addInvaliditiesFromExceptions(AvailabilityTimeLine timeLine) { for (CalendarException each : getExceptions()) { if (!each.getCapacity().allowsWorking()) { timeLine.invalidAt(each.getDate()); } } } @Override protected IBaseCalendarDAO getIntegrationEntityDAO() { return org.libreplan.business.common.Registry.getBaseCalendarDAO(); } @SuppressWarnings("unused") @AssertTrue(message = "dates must be sorted and cannot overlap") public boolean isDateCouldNotOverlapConstraint() { if (calendarDataVersions == null || calendarDataVersions.isEmpty()) { return true; } if (this.getLastCalendarData().getExpiringDate() != null) { return false; } for (int i = 0; i < calendarDataVersions.size() - 2; i++) { LocalDate date1 = calendarDataVersions.get(i).getExpiringDate(); LocalDate date2 = calendarDataVersions.get(i + 1).getExpiringDate(); if ((date1 == null) || (date2 == null) || (date1.compareTo(date2) >= 0)) { return false; } } return true; } public CalendarException getCalendarExceptionByCode(String code) throws InstanceNotFoundException { if (StringUtils.isBlank(code)) { throw new InstanceNotFoundException(code, CalendarException.class.getName()); } for (CalendarException e : this.exceptions) { if (e.getCode().equalsIgnoreCase(StringUtils.trim(code))) { return e; } } throw new InstanceNotFoundException(code, CalendarException.class.getName()); } public CalendarData getCalendarDataByCode(String code) throws InstanceNotFoundException { if (StringUtils.isBlank(code)) { throw new InstanceNotFoundException(code, CalendarData.class.getName()); } for (CalendarData e : this.calendarDataVersions) { if (e.getCode().equalsIgnoreCase(StringUtils.trim(code))) { return e; } } throw new InstanceNotFoundException(code, CalendarData.class.getName()); } public void generateCalendarExceptionCodes(int numberOfDigits) { for (CalendarException exception : this.getExceptions()) { if ((exception.getCode() == null) || (exception.getCode().isEmpty()) || (!exception.getCode().startsWith(this.getCode()))) { this.incrementLastSequenceCode(); String exceptionCode = EntitySequence.formatValue(numberOfDigits, this.getLastSequenceCode()); exception.setCode(this.getCode() + EntitySequence.CODE_SEPARATOR_CHILDREN + exceptionCode); } } for (CalendarData data : this.getCalendarDataVersions()) { if ((data.getCode() == null) || (data.getCode().isEmpty()) || (!data.getCode().startsWith(this.getCode()))) { this.incrementLastSequenceCode(); String dataCode = EntitySequence.formatValue(numberOfDigits, this.getLastSequenceCode()); data.setCode(this.getCode() + EntitySequence.CODE_SEPARATOR_CHILDREN + dataCode); } } for (CalendarAvailability availability : this.getCalendarAvailabilities()) { if ((availability.getCode() == null) || (availability.getCode().isEmpty()) || (!availability.getCode().startsWith(this.getCode()))) { this.incrementLastSequenceCode(); String availabilityCode = EntitySequence.formatValue(numberOfDigits, this.getLastSequenceCode()); availability.setCode(this.getCode() + EntitySequence.CODE_SEPARATOR_CHILDREN + availabilityCode); } } } public void incrementLastSequenceCode() { if (lastSequenceCode == null) { lastSequenceCode = 0; } lastSequenceCode++; } @NotNull(message = "last sequence code not specified") public Integer getLastSequenceCode() { return lastSequenceCode; } @AssertTrue(message = "calendars with zero hours are not allowed") public boolean isZeroHoursConstraint() { if ((calendarDataVersions != null) && (!calendarDataVersions.isEmpty())) { for (CalendarData each : calendarDataVersions) { if (!each.isEmpty()) { return true; } } } return false; } @Override public String getHumanId() { return name; } @Override public int compareTo(BaseCalendar calendar) { return this.getName().compareToIgnoreCase(calendar.getName()); } }