Skip to content

Commit 238a0d6

Browse files
authored
Make strict date parsing in DateFormType configurable via lenientDateParsing flag (#4175)
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.
1 parent 71c2f51 commit 238a0d6

13 files changed

Lines changed: 291 additions & 8 deletions

File tree

modules/flowable-bpmn-converter/src/main/java/org/flowable/bpmn/constants/BpmnXMLConstants.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,7 @@ public interface BpmnXMLConstants {
366366
public static final String ATTRIBUTE_FORM_REQUIRED = "required";
367367
public static final String ATTRIBUTE_FORM_DEFAULT = "default";
368368
public static final String ATTRIBUTE_FORM_DATEPATTERN = "datePattern";
369+
public static final String ATTRIBUTE_FORM_LENIENT_DATE_PARSING = "lenientDateParsing";
369370
public static final String ELEMENT_VALUE = "value";
370371

371372
public static final String ELEMENT_FIELD = "field";

modules/flowable-bpmn-converter/src/main/java/org/flowable/bpmn/converter/BaseBpmnXMLConverter.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,10 @@ protected boolean writeFormProperties(FlowElement flowElement, boolean didWriteE
406406
writeDefaultAttribute(ATTRIBUTE_FORM_VARIABLE, property.getVariable(), xtw);
407407
writeDefaultAttribute(ATTRIBUTE_FORM_DEFAULT, property.getDefaultExpression(), xtw);
408408
writeDefaultAttribute(ATTRIBUTE_FORM_DATEPATTERN, property.getDatePattern(), xtw);
409+
if (property.getLenientDateParsing() != null) {
410+
writeDefaultAttribute(ATTRIBUTE_FORM_LENIENT_DATE_PARSING,
411+
property.getLenientDateParsing() ? ATTRIBUTE_VALUE_TRUE : ATTRIBUTE_VALUE_FALSE, xtw);
412+
}
409413
if (!property.isReadable()) {
410414
writeDefaultAttribute(ATTRIBUTE_FORM_READABLE, ATTRIBUTE_VALUE_FALSE, xtw);
411415
}

modules/flowable-bpmn-converter/src/main/java/org/flowable/bpmn/converter/child/FormPropertyParser.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ public void parseChildElement(XMLStreamReader xtr, BaseElement parentElement, Bp
5454
property.setExpression(xtr.getAttributeValue(null, ATTRIBUTE_FORM_EXPRESSION));
5555
property.setDefaultExpression(xtr.getAttributeValue(null, ATTRIBUTE_FORM_DEFAULT));
5656
property.setDatePattern(xtr.getAttributeValue(null, ATTRIBUTE_FORM_DATEPATTERN));
57+
if (StringUtils.isNotEmpty(xtr.getAttributeValue(null, ATTRIBUTE_FORM_LENIENT_DATE_PARSING))) {
58+
property.setLenientDateParsing(Boolean.valueOf(xtr.getAttributeValue(null, ATTRIBUTE_FORM_LENIENT_DATE_PARSING)));
59+
}
5760
if (StringUtils.isNotEmpty(xtr.getAttributeValue(null, ATTRIBUTE_FORM_REQUIRED))) {
5861
property.setRequired(Boolean.valueOf(xtr.getAttributeValue(null, ATTRIBUTE_FORM_REQUIRED)));
5962
}

modules/flowable-bpmn-converter/src/test/java/org/flowable/editor/language/xml/FormPropertiesConverterTest.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ void validateModel(BpmnModel model) {
4949

5050
List<FormProperty> formProperties = userTask.getFormProperties();
5151

52-
assertThat(formProperties).as("Invalid form properties list: ").hasSize(8);
52+
assertThat(formProperties).as("Invalid form properties list: ").hasSize(11);
5353

5454
for (FormProperty formProperty : formProperties) {
5555
if ("new_property_1".equals(formProperty.getId())) {
@@ -78,6 +78,15 @@ void validateModel(BpmnModel model) {
7878
checkFormProperty(formProperty, true, true, false);
7979
} else if ("new_property_8".equals(formProperty.getId())) {
8080
checkFormProperty(formProperty, true, true, true);
81+
} else if ("date_property_default".equals(formProperty.getId())) {
82+
assertThat(formProperty.getDatePattern()).isEqualTo("dd/MM/yyyy");
83+
assertThat(formProperty.getLenientDateParsing()).isNull();
84+
} else if ("date_property_lenient".equals(formProperty.getId())) {
85+
assertThat(formProperty.getDatePattern()).isEqualTo("dd/MM/yyyy");
86+
assertThat(formProperty.getLenientDateParsing()).isTrue();
87+
} else if ("date_property_strict".equals(formProperty.getId())) {
88+
assertThat(formProperty.getDatePattern()).isEqualTo("dd/MM/yyyy");
89+
assertThat(formProperty.getLenientDateParsing()).isFalse();
8190
}
8291
}
8392

modules/flowable-bpmn-converter/src/test/resources/formPropertiesProcess.bpmn

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@
3434
<activiti:formProperty id="new_property_6" name="v101" type="string" variable="v101" readable="false" required="true"></activiti:formProperty>
3535
<activiti:formProperty id="new_property_7" name="v110" type="string" variable="v110" writable="false" required="true"></activiti:formProperty>
3636
<activiti:formProperty id="new_property_8" name="v111" type="string" variable="v111" required="true"></activiti:formProperty>
37+
<activiti:formProperty id="date_property_default" name="d1" type="date" datePattern="dd/MM/yyyy" variable="d1"></activiti:formProperty>
38+
<activiti:formProperty id="date_property_lenient" name="d2" type="date" datePattern="dd/MM/yyyy" lenientDateParsing="true" variable="d2"></activiti:formProperty>
39+
<activiti:formProperty id="date_property_strict" name="d3" type="date" datePattern="dd/MM/yyyy" lenientDateParsing="false" variable="d3"></activiti:formProperty>
3740
</extensionElements>
3841
</userTask>
3942
<sequenceFlow id="sid-67E5C57C-B8CB-4B76-AF19-46A3DD03290E" sourceRef="startNode" targetRef="userTask"></sequenceFlow>

modules/flowable-bpmn-model/src/main/java/org/flowable/bpmn/model/FormProperty.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public class FormProperty extends BaseElement {
2626
protected String type;
2727
protected String defaultExpression;
2828
protected String datePattern;
29+
protected Boolean lenientDateParsing;
2930
protected boolean readable = true;
3031
protected boolean writeable = true;
3132
protected boolean required;
@@ -79,6 +80,14 @@ public void setDatePattern(String datePattern) {
7980
this.datePattern = datePattern;
8081
}
8182

83+
public Boolean getLenientDateParsing() {
84+
return lenientDateParsing;
85+
}
86+
87+
public void setLenientDateParsing(Boolean lenientDateParsing) {
88+
this.lenientDateParsing = lenientDateParsing;
89+
}
90+
8291
public boolean isReadable() {
8392
return readable;
8493
}
@@ -126,6 +135,7 @@ public void setValues(FormProperty otherProperty) {
126135
setType(otherProperty.getType());
127136
setDefaultExpression(otherProperty.getDefaultExpression());
128137
setDatePattern(otherProperty.getDatePattern());
138+
setLenientDateParsing(otherProperty.getLenientDateParsing());
129139
setReadable(otherProperty.isReadable());
130140
setWriteable(otherProperty.isWriteable());
131141
setRequired(otherProperty.isRequired());

modules/flowable-engine/src/main/java/org/flowable/engine/impl/cfg/ProcessEngineConfigurationImpl.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -699,6 +699,7 @@ public abstract class ProcessEngineConfigurationImpl extends ProcessEngineConfig
699699

700700
protected List<AbstractFormType> customFormTypes;
701701
protected FormTypes formTypes;
702+
protected boolean lenientDateParsing;
702703

703704
protected List<VariableType> customPreVariableTypes;
704705
protected List<VariableType> customPostVariableTypes;
@@ -2286,9 +2287,10 @@ public void initFormEngines() {
22862287
public void initFormTypes() {
22872288
if (formTypes == null) {
22882289
formTypes = new FormTypes();
2290+
formTypes.setLenientDateParsing(lenientDateParsing);
22892291
formTypes.addFormType(new StringFormType());
22902292
formTypes.addFormType(new LongFormType());
2291-
formTypes.addFormType(new DateFormType("dd/MM/yyyy"));
2293+
formTypes.addFormType(new DateFormType("dd/MM/yyyy", lenientDateParsing));
22922294
formTypes.addFormType(new BooleanFormType());
22932295
formTypes.addFormType(new DoubleFormType());
22942296
}
@@ -3013,6 +3015,15 @@ public ProcessEngineConfigurationImpl setFormTypes(FormTypes formTypes) {
30133015
return this;
30143016
}
30153017

3018+
public boolean isLenientDateParsing() {
3019+
return lenientDateParsing;
3020+
}
3021+
3022+
public ProcessEngineConfigurationImpl setLenientDateParsing(boolean lenientDateParsing) {
3023+
this.lenientDateParsing = lenientDateParsing;
3024+
return this;
3025+
}
3026+
30163027
@Override
30173028
public FlowableScriptEngine getScriptEngine() {
30183029
return scriptEngine;

modules/flowable-engine/src/main/java/org/flowable/engine/impl/form/DateFormType.java

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
/* Licensed under the Apache License, Version 2.0 (the "License");
22
* you may not use this file except in compliance with the License.
33
* You may obtain a copy of the License at
4-
*
4+
*
55
* http://www.apache.org/licenses/LICENSE-2.0
6-
*
6+
*
77
* Unless required by applicable law or agreed to in writing, software
88
* distributed under the License is distributed on an "AS IS" BASIS,
99
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -13,10 +13,10 @@
1313

1414
package org.flowable.engine.impl.form;
1515

16-
import java.text.Format;
1716
import java.text.ParseException;
1817

1918
import org.apache.commons.lang3.StringUtils;
19+
import org.apache.commons.lang3.time.DateUtils;
2020
import org.apache.commons.lang3.time.FastDateFormat;
2121
import org.flowable.common.engine.api.FlowableIllegalArgumentException;
2222
import org.flowable.engine.form.AbstractFormType;
@@ -29,10 +29,16 @@ public class DateFormType extends AbstractFormType {
2929
private static final long serialVersionUID = 1L;
3030

3131
protected String datePattern;
32-
protected Format dateFormat;
32+
protected boolean lenientDateParsing;
33+
protected FastDateFormat dateFormat;
3334

3435
public DateFormType(String datePattern) {
36+
this(datePattern, false);
37+
}
38+
39+
public DateFormType(String datePattern, boolean lenientDateParsing) {
3540
this.datePattern = datePattern;
41+
this.lenientDateParsing = lenientDateParsing;
3642
this.dateFormat = FastDateFormat.getInstance(datePattern);
3743
}
3844

@@ -55,7 +61,12 @@ public Object convertFormValueToModelValue(String propertyValue) {
5561
return null;
5662
}
5763
try {
58-
return dateFormat.parseObject(propertyValue);
64+
if (lenientDateParsing) {
65+
return dateFormat.parseObject(propertyValue);
66+
}
67+
// FastDateFormat is always lenient, so strict parsing uses DateUtils,
68+
// which also rejects input with trailing characters after the date.
69+
return DateUtils.parseDateStrictly(propertyValue, datePattern);
5970
} catch (ParseException e) {
6071
throw new FlowableIllegalArgumentException("invalid date value " + propertyValue, e);
6172
}

modules/flowable-engine/src/main/java/org/flowable/engine/impl/form/FormTypes.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
public class FormTypes {
3030

3131
protected Map<String, AbstractFormType> formTypes = new HashMap<>();
32+
protected boolean lenientDateParsing;
3233

3334
public void addFormType(AbstractFormType formType) {
3435
formTypes.put(formType.getName(), formType);
@@ -38,7 +39,8 @@ public AbstractFormType parseFormPropertyType(FormProperty formProperty) {
3839
AbstractFormType formType = null;
3940

4041
if ("date".equals(formProperty.getType()) && StringUtils.isNotEmpty(formProperty.getDatePattern())) {
41-
formType = new DateFormType(formProperty.getDatePattern());
42+
boolean lenient = formProperty.getLenientDateParsing() != null ? formProperty.getLenientDateParsing() : lenientDateParsing;
43+
formType = new DateFormType(formProperty.getDatePattern(), lenient);
4244

4345
} else if ("enum".equals(formProperty.getType())) {
4446
// ACT-1023: Using linked hashmap to preserve the order in which the
@@ -57,4 +59,12 @@ public AbstractFormType parseFormPropertyType(FormProperty formProperty) {
5759
}
5860
return formType;
5961
}
62+
63+
public boolean isLenientDateParsing() {
64+
return lenientDateParsing;
65+
}
66+
67+
public void setLenientDateParsing(boolean lenientDateParsing) {
68+
this.lenientDateParsing = lenientDateParsing;
69+
}
6070
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/* Licensed under the Apache License, Version 2.0 (the "License");
2+
* you may not use this file except in compliance with the License.
3+
* You may obtain a copy of the License at
4+
*
5+
* http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
* limitations under the License.
12+
*/
13+
14+
package org.flowable.engine.impl.form;
15+
16+
import static org.assertj.core.api.Assertions.assertThat;
17+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
18+
19+
import java.util.Calendar;
20+
import java.util.Date;
21+
import java.util.GregorianCalendar;
22+
23+
import org.flowable.common.engine.api.FlowableIllegalArgumentException;
24+
import org.junit.jupiter.api.Test;
25+
26+
class DateFormTypeTest {
27+
28+
private final DateFormType dateFormType = new DateFormType("dd/MM/yyyy");
29+
30+
@Test
31+
void nullValueReturnsNull() {
32+
assertThat(dateFormType.convertFormValueToModelValue(null)).isNull();
33+
}
34+
35+
@Test
36+
void emptyValueReturnsNull() {
37+
assertThat(dateFormType.convertFormValueToModelValue("")).isNull();
38+
}
39+
40+
@Test
41+
void validDateIsParsed() {
42+
Object result = dateFormType.convertFormValueToModelValue("15/06/2024");
43+
assertThat(result).isInstanceOf(Date.class);
44+
}
45+
46+
@Test
47+
void invalidMonthThrowsException() {
48+
assertThatThrownBy(() -> dateFormType.convertFormValueToModelValue("15/13/2024"))
49+
.isInstanceOf(FlowableIllegalArgumentException.class);
50+
}
51+
52+
@Test
53+
void invalidDayThrowsException() {
54+
assertThatThrownBy(() -> dateFormType.convertFormValueToModelValue("32/06/2024"))
55+
.isInstanceOf(FlowableIllegalArgumentException.class);
56+
}
57+
58+
@Test
59+
void trailingCharactersThrowException() {
60+
assertThatThrownBy(() -> dateFormType.convertFormValueToModelValue("15/06/2024abc"))
61+
.isInstanceOf(FlowableIllegalArgumentException.class);
62+
}
63+
64+
@Test
65+
void wrongFormatThrowsException() {
66+
assertThatThrownBy(() -> dateFormType.convertFormValueToModelValue("2024-06-15"))
67+
.isInstanceOf(FlowableIllegalArgumentException.class);
68+
}
69+
70+
@Test
71+
void differentFormatValidDateIsParsed() {
72+
DateFormType isoFormat = new DateFormType("yyyy-MM-dd");
73+
Object result = isoFormat.convertFormValueToModelValue("2024-06-15");
74+
assertThat(result).isInstanceOf(Date.class);
75+
}
76+
77+
@Test
78+
void differentFormatInvalidDayThrowsException() {
79+
DateFormType isoFormat = new DateFormType("yyyy-MM-dd");
80+
assertThatThrownBy(() -> isoFormat.convertFormValueToModelValue("2024-06-32"))
81+
.isInstanceOf(FlowableIllegalArgumentException.class);
82+
}
83+
84+
@Test
85+
void inputValidForOtherFormatThrowsException() {
86+
DateFormType isoFormat = new DateFormType("yyyy-MM-dd");
87+
assertThatThrownBy(() -> isoFormat.convertFormValueToModelValue("15/06/2024"))
88+
.isInstanceOf(FlowableIllegalArgumentException.class);
89+
}
90+
91+
@Test
92+
void lenientParsingRollsOverInvalidDate() {
93+
DateFormType lenientFormType = new DateFormType("dd/MM/yyyy", true);
94+
Object result = lenientFormType.convertFormValueToModelValue("15/13/2024");
95+
assertThat(result).isEqualTo(new GregorianCalendar(2025, Calendar.JANUARY, 15).getTime());
96+
}
97+
98+
@Test
99+
void lenientParsingAcceptsValidDate() {
100+
DateFormType lenientFormType = new DateFormType("dd/MM/yyyy", true);
101+
Object result = lenientFormType.convertFormValueToModelValue("15/06/2024");
102+
assertThat(result).isInstanceOf(Date.class);
103+
}
104+
105+
@Test
106+
void modelValueIsFormatted() {
107+
Date date = new GregorianCalendar(2024, Calendar.JUNE, 15).getTime();
108+
assertThat(dateFormType.convertModelValueToFormValue(date)).isEqualTo("15/06/2024");
109+
}
110+
111+
}

0 commit comments

Comments
 (0)