Skip to content

Commit afe8aea

Browse files
Ensure that multiple character events for the same element are handled correctly
1 parent 0598835 commit afe8aea

10 files changed

Lines changed: 410 additions & 5 deletions

File tree

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,6 @@ protected void parseChildElements(String elementName, BaseElement parentElement,
281281
}
282282

283283
protected void parseChildElements(String elementName, BaseElement parentElement, Map<String, BaseChildElementParser> additionalParsers, BpmnModel model, XMLStreamReader xtr) throws Exception {
284-
285284
Map<String, BaseChildElementParser> childParsers = new HashMap<>();
286285
if (additionalParsers != null) {
287286
childParsers.putAll(additionalParsers);
@@ -308,11 +307,17 @@ protected ExtensionElement parseExtensionElement(XMLStreamReader xtr) throws Exc
308307
xtr.next();
309308
if (xtr.isCharacters() || XMLStreamReader.CDATA == xtr.getEventType()) {
310309
if (StringUtils.isNotEmpty(xtr.getText().trim())) {
311-
extensionElement.setElementText(xtr.getText().trim());
310+
if (extensionElement.getElementText() != null) {
311+
extensionElement.setElementText(extensionElement.getElementText() + xtr.getText().trim());
312+
313+
} else {
314+
extensionElement.setElementText(xtr.getText().trim());
315+
}
312316
}
313317
} else if (xtr.isStartElement()) {
314318
ExtensionElement childExtensionElement = parseExtensionElement(xtr);
315319
extensionElement.addChildElement(childExtensionElement);
320+
316321
} else if (xtr.isEndElement() && extensionElement.getName().equalsIgnoreCase(xtr.getLocalName())) {
317322
readyWithExtensionElement = true;
318323
}

modules/flowable-bpmn-converter/src/main/java/org/flowable/bpmn/converter/util/BpmnXMLUtil.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ public static void parseChildElements(String elementName, BaseElement parentElem
144144
if (xtr.isStartElement()) {
145145
if (ELEMENT_EXTENSIONS.equals(xtr.getLocalName())) {
146146
inExtensionElements = true;
147+
147148
} else if (localParserMap.containsKey(xtr.getLocalName())) {
148149
BaseChildElementParser childParser = localParserMap.get(xtr.getLocalName());
149150
// if we're into an extension element but the current element is not accepted by this parentElement then is read as a custom extension element
@@ -153,6 +154,7 @@ public static void parseChildElements(String elementName, BaseElement parentElem
153154
continue;
154155
}
155156
localParserMap.get(xtr.getLocalName()).parseChildElement(xtr, parentElement, model);
157+
156158
} else if (inExtensionElements) {
157159
ExtensionElement extensionElement = BpmnXMLUtil.parseExtensionElement(xtr);
158160
parentElement.addExtensionElement(extensionElement);
@@ -199,11 +201,17 @@ public static ExtensionElement parseExtensionElement(XMLStreamReader xtr) throws
199201
xtr.next();
200202
if (xtr.isCharacters() || XMLStreamReader.CDATA == xtr.getEventType()) {
201203
if (StringUtils.isNotEmpty(xtr.getText().trim())) {
202-
extensionElement.setElementText(xtr.getText().trim());
204+
if (extensionElement.getElementText() != null) {
205+
extensionElement.setElementText(extensionElement.getElementText() + xtr.getText().trim());
206+
207+
} else {
208+
extensionElement.setElementText(xtr.getText().trim());
209+
}
203210
}
204211
} else if (xtr.isStartElement()) {
205212
ExtensionElement childExtensionElement = parseExtensionElement(xtr);
206213
extensionElement.addChildElement(childExtensionElement);
214+
207215
} else if (xtr.isEndElement() && extensionElement.getName().equalsIgnoreCase(xtr.getLocalName())) {
208216
readyWithExtensionElement = true;
209217
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright 2025, Flowable Licences AG.
3+
* This license is based on the software license agreement and terms and conditions in effect between the parties
4+
* at the time of purchase of the Flowable software product.
5+
* Your agreement to these terms and conditions is required to install or use the Flowable software product and/or this file.
6+
* Flowable is a trademark of Flowable AG registered in several countries.
7+
*/
8+
package org.flowable.editor.language.xml;
9+
10+
import static org.assertj.core.api.Assertions.assertThat;
11+
12+
import org.flowable.bpmn.model.BpmnModel;
13+
import org.flowable.editor.language.xml.util.BpmnXmlConverterTest;
14+
15+
public class MultipleCharacterEventsConverterTest {
16+
17+
@BpmnXmlConverterTest("multipleCharacterEvents.bpmn")
18+
void validateModel(BpmnModel model) {
19+
assertThat(model.getFlowElement("task").getExtensionElements().get("data").get(0).getElementText()).isEqualTo("a&b&c&d&e&f&g");
20+
21+
assertThat(model.getFlowElement("end").getExtensionElements().get("data").get(0).getElementText()).isEqualTo("a&b&c&d&e&f&g");
22+
23+
assertThat(model.getFlowElement("flow2").getExtensionElements().get("data").get(0).getElementText()).isEqualTo("a&b&c&d&e&f&g");
24+
}
25+
26+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:flowable="http://flowable.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" xmlns:design="http://flowable.org/design" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://flowable.org/test">
3+
<process id="test" name="test" isExecutable="true">
4+
<serviceTask id="task" flowable:delegateExpression="${test}">
5+
<extensionElements>
6+
<design:data>a&amp;b&amp;c&amp;d&amp;e&amp;f&amp;g</design:data>
7+
</extensionElements>
8+
</serviceTask>
9+
<startEvent id="start" flowable:initiator="initiator" />
10+
<endEvent id="end">
11+
<extensionElements>
12+
<design:data>a&amp;b&amp;c&amp;d&amp;e&amp;f&amp;g</design:data>
13+
</extensionElements>
14+
</endEvent>
15+
<sequenceFlow id="flow1" sourceRef="start" targetRef="task" />
16+
<sequenceFlow id="flow2" sourceRef="task" targetRef="end">
17+
<extensionElements>
18+
<design:data>a&amp;b&amp;c&amp;d&amp;e&amp;f&amp;g</design:data>
19+
</extensionElements>
20+
</sequenceFlow>
21+
</process>
22+
</definitions>

modules/flowable-cmmn-converter/src/main/java/org/flowable/cmmn/converter/util/CmmnXmlUtil.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,17 @@ public static ExtensionElement parseExtensionElement(XMLStreamReader xtr) throws
7676
while (!readyWithExtensionElement && xtr.hasNext()) {
7777
xtr.next();
7878
if (xtr.isCharacters() || XMLStreamReader.CDATA == xtr.getEventType()) {
79-
if (StringUtils.isNotEmpty(xtr.getText().trim())) {
79+
if (extensionElement.getElementText() != null) {
80+
extensionElement.setElementText(extensionElement.getElementText() + xtr.getText().trim());
81+
82+
} else {
8083
extensionElement.setElementText(xtr.getText().trim());
8184
}
85+
8286
} else if (xtr.isStartElement()) {
8387
ExtensionElement childExtensionElement = parseExtensionElement(xtr);
8488
extensionElement.addChildElement(childExtensionElement);
89+
8590
} else if (xtr.isEndElement() && extensionElement.getName().equalsIgnoreCase(xtr.getLocalName())) {
8691
readyWithExtensionElement = true;
8792
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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+
package org.flowable.test.cmmn.converter;
14+
15+
import static org.assertj.core.api.Assertions.assertThat;
16+
import static org.assertj.core.api.Assertions.tuple;
17+
18+
import java.util.List;
19+
20+
import org.flowable.cmmn.model.Case;
21+
import org.flowable.cmmn.model.CmmnModel;
22+
import org.flowable.cmmn.model.ExtensionElement;
23+
import org.flowable.cmmn.model.Milestone;
24+
import org.flowable.cmmn.model.PlanItem;
25+
import org.flowable.cmmn.model.Sentry;
26+
import org.flowable.cmmn.model.Stage;
27+
import org.flowable.cmmn.model.Task;
28+
import org.flowable.test.cmmn.converter.util.CmmnXmlConverterTest;
29+
30+
/**
31+
* @author Tijs Rademakers
32+
*/
33+
public class ExtensionElementsCharEventsCmmnXmlConverterTest {
34+
35+
@CmmnXmlConverterTest("org/flowable/test/cmmn/converter/extensionElementsCharEvents.cmmn")
36+
public void validateModel(CmmnModel cmmnModel) {
37+
assertThat(cmmnModel).isNotNull();
38+
assertThat(cmmnModel.getCases()).hasSize(1);
39+
40+
// Case
41+
Case caze = cmmnModel.getCases().get(0);
42+
assertThat(caze.getId()).isEqualTo("myCase");
43+
44+
Stage planModel = caze.getPlanModel();
45+
46+
Task task = (Task) planModel.findPlanItemDefinitionInStageOrUpwards("taskA");
47+
assertThat(task.getExtensionElements()).hasSize(1);
48+
List<ExtensionElement> extensionElements = task.getExtensionElements().get("taskTest");
49+
assertThat(extensionElements)
50+
.extracting(ExtensionElement::getName, ExtensionElement::getElementText)
51+
.containsExactly(tuple("taskTest", "a&b&c&d&e&f&g"));
52+
53+
Milestone milestone = (Milestone) planModel.findPlanItemDefinitionInStageOrUpwards("mileStoneOne");
54+
assertThat(milestone.getExtensionElements()).hasSize(1);
55+
extensionElements = milestone.getExtensionElements().get("milestoneTest");
56+
assertThat(extensionElements)
57+
.extracting(ExtensionElement::getName, ExtensionElement::getElementText)
58+
.containsExactly(tuple("milestoneTest", "a&b&c&d&e&f&g"));
59+
60+
PlanItem planItem = planModel.findPlanItemInPlanFragmentOrDownwards("planItemTaskA");
61+
assertThat(planItem.getExtensionElements()).hasSize(1);
62+
extensionElements = planItem.getExtensionElements().get("test");
63+
assertThat(extensionElements)
64+
.extracting(ExtensionElement::getName, ExtensionElement::getElementText)
65+
.containsExactly(tuple("test", "a&b&c&d&e&f&g"));
66+
67+
List<Sentry> sentries = planModel.getSentries();
68+
assertThat(sentries).hasSize(3);
69+
Sentry sentry = sentries.get(0);
70+
assertThat(sentry.getExtensionElements()).hasSize(1);
71+
extensionElements = sentry.getExtensionElements().get("sentryTest");
72+
assertThat(extensionElements)
73+
.extracting(ExtensionElement::getName, ExtensionElement::getElementText)
74+
.containsExactly(tuple("sentryTest", "a&b&c&d&e&f&g"));
75+
}
76+
77+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<definitions
3+
xmlns="http://www.omg.org/spec/CMMN/20151109/MODEL"
4+
xmlns:dc="http://www.omg.org/spec/CMMN/20151109/DC"
5+
xmlns:di="http://www.omg.org/spec/CMMN/20151109/DI"
6+
xmlns:cmmndi="http://www.omg.org/spec/CMMN/20151109/CMMNDI"
7+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
8+
xmlns:flowable="http://flowable.org/cmmn"
9+
targetNamespace="http://flowable.org/cmmn">
10+
11+
<case id="myCase">
12+
<casePlanModel id="myPlanModel" name="My CasePlanModel" flowable:formKey="formKey" flowable:formFieldValidation="formFieldValidationValue">
13+
14+
<planItem id="planItemTaskA" definitionRef="taskA">
15+
<extensionElements>
16+
<flowable:test>a&amp;b&amp;c&amp;d&amp;e&amp;f&amp;g</flowable:test>
17+
</extensionElements>
18+
</planItem>
19+
<planItem id="planItemMileStoneOne" definitionRef="mileStoneOne">
20+
<entryCriterion id="criterion1" sentryRef="sentryMileStoneOne" />
21+
</planItem>
22+
<planItem id="planItemTaskB" definitionRef="taskB">
23+
<entryCriterion id="criterion2" sentryRef="sentryTaskB" />
24+
</planItem>
25+
<planItem id="planItemMileStoneTwo" definitionRef="mileStoneTwo">
26+
<entryCriterion id="criterion3" sentryRef="sentryMileStoneTwo" />
27+
</planItem>
28+
29+
<sentry id="sentryMileStoneTwo">
30+
<extensionElements>
31+
<flowable:sentryTest>a&amp;b&amp;c&amp;d&amp;e&amp;f&amp;g</flowable:sentryTest>
32+
</extensionElements>
33+
<planItemOnPart id="onPart1" sourceRef="planItemTaskA">
34+
<standardEvent>complete</standardEvent>
35+
</planItemOnPart>
36+
</sentry>
37+
<sentry id="sentryTaskB">
38+
<planItemOnPart id="onPart2" sourceRef="planItemMileStoneTwo">
39+
<standardEvent>occur</standardEvent>
40+
</planItemOnPart>
41+
</sentry>
42+
<sentry id="sentryMileStoneOne">
43+
<planItemOnPart id="onPart3" sourceRef="planItemTaskB">
44+
<standardEvent>complete</standardEvent>
45+
</planItemOnPart>
46+
</sentry>
47+
48+
<task id="taskA" name="A">
49+
<extensionElements>
50+
<flowable:taskTest>a&amp;b&amp;c&amp;d&amp;e&amp;f&amp;g</flowable:taskTest>
51+
</extensionElements>
52+
</task>
53+
<milestone id="mileStoneOne" name="Milestone 2">
54+
<extensionElements>
55+
<flowable:milestoneTest>a&amp;b&amp;c&amp;d&amp;e&amp;f&amp;g</flowable:milestoneTest>
56+
</extensionElements>
57+
</milestone>
58+
<task id="taskB" name="B" isBlocking="false"/>
59+
<milestone id="mileStoneTwo" name="Milestone 1" />
60+
61+
</casePlanModel>
62+
</case>
63+
64+
</definitions>

modules/flowable-dmn-xml-converter/src/main/java/org/flowable/dmn/converter/util/DmnXMLUtil.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,12 +143,17 @@ public static DmnExtensionElement parseExtensionElement(XMLStreamReader xtr) thr
143143
while (!readyWithExtensionElement && xtr.hasNext()) {
144144
xtr.next();
145145
if (xtr.isCharacters() || XMLStreamReader.CDATA == xtr.getEventType()) {
146-
if (StringUtils.isNotEmpty(xtr.getText().trim())) {
146+
if (extensionElement.getElementText() != null) {
147+
extensionElement.setElementText(extensionElement.getElementText() + xtr.getText().trim());
148+
149+
} else {
147150
extensionElement.setElementText(xtr.getText().trim());
148151
}
152+
149153
} else if (xtr.isStartElement()) {
150154
DmnExtensionElement childExtensionElement = parseExtensionElement(xtr);
151155
extensionElement.addChildElement(childExtensionElement);
156+
152157
} else if (xtr.isEndElement() && extensionElement.getName().equalsIgnoreCase(xtr.getLocalName())) {
153158
readyWithExtensionElement = true;
154159
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
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+
package org.flowable.dmn.xml;
14+
15+
import static org.assertj.core.api.Assertions.assertThat;
16+
17+
import java.util.List;
18+
19+
import org.flowable.dmn.model.Decision;
20+
import org.flowable.dmn.model.DecisionTable;
21+
import org.flowable.dmn.model.DmnDefinition;
22+
import org.flowable.dmn.model.DmnExtensionElement;
23+
import org.flowable.dmn.model.InputClause;
24+
import org.flowable.dmn.model.OutputClause;
25+
import org.junit.jupiter.api.Test;
26+
27+
public class ExtensionElementCharEventsConverterTest extends AbstractConverterTest {
28+
29+
protected static final String YOURCO_EXTENSIONS_NAMESPACE = "http://yourco/bpmn";
30+
protected static final String YOURCO_EXTENSIONS_PREFIX = "yourco";
31+
32+
protected static final String ELEMENT_ATTRIBUTES = "attributes";
33+
protected static final String ELEMENT_ATTRIBUTE = "attribute";
34+
35+
protected static final String ELEMENT_I18N = "i18n";
36+
37+
@Test
38+
public void convertXMLToModel() throws Exception {
39+
DmnDefinition definition = readXMLFile();
40+
validateModel(definition);
41+
}
42+
43+
@Test
44+
public void convertModelToXML() throws Exception {
45+
DmnDefinition bpmnModel = readXMLFile();
46+
DmnDefinition parsedModel = exportAndReadXMLFile(bpmnModel);
47+
validateModel(parsedModel);
48+
}
49+
50+
@Override
51+
protected String getResource() {
52+
return "extensionElementsCharEvents.dmn";
53+
}
54+
55+
private void validateModel(DmnDefinition model) {
56+
assertThat(model.getDescription()).isEqualTo("DMN description");
57+
58+
/*
59+
* Verify localization extension
60+
*/
61+
List<DmnExtensionElement> i18nExtension = model.getExtensionElements().get(ELEMENT_I18N);
62+
assertThat(i18nExtension.get(0).getElementText()).isEqualTo("a&b&c&d&e&f&g");
63+
64+
List<Decision> decisions = model.getDecisions();
65+
assertThat(decisions).hasSize(1);
66+
67+
DecisionTable decisionTable = (DecisionTable) decisions.get(0).getExpression();
68+
assertThat(decisionTable).isNotNull();
69+
70+
assertThat(decisionTable.getDescription()).isEqualTo("Decision table description");
71+
72+
/*
73+
* Verify decision table localization extension
74+
*/
75+
i18nExtension = decisionTable.getExtensionElements().get(ELEMENT_I18N);
76+
assertThat(i18nExtension.get(0).getElementText()).isEqualTo("a&b&c&d&e&f&g");
77+
78+
List<InputClause> inputClauses = decisionTable.getInputs();
79+
assertThat(inputClauses).hasSize(4);
80+
81+
List<OutputClause> outputClauses = decisionTable.getOutputs();
82+
assertThat(outputClauses).hasSize(1);
83+
84+
/*
85+
* Verify input entry extension elements
86+
*/
87+
assertThat(decisionTable.getRules().get(0).getInputEntries().get(3).getInputEntry().getExtensionElements().get("operator").get(0).getElementText()).isEqualTo("NONE OF");
88+
assertThat(decisionTable.getRules().get(0).getInputEntries().get(3).getInputEntry().getExtensionElements().get("expression").get(0).getElementText()).isEqualTo("20, 13");
89+
assertThat(decisionTable.getRules().get(1).getInputEntries().get(3).getInputEntry().getExtensionElements().get("operator").get(0).getElementText()).isEqualTo("ANY OF");
90+
assertThat(decisionTable.getRules().get(1).getInputEntries().get(3).getInputEntry().getExtensionElements().get("expression").get(0).getElementText()).isEqualTo("\"20\", \"13\"");
91+
assertThat(decisionTable.getRules().get(2).getInputEntries().get(3).getInputEntry().getExtensionElements().get("operator").get(0).getElementText()).isEqualTo("ALL OF");
92+
assertThat(decisionTable.getRules().get(2).getInputEntries().get(3).getInputEntry().getExtensionElements().get("expression").get(0).getElementText()).isEqualTo("20");
93+
}
94+
}

0 commit comments

Comments
 (0)