From b916d577ca62f4b184ecbe0dc19752f77b6e53dd Mon Sep 17 00:00:00 2001 From: Mustapha Zorgati <15628173+mustaphazorgati@users.noreply.github.com> Date: Fri, 10 Mar 2023 14:44:11 +0100 Subject: [PATCH] TSK-1972: Uses time zone for working time calculations --- .../TaskanaConfigurationInitializer.java | 14 ++ .../WorkingTimeCalculatorImpl.java | 159 ++++++++++-------- .../WorkingTimeCalculatorImplTest.java | 114 ++++++++++++- .../src/test/resources/taskana.properties | 2 +- .../acceptance/TaskanaConfigurationTest.java | 7 + .../pro/taskana/TaskanaConfiguration.java | 47 ++---- .../common/internal/TaskanaEngineImpl.java | 4 +- .../WorkingDaysToDaysReportConverterTest.java | 4 +- .../src/test/resources/taskana.properties | 1 + 9 files changed, 248 insertions(+), 104 deletions(-) diff --git a/common/taskana-common/src/main/java/pro/taskana/common/internal/configuration/TaskanaConfigurationInitializer.java b/common/taskana-common/src/main/java/pro/taskana/common/internal/configuration/TaskanaConfigurationInitializer.java index 6bd443448..763e28e61 100644 --- a/common/taskana-common/src/main/java/pro/taskana/common/internal/configuration/TaskanaConfigurationInitializer.java +++ b/common/taskana-common/src/main/java/pro/taskana/common/internal/configuration/TaskanaConfigurationInitializer.java @@ -8,6 +8,7 @@ import java.lang.reflect.Type; import java.time.Duration; import java.time.Instant; import java.time.LocalTime; +import java.time.ZoneId; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -52,6 +53,7 @@ public class TaskanaConfigurationInitializer { PROPERTY_INITIALIZER_BY_CLASS.put(Instant.class, new InstantPropertyParser()); PROPERTY_INITIALIZER_BY_CLASS.put(List.class, new ListPropertyParser()); PROPERTY_INITIALIZER_BY_CLASS.put(Map.class, new MapPropertyParser()); + PROPERTY_INITIALIZER_BY_CLASS.put(ZoneId.class, new ZoneIdPropertyParser()); PROPERTY_INITIALIZER_BY_CLASS.put(Enum.class, new EnumPropertyParser()); } @@ -432,6 +434,18 @@ public class TaskanaConfigurationInitializer { } } + static class ZoneIdPropertyParser implements PropertyParser { + + @Override + public Optional initialize( + Map properties, + String separator, + Field field, + TaskanaProperty taskanaProperty) { + return parseProperty(properties, taskanaProperty.value(), ZoneId::of); + } + } + static class EnumPropertyParser implements PropertyParser> { @Override public Optional> initialize( diff --git a/common/taskana-common/src/main/java/pro/taskana/common/internal/workingtime/WorkingTimeCalculatorImpl.java b/common/taskana-common/src/main/java/pro/taskana/common/internal/workingtime/WorkingTimeCalculatorImpl.java index 909370fe1..058a5066c 100644 --- a/common/taskana-common/src/main/java/pro/taskana/common/internal/workingtime/WorkingTimeCalculatorImpl.java +++ b/common/taskana-common/src/main/java/pro/taskana/common/internal/workingtime/WorkingTimeCalculatorImpl.java @@ -6,8 +6,10 @@ import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; -import java.time.ZoneOffset; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.SortedSet; @@ -18,31 +20,36 @@ import pro.taskana.common.api.exceptions.InvalidArgumentException; public class WorkingTimeCalculatorImpl implements WorkingTimeCalculator { - static final ZoneOffset UTC = ZoneOffset.UTC; + private final ZoneId zoneId; private final HolidaySchedule holidaySchedule; private final WorkingTimeSchedule workingTimeSchedule; public WorkingTimeCalculatorImpl( - HolidaySchedule holidaySchedule, Map> workingTimeSchedule) { + HolidaySchedule holidaySchedule, + Map> workingTimeSchedule, + ZoneId zoneId) { this.holidaySchedule = holidaySchedule; this.workingTimeSchedule = new WorkingTimeSchedule(workingTimeSchedule); + this.zoneId = Objects.requireNonNull(zoneId); } @Override public Instant subtractWorkingTime(Instant workStart, Duration workingTime) throws InvalidArgumentException { validatePositiveDuration(workingTime); - WorkSlot workSlot = getWorkSlotOrPrevious(toLocalDateTime(workStart)); - return workSlot.subtractWorkingTime(workStart, workingTime); + ZonedDateTime workStartInTimeZone = toZonedDateTime(workStart); + WorkSlot workSlot = getWorkSlotOrPrevious(workStartInTimeZone); + return workSlot.subtractWorkingTime(workStartInTimeZone, workingTime).toInstant(); } @Override public Instant addWorkingTime(Instant workStart, Duration workingTime) throws InvalidArgumentException { validatePositiveDuration(workingTime); - WorkSlot bestMatchingWorkSlot = getWorkSlotOrNext(toLocalDateTime(workStart)); - return bestMatchingWorkSlot.addWorkingTime(workStart, workingTime); + ZonedDateTime workStartInTimeZone = toZonedDateTime(workStart); + WorkSlot workSlot = getWorkSlotOrNext(workStartInTimeZone); + return workSlot.addWorkingTime(workStartInTimeZone, workingTime).toInstant(); } @Override @@ -60,24 +67,7 @@ public class WorkingTimeCalculatorImpl implements WorkingTimeCalculator { to = second; } - WorkSlot bestMatchingWorkSlot = getWorkSlotOrNext(toLocalDateTime(from)); - Instant earliestWorkStart = max(from, bestMatchingWorkSlot.start); - Instant endOfWorkSlot = bestMatchingWorkSlot.end; - - if (endOfWorkSlot.compareTo(to) >= 0) { - if (bestMatchingWorkSlot.start.compareTo(to) <= 0) { - // easy part. _from_ and _to_ are in the same work slot - return Duration.between(earliestWorkStart, to); - } else { - // _from_ and _to_ are before the bestMatchingWorkSlot aka between two work slots. We simply - // drop it - return Duration.ZERO; - } - } else { - // Take the current duration and add the working time starting after this work slot. - return Duration.between(earliestWorkStart, endOfWorkSlot) - .plus(workingTimeBetween(endOfWorkSlot, to)); - } + return calculateWorkingTime(toZonedDateTime(from), toZonedDateTime(to)); } @Override @@ -101,6 +91,29 @@ public class WorkingTimeCalculatorImpl implements WorkingTimeCalculator { return holidaySchedule.isGermanHoliday(toLocalDate(instant)); } + private Duration calculateWorkingTime(ZonedDateTime from, ZonedDateTime to) + throws InvalidArgumentException { + + WorkSlot bestMatchingWorkSlot = getWorkSlotOrNext(from); + ZonedDateTime earliestWorkStart = max(from, bestMatchingWorkSlot.start); + ZonedDateTime endOfWorkSlot = bestMatchingWorkSlot.end; + + if (endOfWorkSlot.compareTo(to) >= 0) { + if (bestMatchingWorkSlot.start.compareTo(to) <= 0) { + // easy part. _from_ and _to_ are in the same work slot + return Duration.between(earliestWorkStart, to); + } else { + // _from_ and _to_ are before the bestMatchingWorkSlot aka between two work slots. We simply + // drop it + return Duration.ZERO; + } + } else { + // Take the current duration and add the working time starting after this work slot. + return Duration.between(earliestWorkStart, endOfWorkSlot) + .plus(calculateWorkingTime(endOfWorkSlot, to)); + } + } + private void validateNonNullInstants(Instant first, Instant second) { if (first == null || second == null) { throw new InvalidArgumentException("Neither first nor second may be null."); @@ -118,11 +131,11 @@ public class WorkingTimeCalculatorImpl implements WorkingTimeCalculator { * within a WorkSlot that WorkSlot is returned, if currentDateTime is not within a WorkSlot the * next WorkSlot is returned. * - * @param currentDateTime The LocalDateTime we want the best matching WorkSlot for. May not be + * @param currentDateTime The ZonedDateTime we want the best matching WorkSlot for. May not be * null. * @return The WorkSlot that matches best currentDateTime if we want to add. */ - private WorkSlot getWorkSlotOrNext(LocalDateTime currentDateTime) { + private WorkSlot getWorkSlotOrNext(ZonedDateTime currentDateTime) { LocalDate currentDate = currentDateTime.toLocalDate(); // We do not work on Holidays if (holidaySchedule.isHoliday(currentDate)) { @@ -149,11 +162,11 @@ public class WorkingTimeCalculatorImpl implements WorkingTimeCalculator { * within a WorkSlot that WorkSlot is returned, if currentDateTime is not within a WorkSlot the * previous WorkSlot is returned. * - * @param currentDateTime The LocalDateTime we want the best matching WorkSlot for. May not be + * @param currentDateTime The ZonedDateTime we want the best matching WorkSlot for. May not be * null. * @return The WorkSlot that matches best currentDateTime if we want to subtract. */ - private WorkSlot getWorkSlotOrPrevious(LocalDateTime currentDateTime) { + private WorkSlot getWorkSlotOrPrevious(ZonedDateTime currentDateTime) { LocalDate currentDate = currentDateTime.toLocalDate(); // We do not work on Holidays if (holidaySchedule.isHoliday(currentDate)) { @@ -177,31 +190,41 @@ public class WorkingTimeCalculatorImpl implements WorkingTimeCalculator { getWorkSlotOrPrevious(getDayBefore(currentDateTime))); } - private static boolean isBeforeOrEquals(LocalTime time, LocalDateTime currentDateTime) { + private static boolean isBeforeOrEquals(LocalTime time, ZonedDateTime currentDateTime) { return !time.isAfter(currentDateTime.toLocalTime()); } - private LocalDateTime getDayAfter(LocalDateTime current) { - return LocalDateTime.of(current.toLocalDate().plusDays(1), LocalTime.MIN); - } - - private LocalDateTime getDayBefore(LocalDateTime current) { - return LocalDateTime.of(current.toLocalDate().minusDays(1), LocalTime.MAX); - } - - private LocalDateTime toLocalDateTime(Instant instant) { - return LocalDateTime.ofInstant(instant, UTC); - } - - private LocalDate toLocalDate(Instant instant) { - return LocalDate.ofInstant(instant, UTC); + private ZonedDateTime getDayAfter(ZonedDateTime current) { + return LocalDateTime.of(current.toLocalDate().plusDays(1), LocalTime.MIN) + .atZone(current.getZone()); } private DayOfWeek toDayOfWeek(Instant instant) { return toLocalDate(instant).getDayOfWeek(); } - private static Instant max(Instant a, Instant b) { + private ZonedDateTime getDayBefore(ZonedDateTime current) { + return LocalDateTime.of(current.toLocalDate().minusDays(1), LocalTime.MAX) + .atZone(current.getZone()); + } + + private ZonedDateTime toZonedDateTime(Instant instant) { + return instant.atZone(zoneId); + } + + private ZonedDateTime toZonedDateTime(LocalDateTime localDateTime) { + return localDateTime.atZone(zoneId); + } + + private ZonedDateTime toZonedDateTime(LocalDate day, LocalTime time) { + return toZonedDateTime(LocalDateTime.of(day, time)); + } + + private LocalDate toLocalDate(Instant instant) { + return LocalDate.ofInstant(instant, zoneId); + } + + private static ZonedDateTime max(ZonedDateTime a, ZonedDateTime b) { if (a.isAfter(b)) { return a; } else { @@ -209,7 +232,7 @@ public class WorkingTimeCalculatorImpl implements WorkingTimeCalculator { } } - private static Instant min(Instant a, Instant b) { + private static ZonedDateTime min(ZonedDateTime a, ZonedDateTime b) { if (a.isBefore(b)) { return a; } else { @@ -219,21 +242,34 @@ public class WorkingTimeCalculatorImpl implements WorkingTimeCalculator { class WorkSlot { - private final Instant start; - private final Instant end; + private final ZonedDateTime start; + private final ZonedDateTime end; public WorkSlot(LocalDate day, LocalTimeInterval interval) { - this.start = LocalDateTime.of(day, interval.getBegin()).toInstant(UTC); + this.start = toZonedDateTime(day, interval.getBegin()); if (interval.getEnd().equals(LocalTime.MAX)) { - this.end = day.plusDays(1).atStartOfDay().toInstant(UTC); + this.end = toZonedDateTime(day.plusDays(1).atStartOfDay()); } else { - this.end = LocalDateTime.of(day, interval.getEnd()).toInstant(UTC); + this.end = toZonedDateTime(day, interval.getEnd()); } } - public Instant addWorkingTime(Instant workStart, Duration workingTime) { + private ZonedDateTime subtractWorkingTime(ZonedDateTime workStart, Duration workingTime) { + // _workStart_ might be outside the working hours. We need to adjust the end accordingly. + ZonedDateTime latestWorkEnd = min(workStart, end); + Duration untilStartOfWorkSlot = Duration.between(start, latestWorkEnd); + if (workingTime.compareTo(untilStartOfWorkSlot) <= 0) { + // easy part. It is due within the same work slot + return latestWorkEnd.minus(workingTime); + } else { + Duration remainingWorkingTime = workingTime.minus(untilStartOfWorkSlot); + return previous().subtractWorkingTime(start, remainingWorkingTime); + } + } + + private ZonedDateTime addWorkingTime(ZonedDateTime workStart, Duration workingTime) { // _workStart_ might be outside the working hours. We need to adjust the start accordingly. - Instant earliestWorkStart = max(workStart, start); + ZonedDateTime earliestWorkStart = max(workStart, start); Duration untilEndOfWorkSlot = Duration.between(earliestWorkStart, end); if (workingTime.compareTo(untilEndOfWorkSlot) <= 0) { // easy part. It is due within the same work slot @@ -247,26 +283,13 @@ public class WorkingTimeCalculatorImpl implements WorkingTimeCalculator { } } - public Instant subtractWorkingTime(Instant workStart, Duration workingTime) { - // _workStart_ might be outside the working hours. We need to adjust the end accordingly. - Instant latestWorkEnd = min(workStart, end); - Duration untilStartOfWorkSlot = Duration.between(start, latestWorkEnd); - if (workingTime.compareTo(untilStartOfWorkSlot) <= 0) { - // easy part. It is due within the same work slot - return latestWorkEnd.minus(workingTime); - } else { - Duration remainingWorkingTime = workingTime.minus(untilStartOfWorkSlot); - return previous().subtractWorkingTime(start, remainingWorkingTime); - } - } - private WorkSlot previous() { // We need to subtract a nanosecond because start is inclusive - return getWorkSlotOrPrevious(toLocalDateTime(start.minusNanos(1))); + return getWorkSlotOrPrevious(start.minusNanos(1)); } private WorkSlot next() { - return getWorkSlotOrNext(toLocalDateTime(end)); + return getWorkSlotOrNext(end); } } } diff --git a/common/taskana-common/src/test/java/pro/taskana/common/internal/workingtime/WorkingTimeCalculatorImplTest.java b/common/taskana-common/src/test/java/pro/taskana/common/internal/workingtime/WorkingTimeCalculatorImplTest.java index c2b2d2924..49aa86b7d 100644 --- a/common/taskana-common/src/test/java/pro/taskana/common/internal/workingtime/WorkingTimeCalculatorImplTest.java +++ b/common/taskana-common/src/test/java/pro/taskana/common/internal/workingtime/WorkingTimeCalculatorImplTest.java @@ -6,7 +6,11 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import java.time.DayOfWeek; import java.time.Duration; import java.time.Instant; +import java.time.LocalDateTime; import java.time.LocalTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; import java.util.Map; import java.util.Set; import org.junit.jupiter.api.Nested; @@ -33,7 +37,8 @@ class WorkingTimeCalculatorImplTest { DayOfWeek.TUESDAY, standardWorkingSlots, DayOfWeek.WEDNESDAY, standardWorkingSlots, DayOfWeek.THURSDAY, standardWorkingSlots, - DayOfWeek.FRIDAY, standardWorkingSlots)); + DayOfWeek.FRIDAY, standardWorkingSlots), + ZoneOffset.UTC); @Nested class WorkingTimeAddition { @@ -382,7 +387,8 @@ class WorkingTimeCalculatorImplTest { DayOfWeek.TUESDAY, standardWorkday, DayOfWeek.WEDNESDAY, standardWorkday, DayOfWeek.THURSDAY, standardWorkday, - DayOfWeek.FRIDAY, standardWorkday)); + DayOfWeek.FRIDAY, standardWorkday), + ZoneOffset.UTC); @Test void addTimeToMatchEndOfFirstAndStartOfSecondSlot() { @@ -417,7 +423,8 @@ class WorkingTimeCalculatorImplTest { DayOfWeek.TUESDAY, completeWorkDay, DayOfWeek.WEDNESDAY, completeWorkDay, DayOfWeek.THURSDAY, completeWorkDay, - DayOfWeek.FRIDAY, completeWorkDay)); + DayOfWeek.FRIDAY, completeWorkDay), + ZoneOffset.UTC); @Test void withDurationOfZeroOnHolySaturday() { @@ -463,7 +470,8 @@ class WorkingTimeCalculatorImplTest { private final WorkingTimeCalculator cut = new WorkingTimeCalculatorImpl( new HolidaySchedule(true, false), - Map.of(DayOfWeek.SUNDAY, Set.of(new LocalTimeInterval(LocalTime.MIN, LocalTime.MAX)))); + Map.of(DayOfWeek.SUNDAY, Set.of(new LocalTimeInterval(LocalTime.MIN, LocalTime.MAX))), + ZoneOffset.UTC); @Test void returnsTrueIfWorkingTimeScheduleIsDefinedForDayOfWeek() { @@ -498,4 +506,102 @@ class WorkingTimeCalculatorImplTest { .isThrownBy(() -> cut.isWorkingDay(null)); } } + + @Nested + class WorkingTimeWithNonUtcTimeZoneAcrossDaylightSavingTimeSwitch { + + private final ZoneId cet = ZoneId.of("Europe/Berlin"); + private final Set completeWorkDay = + Set.of(new LocalTimeInterval(LocalTime.MIN, LocalTime.MAX)); + + private final WorkingTimeCalculator cut = + new WorkingTimeCalculatorImpl( + new HolidaySchedule(true, false), + Map.of( + DayOfWeek.MONDAY, completeWorkDay, + DayOfWeek.TUESDAY, completeWorkDay, + DayOfWeek.WEDNESDAY, completeWorkDay, + DayOfWeek.THURSDAY, completeWorkDay, + DayOfWeek.FRIDAY, completeWorkDay), + cet); + + @Test + void addsWorkingTimeCorrectly() { + Instant fridayBefore = + ZonedDateTime.of(LocalDateTime.parse("2022-03-25T09:30:00"), cet).toInstant(); + + Instant dueDate = cut.addWorkingTime(fridayBefore, Duration.ofHours(17)); + + assertThat(dueDate) + .isEqualTo(ZonedDateTime.of(LocalDateTime.parse("2022-03-28T02:30:00"), cet).toInstant()); + } + + @Test + void subtractsWorkingTimeCorrectly() { + Instant mondayAfter = + ZonedDateTime.of(LocalDateTime.parse("2022-10-31T08:54:00"), cet).toInstant(); + + Instant dueDate = cut.subtractWorkingTime(mondayAfter, Duration.ofHours(18)); + + assertThat(dueDate) + .isEqualTo(ZonedDateTime.of(LocalDateTime.parse("2022-10-28T14:54:00"), cet).toInstant()); + } + + @Test + void calculatesWorkingTimeBetweenCorrectly() { + Instant fridayBefore = + ZonedDateTime.of(LocalDateTime.parse("2023-03-24T09:30:00"), cet).toInstant(); + Instant mondayAfter = + ZonedDateTime.of(LocalDateTime.parse("2023-03-27T02:30:00"), cet).toInstant(); + + Duration duration = cut.workingTimeBetween(fridayBefore, mondayAfter); + + assertThat(duration).isEqualTo(Duration.ofHours(17)); + } + } + + @Nested + class WorkingTimeWithWorkSlotsSpanningAcrossDaylightSavingTimeSwitch { + + ZoneId cet = ZoneId.of("Europe/Berlin"); + WorkingTimeCalculator cut = + new WorkingTimeCalculatorImpl( + new HolidaySchedule(true, false), + Map.of(DayOfWeek.SUNDAY, Set.of(new LocalTimeInterval(LocalTime.MIN, LocalTime.MAX))), + cet); + + @Test + void addsCorrectly() { + Instant beforeSwitch = + ZonedDateTime.of(LocalDateTime.parse("2023-03-26T00:00:00"), cet).toInstant(); + + Instant afterSwitch = cut.addWorkingTime(beforeSwitch, Duration.ofHours(3)); + + assertThat(afterSwitch) + .isEqualTo(ZonedDateTime.of(LocalDateTime.parse("2023-03-26T04:00:00"), cet).toInstant()); + } + + @Test + void subtractsCorrectly() { + Instant afterSwitch = + ZonedDateTime.of(LocalDateTime.parse("2023-03-26T05:00:00"), cet).toInstant(); + + Instant beforeSwitch = cut.subtractWorkingTime(afterSwitch, Duration.ofHours(3)); + + assertThat(beforeSwitch) + .isEqualTo(ZonedDateTime.of(LocalDateTime.parse("2023-03-26T01:00:00"), cet).toInstant()); + } + + @Test + void calculatesWorkingTimeBetweenCorrectly() { + Instant beforeSwitch = + ZonedDateTime.of(LocalDateTime.parse("2023-03-26T00:00:00"), cet).toInstant(); + Instant afterSwitch = + ZonedDateTime.of(LocalDateTime.parse("2023-03-26T09:00:00"), cet).toInstant(); + + Duration duration = cut.workingTimeBetween(beforeSwitch, afterSwitch); + + assertThat(duration).isEqualTo(Duration.ofHours(8)); + } + } } diff --git a/history/taskana-simplehistory-provider/src/test/resources/taskana.properties b/history/taskana-simplehistory-provider/src/test/resources/taskana.properties index 2b6ba4ca2..493f592c9 100644 --- a/history/taskana-simplehistory-provider/src/test/resources/taskana.properties +++ b/history/taskana-simplehistory-provider/src/test/resources/taskana.properties @@ -25,6 +25,7 @@ taskana.workingtime.schedule.TUESDAY=00:00-00:00 taskana.workingtime.schedule.WEDNESDAY=00:00-00:00 taskana.workingtime.schedule.THURSDAY=00:00-00:00 taskana.workingtime.schedule.FRIDAY=00:00-00:00 +taskana.workingtime.timezone=UTC # enable or disable the jobscheduler at all # set it to false and no jobs are running taskana.jobscheduler.enabled=false @@ -40,4 +41,3 @@ taskana.jobscheduler.enableTaskUpdatePriorityJob=true taskana.jobscheduler.enableWorkbasketCleanupJob=true taskana.jobscheduler.enableUserInfoRefreshJob=false taskana.jobscheduler.enableHistorieCleanupJob=true - diff --git a/lib/taskana-core-test/src/test/java/acceptance/TaskanaConfigurationTest.java b/lib/taskana-core-test/src/test/java/acceptance/TaskanaConfigurationTest.java index 776aeda3d..f00fcd3e1 100644 --- a/lib/taskana-core-test/src/test/java/acceptance/TaskanaConfigurationTest.java +++ b/lib/taskana-core-test/src/test/java/acceptance/TaskanaConfigurationTest.java @@ -9,6 +9,8 @@ import java.time.DayOfWeek; import java.time.Duration; import java.time.Instant; import java.time.LocalTime; +import java.time.ZoneId; +import java.time.ZoneOffset; import java.time.temporal.ChronoUnit; import java.util.Collection; import java.util.List; @@ -114,6 +116,7 @@ class TaskanaConfigurationTest { long expectedJobSchedulerPeriod = 10; TimeUnit expectedJobSchedulerPeriodTimeUnit = TimeUnit.DAYS; List expectedJobSchedulerCustomJobs = List.of("Job_A", "Job_B"); + ZoneId expectedWorkingTimeScheduleTimeZone = ZoneId.ofOffset("UTC", ZoneOffset.ofHours(4)); // when Map> expectedWorkingTimeSchedule = @@ -153,6 +156,7 @@ class TaskanaConfigurationTest { .jobSchedulerEnableHistorieCleanupJob(false) .jobSchedulerCustomJobs(expectedJobSchedulerCustomJobs) .workingTimeSchedule(expectedWorkingTimeSchedule) + .workingTimeScheduleTimeZone(expectedWorkingTimeScheduleTimeZone) .build(); // then @@ -194,6 +198,8 @@ class TaskanaConfigurationTest { assertThat(configuration.isJobSchedulerEnableHistorieCleanupJob()).isFalse(); assertThat(configuration.getJobSchedulerCustomJobs()).isEqualTo(expectedJobSchedulerCustomJobs); assertThat(configuration.getWorkingTimeSchedule()).isEqualTo(expectedWorkingTimeSchedule); + assertThat(configuration.getWorkingTimeScheduleTimeZone()) + .isEqualTo(expectedWorkingTimeScheduleTimeZone); } @Test @@ -236,6 +242,7 @@ class TaskanaConfigurationTest { .workingTimeSchedule( Map.of( DayOfWeek.MONDAY, Set.of(new LocalTimeInterval(LocalTime.MIN, LocalTime.NOON)))) + .workingTimeScheduleTimeZone(ZoneId.ofOffset("UTC", ZoneOffset.ofHours(4))) .build(); TaskanaConfiguration copyConfiguration = new Builder(configuration).build(); diff --git a/lib/taskana-core/src/main/java/pro/taskana/TaskanaConfiguration.java b/lib/taskana-core/src/main/java/pro/taskana/TaskanaConfiguration.java index 92c9a1696..2712228e8 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/TaskanaConfiguration.java +++ b/lib/taskana-core/src/main/java/pro/taskana/TaskanaConfiguration.java @@ -12,6 +12,7 @@ import java.time.DayOfWeek; import java.time.Duration; import java.time.Instant; import java.time.LocalTime; +import java.time.ZoneId; import java.util.ArrayList; import java.util.Collections; import java.util.EnumMap; @@ -75,6 +76,8 @@ public class TaskanaConfiguration { private final boolean corpusChristiEnabled; private final Map> workingTimeSchedule; + + private final ZoneId workingTimeScheduleTimeZone; // endregion // region history configuration @@ -165,6 +168,7 @@ public class TaskanaConfiguration { .collect( Collectors.toUnmodifiableMap( Entry::getKey, e -> Collections.unmodifiableSet(e.getValue()))); + this.workingTimeScheduleTimeZone = builder.workingTimeScheduleTimeZone; this.jobBatchSize = builder.jobBatchSize; this.maxNumberOfJobRetries = builder.maxNumberOfJobRetries; this.cleanupJobFirstRun = builder.cleanupJobFirstRun; @@ -249,6 +253,10 @@ public class TaskanaConfiguration { return workingTimeSchedule; } + public ZoneId getWorkingTimeScheduleTimeZone() { + return workingTimeScheduleTimeZone; + } + public Map> getRoleMap() { return roleMap; } @@ -507,6 +515,9 @@ public class TaskanaConfiguration { @TaskanaProperty("taskana.workingtime.schedule") private Map> workingTimeSchedule = initDefaultWorkingTimeSchedule(); + + @TaskanaProperty("taskana.workingtime.timezone") + private ZoneId workingTimeScheduleTimeZone = ZoneId.of("Europe/Berlin"); // endregion // region history configuration @@ -610,6 +621,7 @@ public class TaskanaConfiguration { this.germanPublicHolidaysEnabled = conf.isGermanPublicHolidaysEnabled(); this.corpusChristiEnabled = conf.isCorpusChristiEnabled(); this.workingTimeSchedule = conf.getWorkingTimeSchedule(); + this.workingTimeScheduleTimeZone = conf.getWorkingTimeScheduleTimeZone(); this.jobBatchSize = conf.getJobBatchSize(); this.maxNumberOfJobRetries = conf.getMaxNumberOfJobRetries(); this.cleanupJobFirstRun = conf.getCleanupJobFirstRun(); @@ -656,93 +668,78 @@ public class TaskanaConfiguration { // region builder methods - @SuppressWarnings("unused") // TODO: why do we need this method? public Builder schemaName(String schemaName) { this.schemaName = initSchemaName(schemaName); return this; } - @SuppressWarnings("unused") public Builder roleMap(Map> roleMap) { this.roleMap = roleMap; return this; } - @SuppressWarnings("unused") public Builder domains(List domains) { this.domains = domains; return this; } - @SuppressWarnings("unused") public Builder classificationTypes(List classificationTypes) { this.classificationTypes = classificationTypes; return this; } - @SuppressWarnings("unused") public Builder classificationCategoriesByTypeMap( Map> classificationCategoriesByTypeMap) { this.classificationCategoriesByType = classificationCategoriesByTypeMap; return this; } - @SuppressWarnings("unused") public Builder customHolidays(List customHolidays) { this.customHolidays = customHolidays; return this; } - @SuppressWarnings("unused") public Builder deleteHistoryOnTaskDeletionEnabled(boolean deleteHistoryOnTaskDeletionEnabled) { this.deleteHistoryOnTaskDeletionEnabled = deleteHistoryOnTaskDeletionEnabled; return this; } - @SuppressWarnings("unused") public Builder germanPublicHolidaysEnabled(boolean germanPublicHolidaysEnabled) { this.germanPublicHolidaysEnabled = germanPublicHolidaysEnabled; return this; } - @SuppressWarnings("unused") public Builder corpusChristiEnabled(boolean corpusChristiEnabled) { this.corpusChristiEnabled = corpusChristiEnabled; return this; } - @SuppressWarnings("unused") public Builder jobBatchSize(int jobBatchSize) { this.jobBatchSize = jobBatchSize; return this; } - @SuppressWarnings("unused") public Builder maxNumberOfJobRetries(int maxNumberOfJobRetries) { this.maxNumberOfJobRetries = maxNumberOfJobRetries; return this; } - @SuppressWarnings("unused") public Builder cleanupJobFirstRun(Instant cleanupJobFirstRun) { this.cleanupJobFirstRun = cleanupJobFirstRun; return this; } - @SuppressWarnings("unused") public Builder cleanupJobRunEvery(Duration cleanupJobRunEvery) { this.cleanupJobRunEvery = cleanupJobRunEvery; return this; } - @SuppressWarnings("unused") public Builder cleanupJobMinimumAge(Duration cleanupJobMinimumAge) { this.cleanupJobMinimumAge = cleanupJobMinimumAge; return this; } - @SuppressWarnings("unused") public Builder taskCleanupJobAllCompletedSameParentBusiness( boolean taskCleanupJobAllCompletedSameParentBusiness) { this.taskCleanupJobAllCompletedSameParentBusiness = @@ -750,56 +747,47 @@ public class TaskanaConfiguration { return this; } - @SuppressWarnings("unused") public Builder allowTimestampServiceLevelMismatch( boolean validationAllowTimestampServiceLevelMismatch) { this.allowTimestampServiceLevelMismatch = validationAllowTimestampServiceLevelMismatch; return this; } - @SuppressWarnings("unused") public Builder addAdditionalUserInfo(boolean addAdditionalUserInfo) { this.addAdditionalUserInfo = addAdditionalUserInfo; return this; } - @SuppressWarnings("unused") public Builder priorityJobBatchSize(int priorityJobBatchSize) { this.priorityJobBatchSize = priorityJobBatchSize; return this; } - @SuppressWarnings("unused") public Builder priorityJobFirstRun(Instant priorityJobFirstRun) { this.priorityJobFirstRun = priorityJobFirstRun; return this; } - @SuppressWarnings("unused") public Builder priorityJobRunEvery(Duration priorityJobRunEvery) { this.priorityJobRunEvery = priorityJobRunEvery; return this; } - @SuppressWarnings("unused") public Builder priorityJobActive(boolean priorityJobActive) { this.priorityJobActive = priorityJobActive; return this; } - @SuppressWarnings("unused") public Builder userRefreshJobRunEvery(Duration userRefreshJobRunEvery) { this.userRefreshJobRunEvery = userRefreshJobRunEvery; return this; } - @SuppressWarnings("unused") public Builder userRefreshJobFirstRun(Instant userRefreshJobFirstRun) { this.userRefreshJobFirstRun = userRefreshJobFirstRun; return this; } - @SuppressWarnings("unused") public Builder minimalPermissionsToAssignDomains( List minimalPermissionsToAssignDomains) { this.minimalPermissionsToAssignDomains = minimalPermissionsToAssignDomains; @@ -865,6 +853,11 @@ public class TaskanaConfiguration { return this; } + public Builder workingTimeScheduleTimeZone(ZoneId workingTimeScheduleTimeZone) { + this.workingTimeScheduleTimeZone = workingTimeScheduleTimeZone; + return this; + } + public TaskanaConfiguration build() { validateConfiguration(); return new TaskanaConfiguration(this); @@ -1018,17 +1011,13 @@ public class TaskanaConfiguration { private static Map> initDefaultWorkingTimeSchedule() { Map> workingTime = new EnumMap<>(DayOfWeek.class); - // Default working time schedule is from Monday 00:00 - Friday 24:00, but CET (hence -1 hour) Set standardWorkingSlots = Set.of(new LocalTimeInterval(LocalTime.MIN, LocalTime.MAX)); - workingTime.put( - DayOfWeek.SUNDAY, Set.of(new LocalTimeInterval(LocalTime.of(23, 0), LocalTime.MAX))); workingTime.put(DayOfWeek.MONDAY, standardWorkingSlots); workingTime.put(DayOfWeek.TUESDAY, standardWorkingSlots); workingTime.put(DayOfWeek.WEDNESDAY, standardWorkingSlots); workingTime.put(DayOfWeek.THURSDAY, standardWorkingSlots); - workingTime.put( - DayOfWeek.FRIDAY, Set.of(new LocalTimeInterval(LocalTime.MIN, LocalTime.of(23, 0)))); + workingTime.put(DayOfWeek.FRIDAY, standardWorkingSlots); return workingTime; } } diff --git a/lib/taskana-core/src/main/java/pro/taskana/common/internal/TaskanaEngineImpl.java b/lib/taskana-core/src/main/java/pro/taskana/common/internal/TaskanaEngineImpl.java index be5a280f9..d53e6cb5f 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/common/internal/TaskanaEngineImpl.java +++ b/lib/taskana-core/src/main/java/pro/taskana/common/internal/TaskanaEngineImpl.java @@ -132,7 +132,9 @@ public class TaskanaEngineImpl implements TaskanaEngine { taskanaConfiguration.getCustomHolidays()); workingTimeCalculator = new WorkingTimeCalculatorImpl( - holidaySchedule, taskanaConfiguration.getWorkingTimeSchedule()); + holidaySchedule, + taskanaConfiguration.getWorkingTimeSchedule(), + taskanaConfiguration.getWorkingTimeScheduleTimeZone()); currentUserContext = new CurrentUserContextImpl(TaskanaConfiguration.shouldUseLowerCaseForAccessIds()); createTransactionFactory(taskanaConfiguration.isUseManagedTransactions()); diff --git a/lib/taskana-core/src/test/java/acceptance/report/WorkingDaysToDaysReportConverterTest.java b/lib/taskana-core/src/test/java/acceptance/report/WorkingDaysToDaysReportConverterTest.java index 446af0499..81b688802 100644 --- a/lib/taskana-core/src/test/java/acceptance/report/WorkingDaysToDaysReportConverterTest.java +++ b/lib/taskana-core/src/test/java/acceptance/report/WorkingDaysToDaysReportConverterTest.java @@ -5,6 +5,7 @@ import static org.assertj.core.api.Assertions.assertThat; import java.time.DayOfWeek; import java.time.Instant; import java.time.LocalTime; +import java.time.ZoneOffset; import java.util.ArrayList; import java.util.EnumMap; import java.util.List; @@ -40,7 +41,8 @@ class WorkingDaysToDaysReportConverterTest { workingTimeCalculator = new WorkingTimeCalculatorImpl( new HolidaySchedule(true, false, List.of(dayOfReformation, allSaintsDays)), - workingTime); + workingTime, + ZoneOffset.UTC); } @Test diff --git a/lib/taskana-core/src/test/resources/taskana.properties b/lib/taskana-core/src/test/resources/taskana.properties index 06e48ae61..596533305 100644 --- a/lib/taskana-core/src/test/resources/taskana.properties +++ b/lib/taskana-core/src/test/resources/taskana.properties @@ -24,6 +24,7 @@ taskana.workingtime.schedule.TUESDAY=00:00-00:00 taskana.workingtime.schedule.WEDNESDAY=00:00-00:00 taskana.workingtime.schedule.THURSDAY=00:00-00:00 taskana.workingtime.schedule.FRIDAY=00:00-00:00 +taskana.workingtime.timezone=UTC # enable or disable the jobscheduler at all # set it to false and no jobs are running taskana.jobscheduler.enabled=false