Skip to content

Commit 0598835

Browse files
Fix issue with moving parallel MI activities to a single activity
1 parent 8d3c93c commit 0598835

File tree

7 files changed

+354
-6
lines changed

7 files changed

+354
-6
lines changed

modules/flowable-engine/src/main/java/org/flowable/engine/impl/dynamic/AbstractDynamicStateManager.java

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -221,10 +221,10 @@ public List<MoveExecutionEntityContainer> resolveMoveExecutionEntityContainers(C
221221
}
222222
}
223223

224-
//If inside a multiInstance, we create one container for each execution
224+
// If inside a multiInstance, we create one container for each execution
225225
if (isInsideMultiInstance) {
226226

227-
//We group by the parentId (executions belonging to the same parent execution instance
227+
// We group by the parentId (executions belonging to the same parent execution instance
228228
// i.e. gateways nested in MultiInstance subProcesses, need to be in the same move container)
229229
Stream<ExecutionEntity> executionEntitiesStream = activityExecutions.stream();
230230
if (miExecution != null) {
@@ -244,10 +244,27 @@ public List<MoveExecutionEntityContainer> resolveMoveExecutionEntityContainers(C
244244
}
245245
}
246246

247-
//Create a move container for each execution group (executionList)
248-
Stream.concat(activitiesExecutionsByMultiInstanceParentId.values().stream(), Stream.of(activitiesExecutionsNotInMultiInstanceParent))
249-
.filter(executions -> executions != null && !executions.isEmpty())
250-
.forEach(executions -> moveExecutionEntityContainerList.add(createMoveExecutionEntityContainer(activityContainer, executions, commandContext)));
247+
List<ExecutionEntity> combinedExecutions = new ArrayList<>();
248+
if (!activitiesExecutionsByMultiInstanceParentId.isEmpty()) {
249+
for (String parentId : activitiesExecutionsByMultiInstanceParentId.keySet()) {
250+
List<ExecutionEntity> miExecutions = activitiesExecutionsByMultiInstanceParentId.get(parentId);
251+
if (!miExecutions.isEmpty()) {
252+
if (isTopLevelMultiInstanceRoot(miExecutions.get(0))) {
253+
combinedExecutions.addAll(miExecutions);
254+
} else {
255+
moveExecutionEntityContainerList.add(createMoveExecutionEntityContainer(activityContainer, miExecutions, commandContext));
256+
}
257+
}
258+
}
259+
}
260+
261+
if (!activitiesExecutionsNotInMultiInstanceParent.isEmpty()) {
262+
combinedExecutions.addAll(activitiesExecutionsNotInMultiInstanceParent);
263+
}
264+
265+
if (!combinedExecutions.isEmpty()) {
266+
moveExecutionEntityContainerList.add(createMoveExecutionEntityContainer(activityContainer, combinedExecutions, commandContext));
267+
}
251268
}
252269
}
253270

@@ -1456,6 +1473,24 @@ protected boolean isFlowElementMultiInstance(FlowElement flowElement) {
14561473
return false;
14571474
}
14581475

1476+
protected boolean isTopLevelMultiInstanceRoot(ExecutionEntity execution) {
1477+
boolean topLevelMultiInstanceRoot = false;
1478+
if (execution.isMultiInstanceRoot()) {
1479+
topLevelMultiInstanceRoot = true;
1480+
ExecutionEntity parentExecution = execution.getParent();
1481+
while (parentExecution != null) {
1482+
if (parentExecution.isMultiInstanceRoot()) {
1483+
topLevelMultiInstanceRoot = false;
1484+
break;
1485+
}
1486+
1487+
parentExecution = parentExecution.getParent();
1488+
}
1489+
}
1490+
1491+
return topLevelMultiInstanceRoot;
1492+
}
1493+
14591494
protected boolean hasSameMultiInstanceConfig(FlowElement sourceElement, FlowElement targetElement) {
14601495
MultiInstanceLoopCharacteristics sourceMIConfig = null;
14611496
if (sourceElement instanceof Activity) {

modules/flowable-engine/src/test/java/org/flowable/engine/test/api/runtime/changestate/ChangeStateForMultiInstanceTest.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,49 @@ public void testSetCurrentActivityToOtherParallelMultiInstanceTask() {
469469
taskService.complete(task.getId());
470470
assertProcessEnded(processInstance.getId());
471471
}
472+
473+
@Test
474+
@Deployment(resources = "org/flowable/engine/test/api/parallelTaskWithMI.bpmn20.xml")
475+
public void testMoveFromParallelMultiInstanceTasksToOneActivity() {
476+
ProcessInstance processInstance = runtimeService.createProcessInstanceBuilder().processDefinitionKey("startParallelProcess")
477+
.start();
478+
479+
completeTask(taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult());
480+
481+
List<Execution> parallelExecutions = runtimeService.createExecutionQuery().processInstanceId(processInstance.getId()).onlyChildExecutions().list();
482+
assertThat(parallelExecutions).hasSize(6);
483+
List<Task> activeParallelTasks = taskService.createTaskQuery().processInstanceId(processInstance.getId()).active().list();
484+
assertThat(activeParallelTasks).hasSize(4);
485+
486+
List<String> currentActivityIds = new ArrayList<>();
487+
currentActivityIds.add("task1");
488+
currentActivityIds.add("task2");
489+
490+
runtimeService.createChangeActivityStateBuilder()
491+
.processInstanceId(processInstance.getId())
492+
.moveActivityIdsToSingleActivityId(currentActivityIds, "taskBefore")
493+
.changeState();
494+
495+
parallelExecutions = runtimeService.createExecutionQuery().processInstanceId(processInstance.getId()).onlyChildExecutions().list();
496+
assertThat(parallelExecutions).hasSize(1);
497+
Task task = taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult();
498+
assertThat(task.getTaskDefinitionKey()).isEqualTo("taskBefore");
499+
taskService.complete(task.getId());
500+
501+
assertThat(runtimeService.createExecutionQuery().processInstanceId(processInstance.getId()).onlyChildExecutions().count()).isEqualTo(6);
502+
assertThat(taskService.createTaskQuery().processInstanceId(processInstance.getId()).active().count()).isEqualTo(4);
503+
504+
List<Task> tasks = taskService.createTaskQuery().processInstanceId(processInstance.getId()).list();
505+
for (Task parallelTask : tasks) {
506+
taskService.complete(parallelTask.getId());
507+
}
508+
509+
task = taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult();
510+
assertThat(task.getTaskDefinitionKey()).isEqualTo("taskAfter");
511+
taskService.complete(task.getId());
512+
513+
assertProcessEnded(processInstance.getId());
514+
}
472515

473516
@Test
474517
@Deployment(resources = "org/flowable/engine/test/api/multiInstanceParallelSubProcess.bpmn20.xml")
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<definitions id="definitions"
3+
xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
4+
targetNamespace="Examples">
5+
6+
<process id="startParallelProcess">
7+
8+
<startEvent id="theStart"/>
9+
<sequenceFlow id="flow1" sourceRef="theStart" targetRef="taskBefore"/>
10+
11+
<userTask id="taskBefore" name="Task before sub process"/>
12+
<sequenceFlow id="flow2" sourceRef="taskBefore" targetRef="parallelFork"/>
13+
14+
<parallelGateway id="parallelFork"/>
15+
16+
<sequenceFlow id="flow3" sourceRef="parallelFork" targetRef="task1"/>
17+
<userTask id="task1">
18+
<multiInstanceLoopCharacteristics isSequential="false">
19+
<loopCardinality>2</loopCardinality>
20+
</multiInstanceLoopCharacteristics>
21+
</userTask>
22+
<sequenceFlow id="flow4" sourceRef="parallelFork" targetRef="task2"/>
23+
<userTask id="task2">
24+
<multiInstanceLoopCharacteristics isSequential="false">
25+
<loopCardinality>2</loopCardinality>
26+
</multiInstanceLoopCharacteristics>
27+
</userTask>
28+
29+
<sequenceFlow id="flow5" sourceRef="task1" targetRef="parallelJoin"/>
30+
<sequenceFlow id="flow6" sourceRef="task2" targetRef="parallelJoin"/>
31+
32+
<parallelGateway id="parallelJoin"/>
33+
34+
<sequenceFlow id="flow7" sourceRef="parallelJoin" targetRef="taskAfter"/>
35+
<userTask id="taskAfter" name="Task after sub process"/>
36+
37+
<sequenceFlow id="flow8" sourceRef="taskAfter" targetRef="theEnd"/>
38+
<endEvent id="theEnd"/>
39+
40+
</process>
41+
42+
</definitions>

modules/flowable-rest/src/main/java/org/flowable/rest/service/api/RestUrls.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ public final class RestUrls {
9090
public static final String SEGMENT_SOURCE = "source";
9191
public static final String SEGMENT_SOURCE_EXTRA = "source-extra";
9292
public static final String SEGMENT_DIAGRAM = "diagram";
93+
public static final String SEGMENT_CHANGE_STATE = "change-state";
9394
public static final String SEGMENT_SIGNALS = "signals";
9495
public static final String SEGMENT_IMAGE = "image";
9596
public static final String SEGMENT_START_FORM = "start-form";
@@ -341,6 +342,11 @@ public final class RestUrls {
341342
* URL template for a single process instance: <i>runtime/process-instances/{0:processInstanceId}</i>
342343
*/
343344
public static final String[] URL_PROCESS_INSTANCE = { SEGMENT_RUNTIME_RESOURCES, SEGMENT_PROCESS_INSTANCE_RESOURCE, "{0}" };
345+
346+
/**
347+
* URL template for changing the activity state for a process instance: <i>runtime/process-instances/{0:processInstanceId}/change-state</i>
348+
*/
349+
public static final String[] URL_PROCESS_INSTANCE_CHANGE_STATE = { SEGMENT_RUNTIME_RESOURCES, SEGMENT_PROCESS_INSTANCE_RESOURCE, "{0}", SEGMENT_CHANGE_STATE};
344350

345351
/**
346352
* URL template for the diagram for a single process instance: <i>runtime/process-instances/{0:processInstanceId}/diagram</i>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
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.rest.service.api.runtime;
15+
16+
import static org.assertj.core.api.Assertions.assertThat;
17+
18+
import java.util.List;
19+
20+
import org.apache.http.HttpStatus;
21+
import org.apache.http.client.methods.CloseableHttpResponse;
22+
import org.apache.http.client.methods.HttpPost;
23+
import org.apache.http.entity.StringEntity;
24+
import org.flowable.common.engine.impl.identity.Authentication;
25+
import org.flowable.engine.runtime.ProcessInstance;
26+
import org.flowable.engine.test.Deployment;
27+
import org.flowable.rest.service.BaseSpringRestTestCase;
28+
import org.flowable.rest.service.api.RestUrls;
29+
import org.flowable.task.api.Task;
30+
import org.junit.jupiter.api.Test;
31+
32+
import com.fasterxml.jackson.databind.node.ArrayNode;
33+
import com.fasterxml.jackson.databind.node.ObjectNode;
34+
35+
public class ProcessInstanceChangeActivityStateResourceTest extends BaseSpringRestTestCase {
36+
37+
@Test
38+
@Deployment(resources = { "org/flowable/rest/service/api/runtime/parallelTask.bpmn20.xml" })
39+
public void testChangeActivityStateManyToOne() throws Exception {
40+
Authentication.setAuthenticatedUserId("testUser");
41+
ProcessInstance processInstance = runtimeService.createProcessInstanceBuilder()
42+
.processDefinitionKey("startParallelProcess")
43+
.start();
44+
Task task = taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult();
45+
assertThat(task.getTaskDefinitionKey()).isEqualTo("taskBefore");
46+
47+
taskService.complete(task.getId());
48+
49+
assertThat(taskService.createTaskQuery().processInstanceId(processInstance.getId()).count()).isEqualTo(2);
50+
51+
assertThat(runtimeService.createExecutionQuery().processInstanceId(processInstance.getId()).count()).isEqualTo(3);
52+
53+
Authentication.setAuthenticatedUserId(null);
54+
55+
ObjectNode requestNode = objectMapper.createObjectNode();
56+
ArrayNode cancelActivityArray = requestNode.putArray("cancelActivityIds");
57+
cancelActivityArray.add("task1");
58+
cancelActivityArray.add("task2");
59+
60+
ArrayNode startActivityArray = requestNode.putArray("startActivityIds");
61+
startActivityArray.add("taskBefore");
62+
63+
HttpPost httpPost = new HttpPost(buildUrl(RestUrls.URL_PROCESS_INSTANCE_CHANGE_STATE, processInstance.getId()));
64+
httpPost.setEntity(new StringEntity(requestNode.toString()));
65+
CloseableHttpResponse response = executeRequest(httpPost, HttpStatus.SC_OK);
66+
closeResponse(response);
67+
68+
assertThat(taskService.createTaskQuery().processInstanceId(processInstance.getId()).count()).isEqualTo(1);
69+
70+
task = taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult();
71+
assertThat(task.getTaskDefinitionKey()).isEqualTo("taskBefore");
72+
73+
assertThat(runtimeService.createExecutionQuery().processInstanceId(processInstance.getId()).count()).isEqualTo(2);
74+
75+
taskService.complete(task.getId());
76+
77+
assertThat(taskService.createTaskQuery().processInstanceId(processInstance.getId()).count()).isEqualTo(2);
78+
79+
List<Task> tasks = taskService.createTaskQuery().processInstanceId(processInstance.getId()).list();
80+
for (Task parallelTask : tasks) {
81+
taskService.complete(parallelTask.getId());
82+
}
83+
84+
task = taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult();
85+
assertThat(task.getTaskDefinitionKey()).isEqualTo("taskAfter");
86+
87+
taskService.complete(task.getId());
88+
89+
assertProcessEnded(processInstance.getId());
90+
}
91+
92+
@Test
93+
@Deployment(resources = { "org/flowable/rest/service/api/runtime/parallelTaskWithMI.bpmn20.xml" })
94+
public void testChangeActivityStateManyToOneWithMI() throws Exception {
95+
Authentication.setAuthenticatedUserId("testUser");
96+
ProcessInstance processInstance = runtimeService.createProcessInstanceBuilder()
97+
.processDefinitionKey("startParallelProcess")
98+
.start();
99+
Task task = taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult();
100+
assertThat(task.getTaskDefinitionKey()).isEqualTo("taskBefore");
101+
102+
taskService.complete(task.getId());
103+
104+
assertThat(taskService.createTaskQuery().processInstanceId(processInstance.getId()).count()).isEqualTo(4);
105+
106+
assertThat(runtimeService.createExecutionQuery().processInstanceId(processInstance.getId()).count()).isEqualTo(7);
107+
108+
Authentication.setAuthenticatedUserId(null);
109+
110+
ObjectNode requestNode = objectMapper.createObjectNode();
111+
ArrayNode cancelActivityArray = requestNode.putArray("cancelActivityIds");
112+
cancelActivityArray.add("task1");
113+
cancelActivityArray.add("task2");
114+
115+
ArrayNode startActivityArray = requestNode.putArray("startActivityIds");
116+
startActivityArray.add("taskBefore");
117+
118+
HttpPost httpPost = new HttpPost(buildUrl(RestUrls.URL_PROCESS_INSTANCE_CHANGE_STATE, processInstance.getId()));
119+
httpPost.setEntity(new StringEntity(requestNode.toString()));
120+
CloseableHttpResponse response = executeRequest(httpPost, HttpStatus.SC_OK);
121+
closeResponse(response);
122+
123+
assertThat(taskService.createTaskQuery().processInstanceId(processInstance.getId()).count()).isEqualTo(1);
124+
125+
task = taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult();
126+
assertThat(task.getTaskDefinitionKey()).isEqualTo("taskBefore");
127+
128+
assertThat(runtimeService.createExecutionQuery().processInstanceId(processInstance.getId()).count()).isEqualTo(2);
129+
130+
taskService.complete(task.getId());
131+
132+
assertThat(taskService.createTaskQuery().processInstanceId(processInstance.getId()).count()).isEqualTo(4);
133+
134+
List<Task> tasks = taskService.createTaskQuery().processInstanceId(processInstance.getId()).list();
135+
for (Task parallelTask : tasks) {
136+
taskService.complete(parallelTask.getId());
137+
}
138+
139+
task = taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult();
140+
assertThat(task.getTaskDefinitionKey()).isEqualTo("taskAfter");
141+
142+
taskService.complete(task.getId());
143+
144+
assertProcessEnded(processInstance.getId());
145+
}
146+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<definitions id="definitions"
3+
xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
4+
targetNamespace="Examples">
5+
6+
<process id="startParallelProcess">
7+
8+
<startEvent id="theStart"/>
9+
<sequenceFlow id="flow1" sourceRef="theStart" targetRef="taskBefore"/>
10+
11+
<userTask id="taskBefore" name="Task before sub process"/>
12+
<sequenceFlow id="flow2" sourceRef="taskBefore" targetRef="parallelFork"/>
13+
14+
<parallelGateway id="parallelFork"/>
15+
16+
<sequenceFlow id="flow3" sourceRef="parallelFork" targetRef="task1"/>
17+
<userTask id="task1"/>
18+
<sequenceFlow id="flow4" sourceRef="parallelFork" targetRef="task2"/>
19+
<userTask id="task2"/>
20+
21+
<sequenceFlow id="flow5" sourceRef="task1" targetRef="parallelJoin"/>
22+
<sequenceFlow id="flow6" sourceRef="task2" targetRef="parallelJoin"/>
23+
24+
<parallelGateway id="parallelJoin"/>
25+
26+
<sequenceFlow id="flow7" sourceRef="parallelJoin" targetRef="taskAfter"/>
27+
<userTask id="taskAfter" name="Task after sub process"/>
28+
29+
<sequenceFlow id="flow8" sourceRef="taskAfter" targetRef="theEnd"/>
30+
<endEvent id="theEnd"/>
31+
32+
</process>
33+
34+
</definitions>

0 commit comments

Comments
 (0)