From 8b2f069480d14684896e16696a4f70e1539e9375 Mon Sep 17 00:00:00 2001 From: david-collibra Date: Wed, 4 Mar 2026 18:15:56 +0100 Subject: [PATCH 1/3] Disable lenient parsing in DateFormType FastDateFormat.parseObject() silently accepted invalid dates by rolling over values (e.g. month 13 became month 1 of the next year). Use SimpleDateFormat with setLenient(false) to reject invalid dates. --- .../engine/impl/form/DateFormType.java | 5 +- .../engine/impl/form/DateFormTypeTest.java | 83 +++++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 modules/flowable-engine/src/test/java/org/flowable/engine/impl/form/DateFormTypeTest.java diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/form/DateFormType.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/form/DateFormType.java index 02cae9d3f4e..c891b18a78a 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/form/DateFormType.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/form/DateFormType.java @@ -15,6 +15,7 @@ import java.text.Format; import java.text.ParseException; +import java.text.SimpleDateFormat; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.time.FastDateFormat; @@ -55,7 +56,9 @@ public Object convertFormValueToModelValue(String propertyValue) { return null; } try { - return dateFormat.parseObject(propertyValue); + SimpleDateFormat sdf = new SimpleDateFormat(datePattern); + sdf.setLenient(false); + return sdf.parse(propertyValue); } catch (ParseException e) { throw new FlowableIllegalArgumentException("invalid date value " + propertyValue, e); } diff --git a/modules/flowable-engine/src/test/java/org/flowable/engine/impl/form/DateFormTypeTest.java b/modules/flowable-engine/src/test/java/org/flowable/engine/impl/form/DateFormTypeTest.java new file mode 100644 index 00000000000..f869029d0d0 --- /dev/null +++ b/modules/flowable-engine/src/test/java/org/flowable/engine/impl/form/DateFormTypeTest.java @@ -0,0 +1,83 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.flowable.engine.impl.form; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.Date; + +import org.flowable.common.engine.api.FlowableIllegalArgumentException; +import org.junit.jupiter.api.Test; + +class DateFormTypeTest { + + private final DateFormType dateFormType = new DateFormType("dd/MM/yyyy"); + + @Test + void nullValueReturnsNull() { + assertThat(dateFormType.convertFormValueToModelValue(null)).isNull(); + } + + @Test + void emptyValueReturnsNull() { + assertThat(dateFormType.convertFormValueToModelValue("")).isNull(); + } + + @Test + void validDateIsParsed() { + Object result = dateFormType.convertFormValueToModelValue("15/06/2024"); + assertThat(result).isInstanceOf(Date.class); + } + + @Test + void invalidMonthThrowsException() { + assertThatThrownBy(() -> dateFormType.convertFormValueToModelValue("15/13/2024")) + .isInstanceOf(FlowableIllegalArgumentException.class); + } + + @Test + void invalidDayThrowsException() { + assertThatThrownBy(() -> dateFormType.convertFormValueToModelValue("32/06/2024")) + .isInstanceOf(FlowableIllegalArgumentException.class); + } + + @Test + void wrongFormatThrowsException() { + assertThatThrownBy(() -> dateFormType.convertFormValueToModelValue("2024-06-15")) + .isInstanceOf(FlowableIllegalArgumentException.class); + } + + @Test + void differentFormatValidDateIsParsed() { + DateFormType isoFormat = new DateFormType("yyyy-MM-dd"); + Object result = isoFormat.convertFormValueToModelValue("2024-06-15"); + assertThat(result).isInstanceOf(Date.class); + } + + @Test + void differentFormatInvalidDayThrowsException() { + DateFormType isoFormat = new DateFormType("yyyy-MM-dd"); + assertThatThrownBy(() -> isoFormat.convertFormValueToModelValue("2024-06-32")) + .isInstanceOf(FlowableIllegalArgumentException.class); + } + + @Test + void inputValidForOtherFormatThrowsException() { + DateFormType isoFormat = new DateFormType("yyyy-MM-dd"); + assertThatThrownBy(() -> isoFormat.convertFormValueToModelValue("15/06/2024")) + .isInstanceOf(FlowableIllegalArgumentException.class); + } + +} From 577c1beb20972fc481fc692244ad7d3b858468ec Mon Sep 17 00:00:00 2001 From: david-collibra Date: Wed, 18 Mar 2026 13:55:51 +0100 Subject: [PATCH 2/3] Replace FastDateFormat field with local SimpleDateFormat instances FastDateFormat does not support lenient=false. Using local SimpleDateFormat instances in both methods avoids shared state and is thread-safe. The overhead of creating a new instance per call is acceptable as these methods are not expected to be called frequently on the same object. --- .../java/org/flowable/engine/impl/form/DateFormType.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/form/DateFormType.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/form/DateFormType.java index c891b18a78a..38aa26795fa 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/form/DateFormType.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/form/DateFormType.java @@ -13,12 +13,10 @@ package org.flowable.engine.impl.form; -import java.text.Format; import java.text.ParseException; import java.text.SimpleDateFormat; import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.time.FastDateFormat; import org.flowable.common.engine.api.FlowableIllegalArgumentException; import org.flowable.engine.form.AbstractFormType; @@ -30,11 +28,9 @@ public class DateFormType extends AbstractFormType { private static final long serialVersionUID = 1L; protected String datePattern; - protected Format dateFormat; public DateFormType(String datePattern) { this.datePattern = datePattern; - this.dateFormat = FastDateFormat.getInstance(datePattern); } @Override @@ -69,6 +65,6 @@ public String convertModelValueToFormValue(Object modelValue) { if (modelValue == null) { return null; } - return dateFormat.format(modelValue); + return new SimpleDateFormat(datePattern).format(modelValue); } } From 5539bb3ab9fcdfb4e7cdf22702d2916d0da463e1 Mon Sep 17 00:00:00 2001 From: david-collibra Date: Thu, 11 Jun 2026 15:05:51 +0200 Subject: [PATCH 3/3] Make strict date parsing configurable via lenientDateParsing flag Date form properties now parse strictly by default, rejecting rolled-over dates (e.g. 15/13/2024) and trailing characters. The previous lenient behaviour can be restored per engine (ProcessEngineConfiguration) or per form property (lenientDateParsing attribute), with the property-level flag taking precedence. Formatting still uses FastDateFormat, so the output format is unchanged. --- .../bpmn/constants/BpmnXMLConstants.java | 1 + .../bpmn/converter/BaseBpmnXMLConverter.java | 4 ++ .../converter/child/FormPropertyParser.java | 3 + .../xml/FormPropertiesConverterTest.java | 11 ++- .../test/resources/formPropertiesProcess.bpmn | 3 + .../org/flowable/bpmn/model/FormProperty.java | 10 +++ .../cfg/ProcessEngineConfigurationImpl.java | 13 +++- .../engine/impl/form/DateFormType.java | 26 +++++-- .../flowable/engine/impl/form/FormTypes.java | 12 +++- .../engine/impl/form/DateFormTypeTest.java | 28 ++++++++ .../engine/impl/form/FormTypesTest.java | 68 +++++++++++++++++++ .../engine/test/api/form/FormServiceTest.java | 19 ++++++ ...tDateFormPropertyLenientParsing.bpmn20.xml | 23 +++++++ 13 files changed, 211 insertions(+), 10 deletions(-) create mode 100644 modules/flowable-engine/src/test/java/org/flowable/engine/impl/form/FormTypesTest.java create mode 100644 modules/flowable-engine/src/test/resources/org/flowable/engine/test/api/form/FormServiceTest.testDateFormPropertyLenientParsing.bpmn20.xml diff --git a/modules/flowable-bpmn-converter/src/main/java/org/flowable/bpmn/constants/BpmnXMLConstants.java b/modules/flowable-bpmn-converter/src/main/java/org/flowable/bpmn/constants/BpmnXMLConstants.java index 1473238fb8c..67a59b8247f 100644 --- a/modules/flowable-bpmn-converter/src/main/java/org/flowable/bpmn/constants/BpmnXMLConstants.java +++ b/modules/flowable-bpmn-converter/src/main/java/org/flowable/bpmn/constants/BpmnXMLConstants.java @@ -366,6 +366,7 @@ public interface BpmnXMLConstants { public static final String ATTRIBUTE_FORM_REQUIRED = "required"; public static final String ATTRIBUTE_FORM_DEFAULT = "default"; public static final String ATTRIBUTE_FORM_DATEPATTERN = "datePattern"; + public static final String ATTRIBUTE_FORM_LENIENT_DATE_PARSING = "lenientDateParsing"; public static final String ELEMENT_VALUE = "value"; public static final String ELEMENT_FIELD = "field"; diff --git a/modules/flowable-bpmn-converter/src/main/java/org/flowable/bpmn/converter/BaseBpmnXMLConverter.java b/modules/flowable-bpmn-converter/src/main/java/org/flowable/bpmn/converter/BaseBpmnXMLConverter.java index 6a7afd6324b..f5a8afc8473 100644 --- a/modules/flowable-bpmn-converter/src/main/java/org/flowable/bpmn/converter/BaseBpmnXMLConverter.java +++ b/modules/flowable-bpmn-converter/src/main/java/org/flowable/bpmn/converter/BaseBpmnXMLConverter.java @@ -406,6 +406,10 @@ protected boolean writeFormProperties(FlowElement flowElement, boolean didWriteE writeDefaultAttribute(ATTRIBUTE_FORM_VARIABLE, property.getVariable(), xtw); writeDefaultAttribute(ATTRIBUTE_FORM_DEFAULT, property.getDefaultExpression(), xtw); writeDefaultAttribute(ATTRIBUTE_FORM_DATEPATTERN, property.getDatePattern(), xtw); + if (property.getLenientDateParsing() != null) { + writeDefaultAttribute(ATTRIBUTE_FORM_LENIENT_DATE_PARSING, + property.getLenientDateParsing() ? ATTRIBUTE_VALUE_TRUE : ATTRIBUTE_VALUE_FALSE, xtw); + } if (!property.isReadable()) { writeDefaultAttribute(ATTRIBUTE_FORM_READABLE, ATTRIBUTE_VALUE_FALSE, xtw); } diff --git a/modules/flowable-bpmn-converter/src/main/java/org/flowable/bpmn/converter/child/FormPropertyParser.java b/modules/flowable-bpmn-converter/src/main/java/org/flowable/bpmn/converter/child/FormPropertyParser.java index c668d7ee333..a3bdc28135f 100644 --- a/modules/flowable-bpmn-converter/src/main/java/org/flowable/bpmn/converter/child/FormPropertyParser.java +++ b/modules/flowable-bpmn-converter/src/main/java/org/flowable/bpmn/converter/child/FormPropertyParser.java @@ -54,6 +54,9 @@ public void parseChildElement(XMLStreamReader xtr, BaseElement parentElement, Bp property.setExpression(xtr.getAttributeValue(null, ATTRIBUTE_FORM_EXPRESSION)); property.setDefaultExpression(xtr.getAttributeValue(null, ATTRIBUTE_FORM_DEFAULT)); property.setDatePattern(xtr.getAttributeValue(null, ATTRIBUTE_FORM_DATEPATTERN)); + if (StringUtils.isNotEmpty(xtr.getAttributeValue(null, ATTRIBUTE_FORM_LENIENT_DATE_PARSING))) { + property.setLenientDateParsing(Boolean.valueOf(xtr.getAttributeValue(null, ATTRIBUTE_FORM_LENIENT_DATE_PARSING))); + } if (StringUtils.isNotEmpty(xtr.getAttributeValue(null, ATTRIBUTE_FORM_REQUIRED))) { property.setRequired(Boolean.valueOf(xtr.getAttributeValue(null, ATTRIBUTE_FORM_REQUIRED))); } diff --git a/modules/flowable-bpmn-converter/src/test/java/org/flowable/editor/language/xml/FormPropertiesConverterTest.java b/modules/flowable-bpmn-converter/src/test/java/org/flowable/editor/language/xml/FormPropertiesConverterTest.java index 3f66f1b949c..49c6218991a 100644 --- a/modules/flowable-bpmn-converter/src/test/java/org/flowable/editor/language/xml/FormPropertiesConverterTest.java +++ b/modules/flowable-bpmn-converter/src/test/java/org/flowable/editor/language/xml/FormPropertiesConverterTest.java @@ -49,7 +49,7 @@ void validateModel(BpmnModel model) { List formProperties = userTask.getFormProperties(); - assertThat(formProperties).as("Invalid form properties list: ").hasSize(8); + assertThat(formProperties).as("Invalid form properties list: ").hasSize(11); for (FormProperty formProperty : formProperties) { if ("new_property_1".equals(formProperty.getId())) { @@ -78,6 +78,15 @@ void validateModel(BpmnModel model) { checkFormProperty(formProperty, true, true, false); } else if ("new_property_8".equals(formProperty.getId())) { checkFormProperty(formProperty, true, true, true); + } else if ("date_property_default".equals(formProperty.getId())) { + assertThat(formProperty.getDatePattern()).isEqualTo("dd/MM/yyyy"); + assertThat(formProperty.getLenientDateParsing()).isNull(); + } else if ("date_property_lenient".equals(formProperty.getId())) { + assertThat(formProperty.getDatePattern()).isEqualTo("dd/MM/yyyy"); + assertThat(formProperty.getLenientDateParsing()).isTrue(); + } else if ("date_property_strict".equals(formProperty.getId())) { + assertThat(formProperty.getDatePattern()).isEqualTo("dd/MM/yyyy"); + assertThat(formProperty.getLenientDateParsing()).isFalse(); } } diff --git a/modules/flowable-bpmn-converter/src/test/resources/formPropertiesProcess.bpmn b/modules/flowable-bpmn-converter/src/test/resources/formPropertiesProcess.bpmn index 6ec57e0a862..593aca10e3f 100644 --- a/modules/flowable-bpmn-converter/src/test/resources/formPropertiesProcess.bpmn +++ b/modules/flowable-bpmn-converter/src/test/resources/formPropertiesProcess.bpmn @@ -34,6 +34,9 @@ + + + diff --git a/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/FormProperty.java b/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/FormProperty.java index 9abd7d6efc9..f32709d2370 100644 --- a/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/FormProperty.java +++ b/modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/FormProperty.java @@ -26,6 +26,7 @@ public class FormProperty extends BaseElement { protected String type; protected String defaultExpression; protected String datePattern; + protected Boolean lenientDateParsing; protected boolean readable = true; protected boolean writeable = true; protected boolean required; @@ -79,6 +80,14 @@ public void setDatePattern(String datePattern) { this.datePattern = datePattern; } + public Boolean getLenientDateParsing() { + return lenientDateParsing; + } + + public void setLenientDateParsing(Boolean lenientDateParsing) { + this.lenientDateParsing = lenientDateParsing; + } + public boolean isReadable() { return readable; } @@ -126,6 +135,7 @@ public void setValues(FormProperty otherProperty) { setType(otherProperty.getType()); setDefaultExpression(otherProperty.getDefaultExpression()); setDatePattern(otherProperty.getDatePattern()); + setLenientDateParsing(otherProperty.getLenientDateParsing()); setReadable(otherProperty.isReadable()); setWriteable(otherProperty.isWriteable()); setRequired(otherProperty.isRequired()); diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/cfg/ProcessEngineConfigurationImpl.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/cfg/ProcessEngineConfigurationImpl.java index f35dd0bcbc5..15edb101a9a 100755 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/cfg/ProcessEngineConfigurationImpl.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/cfg/ProcessEngineConfigurationImpl.java @@ -681,6 +681,7 @@ public abstract class ProcessEngineConfigurationImpl extends ProcessEngineConfig protected List customFormTypes; protected FormTypes formTypes; + protected boolean lenientDateParsing; protected List customPreVariableTypes; protected List customPostVariableTypes; @@ -2257,9 +2258,10 @@ public void initFormEngines() { public void initFormTypes() { if (formTypes == null) { formTypes = new FormTypes(); + formTypes.setLenientDateParsing(lenientDateParsing); formTypes.addFormType(new StringFormType()); formTypes.addFormType(new LongFormType()); - formTypes.addFormType(new DateFormType("dd/MM/yyyy")); + formTypes.addFormType(new DateFormType("dd/MM/yyyy", lenientDateParsing)); formTypes.addFormType(new BooleanFormType()); formTypes.addFormType(new DoubleFormType()); } @@ -2993,6 +2995,15 @@ public ProcessEngineConfigurationImpl setFormTypes(FormTypes formTypes) { return this; } + public boolean isLenientDateParsing() { + return lenientDateParsing; + } + + public ProcessEngineConfigurationImpl setLenientDateParsing(boolean lenientDateParsing) { + this.lenientDateParsing = lenientDateParsing; + return this; + } + @Override public FlowableScriptEngine getScriptEngine() { return scriptEngine; diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/form/DateFormType.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/form/DateFormType.java index 38aa26795fa..d1c6eb7177a 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/form/DateFormType.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/form/DateFormType.java @@ -1,9 +1,9 @@ /* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -14,9 +14,10 @@ package org.flowable.engine.impl.form; import java.text.ParseException; -import java.text.SimpleDateFormat; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.time.DateUtils; +import org.apache.commons.lang3.time.FastDateFormat; import org.flowable.common.engine.api.FlowableIllegalArgumentException; import org.flowable.engine.form.AbstractFormType; @@ -28,9 +29,17 @@ public class DateFormType extends AbstractFormType { private static final long serialVersionUID = 1L; protected String datePattern; + protected boolean lenientDateParsing; + protected FastDateFormat dateFormat; public DateFormType(String datePattern) { + this(datePattern, false); + } + + public DateFormType(String datePattern, boolean lenientDateParsing) { this.datePattern = datePattern; + this.lenientDateParsing = lenientDateParsing; + this.dateFormat = FastDateFormat.getInstance(datePattern); } @Override @@ -52,9 +61,12 @@ public Object convertFormValueToModelValue(String propertyValue) { return null; } try { - SimpleDateFormat sdf = new SimpleDateFormat(datePattern); - sdf.setLenient(false); - return sdf.parse(propertyValue); + if (lenientDateParsing) { + return dateFormat.parseObject(propertyValue); + } + // FastDateFormat is always lenient, so strict parsing uses DateUtils, + // which also rejects input with trailing characters after the date. + return DateUtils.parseDateStrictly(propertyValue, datePattern); } catch (ParseException e) { throw new FlowableIllegalArgumentException("invalid date value " + propertyValue, e); } @@ -65,6 +77,6 @@ public String convertModelValueToFormValue(Object modelValue) { if (modelValue == null) { return null; } - return new SimpleDateFormat(datePattern).format(modelValue); + return dateFormat.format(modelValue); } } diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/form/FormTypes.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/form/FormTypes.java index 6e58b225a5e..585a31c3912 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/form/FormTypes.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/form/FormTypes.java @@ -29,6 +29,7 @@ public class FormTypes { protected Map formTypes = new HashMap<>(); + protected boolean lenientDateParsing; public void addFormType(AbstractFormType formType) { formTypes.put(formType.getName(), formType); @@ -38,7 +39,8 @@ public AbstractFormType parseFormPropertyType(FormProperty formProperty) { AbstractFormType formType = null; if ("date".equals(formProperty.getType()) && StringUtils.isNotEmpty(formProperty.getDatePattern())) { - formType = new DateFormType(formProperty.getDatePattern()); + boolean lenient = formProperty.getLenientDateParsing() != null ? formProperty.getLenientDateParsing() : lenientDateParsing; + formType = new DateFormType(formProperty.getDatePattern(), lenient); } else if ("enum".equals(formProperty.getType())) { // ACT-1023: Using linked hashmap to preserve the order in which the @@ -57,4 +59,12 @@ public AbstractFormType parseFormPropertyType(FormProperty formProperty) { } return formType; } + + public boolean isLenientDateParsing() { + return lenientDateParsing; + } + + public void setLenientDateParsing(boolean lenientDateParsing) { + this.lenientDateParsing = lenientDateParsing; + } } diff --git a/modules/flowable-engine/src/test/java/org/flowable/engine/impl/form/DateFormTypeTest.java b/modules/flowable-engine/src/test/java/org/flowable/engine/impl/form/DateFormTypeTest.java index f869029d0d0..129fedbf8d3 100644 --- a/modules/flowable-engine/src/test/java/org/flowable/engine/impl/form/DateFormTypeTest.java +++ b/modules/flowable-engine/src/test/java/org/flowable/engine/impl/form/DateFormTypeTest.java @@ -16,7 +16,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import java.util.Calendar; import java.util.Date; +import java.util.GregorianCalendar; import org.flowable.common.engine.api.FlowableIllegalArgumentException; import org.junit.jupiter.api.Test; @@ -53,6 +55,12 @@ void invalidDayThrowsException() { .isInstanceOf(FlowableIllegalArgumentException.class); } + @Test + void trailingCharactersThrowException() { + assertThatThrownBy(() -> dateFormType.convertFormValueToModelValue("15/06/2024abc")) + .isInstanceOf(FlowableIllegalArgumentException.class); + } + @Test void wrongFormatThrowsException() { assertThatThrownBy(() -> dateFormType.convertFormValueToModelValue("2024-06-15")) @@ -80,4 +88,24 @@ void inputValidForOtherFormatThrowsException() { .isInstanceOf(FlowableIllegalArgumentException.class); } + @Test + void lenientParsingRollsOverInvalidDate() { + DateFormType lenientFormType = new DateFormType("dd/MM/yyyy", true); + Object result = lenientFormType.convertFormValueToModelValue("15/13/2024"); + assertThat(result).isEqualTo(new GregorianCalendar(2025, Calendar.JANUARY, 15).getTime()); + } + + @Test + void lenientParsingAcceptsValidDate() { + DateFormType lenientFormType = new DateFormType("dd/MM/yyyy", true); + Object result = lenientFormType.convertFormValueToModelValue("15/06/2024"); + assertThat(result).isInstanceOf(Date.class); + } + + @Test + void modelValueIsFormatted() { + Date date = new GregorianCalendar(2024, Calendar.JUNE, 15).getTime(); + assertThat(dateFormType.convertModelValueToFormValue(date)).isEqualTo("15/06/2024"); + } + } diff --git a/modules/flowable-engine/src/test/java/org/flowable/engine/impl/form/FormTypesTest.java b/modules/flowable-engine/src/test/java/org/flowable/engine/impl/form/FormTypesTest.java new file mode 100644 index 00000000000..aab92da4df2 --- /dev/null +++ b/modules/flowable-engine/src/test/java/org/flowable/engine/impl/form/FormTypesTest.java @@ -0,0 +1,68 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.flowable.engine.impl.form; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.Date; + +import org.flowable.bpmn.model.FormProperty; +import org.flowable.common.engine.api.FlowableIllegalArgumentException; +import org.flowable.engine.form.AbstractFormType; +import org.junit.jupiter.api.Test; + +class FormTypesTest { + + private static final String INVALID_DATE = "15/13/2024"; + + private final FormTypes formTypes = new FormTypes(); + + @Test + void dateParsingIsStrictByDefault() { + AbstractFormType formType = formTypes.parseFormPropertyType(dateFormProperty(null)); + assertThatThrownBy(() -> formType.convertFormValueToModelValue(INVALID_DATE)) + .isInstanceOf(FlowableIllegalArgumentException.class); + } + + @Test + void engineDefaultLenientIsApplied() { + formTypes.setLenientDateParsing(true); + AbstractFormType formType = formTypes.parseFormPropertyType(dateFormProperty(null)); + assertThat(formType.convertFormValueToModelValue(INVALID_DATE)).isInstanceOf(Date.class); + } + + @Test + void formPropertyOverrideWinsOverStrictDefault() { + AbstractFormType formType = formTypes.parseFormPropertyType(dateFormProperty(true)); + assertThat(formType.convertFormValueToModelValue(INVALID_DATE)).isInstanceOf(Date.class); + } + + @Test + void formPropertyOverrideWinsOverLenientDefault() { + formTypes.setLenientDateParsing(true); + AbstractFormType formType = formTypes.parseFormPropertyType(dateFormProperty(false)); + assertThatThrownBy(() -> formType.convertFormValueToModelValue(INVALID_DATE)) + .isInstanceOf(FlowableIllegalArgumentException.class); + } + + private FormProperty dateFormProperty(Boolean lenientDateParsing) { + FormProperty formProperty = new FormProperty(); + formProperty.setId("dateProperty"); + formProperty.setType("date"); + formProperty.setDatePattern("dd/MM/yyyy"); + formProperty.setLenientDateParsing(lenientDateParsing); + return formProperty; + } +} diff --git a/modules/flowable-engine/src/test/java/org/flowable/engine/test/api/form/FormServiceTest.java b/modules/flowable-engine/src/test/java/org/flowable/engine/test/api/form/FormServiceTest.java index 135899f137c..267619e64a4 100644 --- a/modules/flowable-engine/src/test/java/org/flowable/engine/test/api/form/FormServiceTest.java +++ b/modules/flowable-engine/src/test/java/org/flowable/engine/test/api/form/FormServiceTest.java @@ -18,6 +18,8 @@ import static org.assertj.core.api.Assertions.entry; import java.util.ArrayList; +import java.util.Calendar; +import java.util.GregorianCalendar; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; @@ -238,6 +240,23 @@ public void testFormPropertyHandling() { assertThat(variables).isEqualTo(expectedVariables); } + @Test + @Deployment + public void testDateFormPropertyLenientParsing() { + String procDefId = repositoryService.createProcessDefinitionQuery().singleResult().getId(); + + Map invalidStrictDate = new HashMap<>(); + invalidStrictDate.put("strictDate", "15/13/2024"); + assertThatThrownBy(() -> formService.submitStartFormData(procDefId, invalidStrictDate)) + .isInstanceOf(FlowableIllegalArgumentException.class); + + Map invalidLenientDate = new HashMap<>(); + invalidLenientDate.put("lenientDate", "15/13/2024"); + String processInstanceId = formService.submitStartFormData(procDefId, invalidLenientDate).getId(); + assertThat(runtimeService.getVariable(processInstanceId, "lenientDate")) + .isEqualTo(new GregorianCalendar(2025, Calendar.JANUARY, 15).getTime()); + } + @Test @Deployment public void testFormPropertyExpression() { diff --git a/modules/flowable-engine/src/test/resources/org/flowable/engine/test/api/form/FormServiceTest.testDateFormPropertyLenientParsing.bpmn20.xml b/modules/flowable-engine/src/test/resources/org/flowable/engine/test/api/form/FormServiceTest.testDateFormPropertyLenientParsing.bpmn20.xml new file mode 100644 index 00000000000..2c71e99cc35 --- /dev/null +++ b/modules/flowable-engine/src/test/resources/org/flowable/engine/test/api/form/FormServiceTest.testDateFormPropertyLenientParsing.bpmn20.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + +