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 02cae9d3f4e..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. @@ -13,10 +13,10 @@ package org.flowable.engine.impl.form; -import java.text.Format; import java.text.ParseException; 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; @@ -29,10 +29,16 @@ public class DateFormType extends AbstractFormType { private static final long serialVersionUID = 1L; protected String datePattern; - protected Format dateFormat; + 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); } @@ -55,7 +61,12 @@ public Object convertFormValueToModelValue(String propertyValue) { return null; } try { - return dateFormat.parseObject(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); } 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 new file mode 100644 index 00000000000..129fedbf8d3 --- /dev/null +++ b/modules/flowable-engine/src/test/java/org/flowable/engine/impl/form/DateFormTypeTest.java @@ -0,0 +1,111 @@ +/* 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.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; + +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 trailingCharactersThrowException() { + assertThatThrownBy(() -> dateFormType.convertFormValueToModelValue("15/06/2024abc")) + .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); + } + + @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 @@ + + + + + + + + + + + + + + + + + + + +