TSK-1972: Uses time zone for working time calculations

This commit is contained in:
Mustapha Zorgati 2023-03-10 14:44:11 +01:00
parent 37280cc73b
commit b916d577ca
9 changed files with 248 additions and 104 deletions

View File

@ -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<ZoneId> {
@Override
public Optional<ZoneId> initialize(
Map<String, String> properties,
String separator,
Field field,
TaskanaProperty taskanaProperty) {
return parseProperty(properties, taskanaProperty.value(), ZoneId::of);
}
}
static class EnumPropertyParser implements PropertyParser<Enum<?>> {
@Override
public Optional<Enum<?>> initialize(

View File

@ -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<DayOfWeek, Set<LocalTimeInterval>> workingTimeSchedule) {
HolidaySchedule holidaySchedule,
Map<DayOfWeek, Set<LocalTimeInterval>> 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
* <code>null</code>.
* @return The WorkSlot that matches best <code>currentDateTime</code> 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
* <code>null</code>.
* @return The WorkSlot that matches best <code>currentDateTime</code> 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);
}
}
}

View File

@ -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<LocalTimeInterval> 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));
}
}
}

View File

@ -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

View File

@ -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<String> expectedJobSchedulerCustomJobs = List.of("Job_A", "Job_B");
ZoneId expectedWorkingTimeScheduleTimeZone = ZoneId.ofOffset("UTC", ZoneOffset.ofHours(4));
// when
Map<DayOfWeek, Set<LocalTimeInterval>> 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();

View File

@ -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<DayOfWeek, Set<LocalTimeInterval>> 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<TaskanaRole, Set<String>> getRoleMap() {
return roleMap;
}
@ -507,6 +515,9 @@ public class TaskanaConfiguration {
@TaskanaProperty("taskana.workingtime.schedule")
private Map<DayOfWeek, Set<LocalTimeInterval>> 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<TaskanaRole, Set<String>> roleMap) {
this.roleMap = roleMap;
return this;
}
@SuppressWarnings("unused")
public Builder domains(List<String> domains) {
this.domains = domains;
return this;
}
@SuppressWarnings("unused")
public Builder classificationTypes(List<String> classificationTypes) {
this.classificationTypes = classificationTypes;
return this;
}
@SuppressWarnings("unused")
public Builder classificationCategoriesByTypeMap(
Map<String, List<String>> classificationCategoriesByTypeMap) {
this.classificationCategoriesByType = classificationCategoriesByTypeMap;
return this;
}
@SuppressWarnings("unused")
public Builder customHolidays(List<CustomHoliday> 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<WorkbasketPermission> 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<DayOfWeek, Set<LocalTimeInterval>> initDefaultWorkingTimeSchedule() {
Map<DayOfWeek, Set<LocalTimeInterval>> workingTime = new EnumMap<>(DayOfWeek.class);
// Default working time schedule is from Monday 00:00 - Friday 24:00, but CET (hence -1 hour)
Set<LocalTimeInterval> 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;
}
}

View File

@ -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());

View File

@ -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

View File

@ -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