From 48571830d383678a84214de17b69525a89024d89 Mon Sep 17 00:00:00 2001 From: Filip Hrisafov Date: Mon, 26 Oct 2026 14:17:36 +0100 Subject: [PATCH] Use the engine clock when converting periods and durations to dates The DefaultVariableValueConversionHandler used the system clock when converting ISO 8601 periods and durations (e.g. P10D) to a date or local date. It now uses the clock of the engine configuration, in the same way as the business calendars, so the conversions respect a configured clock. The type conversion tests now use a fixed clock and assert the exact values, which also makes CallActivityTypeConversionTest stable around DST changes. --- .../cmmn/test/TypeConversionTest.java | 39 ++++++++++++------- .../impl/AbstractEngineConfiguration.java | 2 +- ...DefaultVariableValueConversionHandler.java | 11 +++++- .../CallActivityTypeConversionTest.java | 24 ++++++------ 4 files changed, 49 insertions(+), 27 deletions(-) diff --git a/modules/flowable-cmmn-engine-configurator/src/test/java/org/flowable/cmmn/test/TypeConversionTest.java b/modules/flowable-cmmn-engine-configurator/src/test/java/org/flowable/cmmn/test/TypeConversionTest.java index b4b661b1526..ad4e37bfba0 100644 --- a/modules/flowable-cmmn-engine-configurator/src/test/java/org/flowable/cmmn/test/TypeConversionTest.java +++ b/modules/flowable-cmmn-engine-configurator/src/test/java/org/flowable/cmmn/test/TypeConversionTest.java @@ -14,8 +14,9 @@ import static org.assertj.core.api.Assertions.assertThat; -import java.time.Duration; +import java.time.Instant; import java.time.LocalDate; +import java.time.ZonedDateTime; import java.util.Date; import java.util.HashMap; import java.util.Map; @@ -27,6 +28,7 @@ import org.flowable.engine.runtime.Execution; import org.flowable.engine.runtime.ProcessInstance; import org.flowable.task.api.Task; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; /** @@ -53,6 +55,8 @@ public void testProcessTaskTypeConversion() { variables.put("periodVar", "P10D"); variables.put("durationVar", "PT10H"); + ZonedDateTime now = setClocks(); + CaseInstance caseInstance = cmmnRuntimeService.createCaseInstanceBuilder() .caseDefinitionKey("processTaskTypeConversion") .variables(variables) @@ -79,16 +83,12 @@ public void testProcessTaskTypeConversion() { .isInstanceOf(Long.class).isEqualTo(789012345678L); // Verify period/duration conversions - Date now = new Date(); Object periodToDate = processEngine.getRuntimeService().getVariable(processInstance.getId(), "periodToDate"); assertThat(periodToDate).isInstanceOf(Date.class); - assertThat(Duration.between(now.toInstant(), ((Date) periodToDate).toInstant())) - .isBetween(Duration.ofDays(9).plusHours(23), Duration.ofDays(10).plusMinutes(1)); + assertThat(((Date) periodToDate).toInstant()).isEqualTo(now.plusDays(10).toInstant()); Object durationToLocalDate = processEngine.getRuntimeService().getVariable(processInstance.getId(), "durationToLocalDate"); - assertThat(durationToLocalDate).isInstanceOf(LocalDate.class); - LocalDate today = LocalDate.now(); - assertThat((LocalDate) durationToLocalDate).isBetween(today, today.plusDays(1)); + assertThat(durationToLocalDate).isInstanceOf(LocalDate.class).isEqualTo(now.plusHours(10).toLocalDate()); // Set variables on the process instance for out parameter testing processEngine.getRuntimeService().setVariable(processInstance.getId(), "processIntVar", "456"); @@ -120,6 +120,8 @@ public void testCaseTaskTypeConversion() { variables.put("periodVar", "P10D"); variables.put("durationVar", "PT10H"); + ZonedDateTime now = setClocks(); + CaseInstance parentCaseInstance = cmmnRuntimeService.createCaseInstanceBuilder() .caseDefinitionKey("parentCaseTypeConversion") .variables(variables) @@ -140,16 +142,12 @@ public void testCaseTaskTypeConversion() { .isInstanceOf(Boolean.class).isEqualTo(true); // Verify period/duration conversions - Date now = new Date(); Object periodToDate = cmmnRuntimeService.getVariable(childCaseInstance.getId(), "periodToDate"); assertThat(periodToDate).isInstanceOf(Date.class); - assertThat(Duration.between(now.toInstant(), ((Date) periodToDate).toInstant())) - .isBetween(Duration.ofDays(9).plusHours(23), Duration.ofDays(10).plusMinutes(1)); + assertThat(((Date) periodToDate).toInstant()).isEqualTo(now.plusDays(10).toInstant()); Object durationToLocalDate = cmmnRuntimeService.getVariable(childCaseInstance.getId(), "durationToLocalDate"); - assertThat(durationToLocalDate).isInstanceOf(LocalDate.class); - LocalDate today = LocalDate.now(); - assertThat((LocalDate) durationToLocalDate).isBetween(today, today.plusDays(1)); + assertThat(durationToLocalDate).isInstanceOf(LocalDate.class).isEqualTo(now.plusHours(10).toLocalDate()); // Set variables on the child case for out parameter testing cmmnRuntimeService.setVariable(childCaseInstance.getId(), "childIntVar", "456"); @@ -224,4 +222,19 @@ public void testCaseServiceTaskTypeConversion() { processEngineRepositoryService.deleteDeployment(deployment.getId(), true); } } + + protected ZonedDateTime setClocks() { + // The period and duration conversions are relative to the current time of the engine clock. + // The next 10 days cross the end of the daylight saving time in Europe and in the US + Instant now = Instant.parse("2025-10-24T10:15:30Z"); + processEngineConfiguration.getClock().setCurrentTime(Date.from(now)); + cmmnEngineConfiguration.getClock().setCurrentTime(Date.from(now)); + return now.atZone(cmmnEngineConfiguration.getClock().getCurrentTimeZone().toZoneId()); + } + + @AfterEach + public void resetClocks() { + processEngineConfiguration.getClock().reset(); + cmmnEngineConfiguration.getClock().reset(); + } } diff --git a/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/AbstractEngineConfiguration.java b/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/AbstractEngineConfiguration.java index 48a9a83ca35..40866fb21d5 100755 --- a/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/AbstractEngineConfiguration.java +++ b/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/AbstractEngineConfiguration.java @@ -645,7 +645,7 @@ public void initIdGenerator() { public void initVariableValueConversionHandler() { if (variableValueConversionHandler == null) { - variableValueConversionHandler = new DefaultVariableValueConversionHandler(); + variableValueConversionHandler = new DefaultVariableValueConversionHandler(clock); } } diff --git a/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/DefaultVariableValueConversionHandler.java b/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/DefaultVariableValueConversionHandler.java index 30210611462..ee0968de357 100644 --- a/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/DefaultVariableValueConversionHandler.java +++ b/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/DefaultVariableValueConversionHandler.java @@ -24,6 +24,7 @@ import org.flowable.common.engine.api.FlowableException; import org.flowable.common.engine.impl.json.FlowableJsonNode; import org.flowable.common.engine.impl.json.VariableJsonMapper; +import org.flowable.common.engine.impl.runtime.ClockReader; import org.flowable.common.engine.impl.util.DateUtil; import org.flowable.common.engine.impl.util.JsonUtil; @@ -34,6 +35,12 @@ */ public class DefaultVariableValueConversionHandler implements VariableValueConversionHandler { + protected final ClockReader clockReader; + + public DefaultVariableValueConversionHandler(ClockReader clockReader) { + this.clockReader = clockReader; + } + @Override public Object convertValue(Object value, String type, VariableJsonMapper variableJsonMapper) { return switch (type.toLowerCase()) { @@ -194,11 +201,11 @@ protected Object convertToArray(Object value, VariableJsonMapper variableJsonMap } /** - * Parses an ISO 8601 duration/period string (e.g. "P10D", "PT10H", "P1Y2M3DT4H") and adds it to the current time. + * Parses an ISO 8601 duration/period string (e.g. "P10D", "PT10H", "P1Y2M3DT4H") and adds it to the current time of the clock. * Uses the same parsing logic as {@link org.flowable.common.engine.impl.calendar.DueDateBusinessCalendar}. */ protected ZonedDateTime addDurationToNow(String durationString) { - ZonedDateTime now = ZonedDateTime.now(); + ZonedDateTime now = ZonedDateTime.ofInstant(clockReader.getCurrentTime().toInstant(), clockReader.getCurrentTimeZone().toZoneId()); Period period; Duration duration; if (durationString.startsWith("PT")) { diff --git a/modules/flowable-engine/src/test/java/org/flowable/engine/test/bpmn/callactivity/CallActivityTypeConversionTest.java b/modules/flowable-engine/src/test/java/org/flowable/engine/test/bpmn/callactivity/CallActivityTypeConversionTest.java index abaa10168dd..24c9a537e60 100644 --- a/modules/flowable-engine/src/test/java/org/flowable/engine/test/bpmn/callactivity/CallActivityTypeConversionTest.java +++ b/modules/flowable-engine/src/test/java/org/flowable/engine/test/bpmn/callactivity/CallActivityTypeConversionTest.java @@ -14,10 +14,11 @@ import static org.assertj.core.api.Assertions.assertThat; -import java.time.Duration; import java.time.Instant; import java.time.LocalDate; import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; import java.util.Date; import java.util.HashMap; import java.util.Map; @@ -87,6 +88,12 @@ public void testInParameterTypeConversion() { // Source variables for array conversion variables.put("arrayStringVar", "[1,2,3]"); + // The period and duration conversions are relative to the current time of the engine clock. + // The next 10 days cross the end of the daylight saving time in Europe and in the US + Instant now = Instant.parse("2025-10-24T10:15:30Z"); + processEngineConfiguration.getClock().setCurrentTime(Date.from(now)); + ZonedDateTime zonedNow = now.atZone(processEngineConfiguration.getClock().getCurrentTimeZone().toZoneId()); + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("parentProcess", variables); // Get the child process instance @@ -145,16 +152,14 @@ public void testInParameterTypeConversion() { assertThat(((Date) instantToDate).toInstant()).isEqualTo(testInstant); // Verify period/duration to date conversions - Date now = new Date(); + // A period is added in calendar days in the system time zone, so it is not always a multiple of 24 hours (e.g. when crossing a DST change) Object periodToDate = runtimeService.getVariable(childProcessInstance.getId(), "periodToDate"); assertThat(periodToDate).isInstanceOf(Date.class); - assertThat(Duration.between(now.toInstant(), ((Date) periodToDate).toInstant())) - .isBetween(Duration.ofDays(9).plusHours(23), Duration.ofDays(10).plusMinutes(1)); + assertThat(((Date) periodToDate).toInstant()).isEqualTo(zonedNow.plusDays(10).toInstant()); Object durationToDate = runtimeService.getVariable(childProcessInstance.getId(), "durationToDate"); assertThat(durationToDate).isInstanceOf(Date.class); - assertThat(Duration.between(now.toInstant(), ((Date) durationToDate).toInstant())) - .isBetween(Duration.ofHours(9).plusMinutes(59), Duration.ofHours(10).plusMinutes(1)); + assertThat(((Date) durationToDate).toInstant()).isEqualTo(now.plus(10, ChronoUnit.HOURS)); // Verify localdate conversions Object stringToLocalDate = runtimeService.getVariable(childProcessInstance.getId(), "stringToLocalDate"); @@ -167,14 +172,11 @@ public void testInParameterTypeConversion() { assertThat(instantToLocalDate).isInstanceOf(LocalDate.class).isEqualTo(testLocalDate); // Verify period/duration to localdate conversions - LocalDate today = LocalDate.now(); Object periodToLocalDate = runtimeService.getVariable(childProcessInstance.getId(), "periodToLocalDate"); - assertThat(periodToLocalDate).isInstanceOf(LocalDate.class).isEqualTo(today.plusDays(10)); + assertThat(periodToLocalDate).isInstanceOf(LocalDate.class).isEqualTo(zonedNow.plusDays(10).toLocalDate()); Object durationToLocalDate = runtimeService.getVariable(childProcessInstance.getId(), "durationToLocalDate"); - assertThat(durationToLocalDate).isInstanceOf(LocalDate.class); - // PT10H from now could be today or tomorrow depending on time of day - assertThat((LocalDate) durationToLocalDate).isBetween(today, today.plusDays(1)); + assertThat(durationToLocalDate).isInstanceOf(LocalDate.class).isEqualTo(zonedNow.plusHours(10).toLocalDate()); // Verify JSON conversion Object stringToJson = runtimeService.getVariable(childProcessInstance.getId(), "stringToJson");