Skip to content

Commit 85a1cb4

Browse files
committed
Merge branch 'main' into flowable-release-8.1.0
2 parents b64151d + f1c0d4d commit 85a1cb4

43 files changed

Lines changed: 865 additions & 30 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

modules/flowable-cmmn-api/src/main/java/org/flowable/cmmn/api/runtime/PlanItemInstanceTransitionBuilder.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,16 @@ public interface PlanItemInstanceTransitionBuilder {
113113
* Starts a plan item instance, this typically will executes it associated behavior.
114114
*/
115115
void start();
116+
117+
/**
118+
* Suspend a plan item instance.
119+
*/
120+
void suspend();
121+
122+
/**
123+
* Sets the plan item instance to available state.
124+
*/
125+
void resume();
116126

117127
/**
118128
* Manually terminates a plan item instance.

modules/flowable-cmmn-engine-configurator/src/test/java/org/flowable/cmmn/test/ProcessTaskTest.java

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2288,4 +2288,56 @@ public void testInMappingArrayNodeWithSyncFirstStep() {
22882288
}
22892289
}
22902290

2291+
@Test
2292+
@CmmnDeployment
2293+
@org.flowable.engine.test.Deployment(resources = "org/flowable/cmmn/test/oneTaskProcess.bpmn20.xml")
2294+
public void testTerminateCaseInstanceWithBlockingProcessTaskHistoricPlanItemState() {
2295+
CaseInstance caseInstance = cmmnRuntimeService.createCaseInstanceBuilder().caseDefinitionKey("myCase").start();
2296+
2297+
// Complete the human task so the process task becomes active
2298+
Task caseTask = cmmnTaskService.createTaskQuery().caseInstanceId(caseInstance.getId()).singleResult();
2299+
assertThat(caseTask).isNotNull();
2300+
assertThat(caseTask.getName()).isEqualTo("Task A");
2301+
cmmnTaskService.complete(caseTask.getId());
2302+
2303+
// Process task should now be active with a child process instance
2304+
PlanItemInstance processTaskPlanItem = cmmnRuntimeService.createPlanItemInstanceQuery()
2305+
.caseInstanceId(caseInstance.getId())
2306+
.planItemInstanceState(PlanItemInstanceState.ACTIVE)
2307+
.planItemDefinitionType(PlanItemDefinitionType.PROCESS_TASK)
2308+
.singleResult();
2309+
assertThat(processTaskPlanItem).isNotNull();
2310+
2311+
// The child process instance should have a user task
2312+
Task processTask = processEngine.getTaskService().createTaskQuery().singleResult();
2313+
assertThat(processTask).isNotNull();
2314+
assertThat(processTask.getName()).isEqualTo("my task");
2315+
2316+
// Terminate the case instance
2317+
cmmnRuntimeService.terminateCaseInstance(caseInstance.getId());
2318+
2319+
// Verify runtime data is cleaned up
2320+
assertThat(cmmnRuntimeService.createPlanItemInstanceQuery().caseInstanceId(caseInstance.getId()).count()).isZero();
2321+
assertThat(processEngine.getTaskService().createTaskQuery().count()).isZero();
2322+
assertThat(processEngineRuntimeService.createProcessInstanceQuery().count()).isZero();
2323+
2324+
if (CmmnHistoryTestHelper.isHistoryLevelAtLeast(HistoryLevel.ACTIVITY, cmmnEngineConfiguration)) {
2325+
// The historic case instance should be terminated
2326+
HistoricCaseInstance historicCaseInstance = cmmnHistoryService.createHistoricCaseInstanceQuery()
2327+
.caseInstanceId(caseInstance.getId())
2328+
.singleResult();
2329+
assertThat(historicCaseInstance).isNotNull();
2330+
assertThat(historicCaseInstance.getState()).isEqualTo(CaseInstanceState.TERMINATED);
2331+
2332+
// The historic plan item for the process task should be terminated, not active
2333+
HistoricPlanItemInstance historicProcessTaskPlanItem = cmmnHistoryService.createHistoricPlanItemInstanceQuery()
2334+
.planItemInstanceId(processTaskPlanItem.getId())
2335+
.singleResult();
2336+
assertThat(historicProcessTaskPlanItem).isNotNull();
2337+
assertThat(historicProcessTaskPlanItem.getState()).isEqualTo(PlanItemInstanceState.TERMINATED);
2338+
assertThat(historicProcessTaskPlanItem.getEndedTime()).isNotNull();
2339+
assertThat(historicProcessTaskPlanItem.getTerminatedTime()).isNotNull();
2340+
}
2341+
}
2342+
22912343
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<definitions xmlns="http://www.omg.org/spec/CMMN/20151109/MODEL" xmlns:dc="http://www.omg.org/spec/CMMN/20151109/DC" xmlns:di="http://www.omg.org/spec/CMMN/20151109/DI"
3+
xmlns:cmmndi="http://www.omg.org/spec/CMMN/20151109/CMMNDI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" targetNamespace="http://flowable.org/cmmn">
4+
5+
<case id="myCase">
6+
<casePlanModel id="myPlanModel" name="My CasePlanModel">
7+
8+
<planItem id="planItem1" name="Task A" definitionRef="humanTask" />
9+
<planItem id="planItem2" name="The Process" definitionRef="theProcess">
10+
<entryCriterion sentryRef="sentry1" />
11+
</planItem>
12+
13+
<sentry id="sentry1">
14+
<planItemOnPart sourceRef="planItem1">
15+
<standardEvent>complete</standardEvent>
16+
</planItemOnPart>
17+
</sentry>
18+
19+
<humanTask id="humanTask" name="Task A" />
20+
<processTask id="theProcess" processRef="oneTaskProcess" isBlocking="true" />
21+
22+
</casePlanModel>
23+
</case>
24+
25+
<process id="oneTaskProcess" name="The One Task process" externalRef="oneTask" />
26+
27+
</definitions>

modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/CmmnEngineAgenda.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,15 @@ public interface CmmnEngineAgenda extends Agenda {
7474

7575
void planExitPlanItemInstanceOperation(PlanItemInstanceEntity planItemInstanceEntity, String exitCriterionId, String exitType, String exitEventType);
7676

77+
void planSuspendPlanItemInstanceOperation(PlanItemInstanceEntity planItemInstanceEntity);
78+
7779
void planTerminatePlanItemInstanceOperation(PlanItemInstanceEntity planItemInstanceEntity, String exitType, String exitEventType);
7880

7981
void planTriggerPlanItemInstanceOperation(PlanItemInstanceEntity planItemInstanceEntity);
8082

8183
void planChangePlanItemInstanceToAvailableOperation(PlanItemInstanceEntity planItemInstanceEntity);
84+
85+
void planResumePlanItemInstanceOperation(PlanItemInstanceEntity planItemInstanceEntity);
8286

8387
void planCompleteCaseInstanceOperation(CaseInstanceEntity caseInstanceEntity);
8488

modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/DefaultCmmnEngineAgenda.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@
4141
import org.flowable.cmmn.engine.impl.agenda.operation.ReactivateCaseInstanceOperation;
4242
import org.flowable.cmmn.engine.impl.agenda.operation.ReactivatePlanItemInstanceOperation;
4343
import org.flowable.cmmn.engine.impl.agenda.operation.ReactivatePlanModelInstanceOperation;
44+
import org.flowable.cmmn.engine.impl.agenda.operation.ResumePlanItemInstanceOperation;
4445
import org.flowable.cmmn.engine.impl.agenda.operation.StartPlanItemInstanceOperation;
46+
import org.flowable.cmmn.engine.impl.agenda.operation.SuspendPlanItemInstanceOperation;
4547
import org.flowable.cmmn.engine.impl.agenda.operation.TerminateCaseInstanceOperation;
4648
import org.flowable.cmmn.engine.impl.agenda.operation.TerminatePlanItemInstanceOperation;
4749
import org.flowable.cmmn.engine.impl.agenda.operation.TriggerPlanItemInstanceOperation;
@@ -253,6 +255,11 @@ public void planOccurPlanItemInstanceOperation(PlanItemInstanceEntity planItemIn
253255
public void planExitPlanItemInstanceOperation(PlanItemInstanceEntity planItemInstanceEntity, String exitCriterionId, String exitType, String exitEventType) {
254256
addOperation(new ExitPlanItemInstanceOperation(commandContext, planItemInstanceEntity, exitCriterionId, exitType, exitEventType));
255257
}
258+
259+
@Override
260+
public void planSuspendPlanItemInstanceOperation(PlanItemInstanceEntity planItemInstanceEntity) {
261+
addOperation(new SuspendPlanItemInstanceOperation(commandContext, planItemInstanceEntity));
262+
}
256263

257264
@Override
258265
public void planTerminatePlanItemInstanceOperation(PlanItemInstanceEntity planItemInstanceEntity, String exitType, String exitEventType) {
@@ -263,6 +270,11 @@ public void planTerminatePlanItemInstanceOperation(PlanItemInstanceEntity planIt
263270
public void planChangePlanItemInstanceToAvailableOperation(PlanItemInstanceEntity planItemInstanceEntity) {
264271
addOperation(new ChangePlanItemInstanceToAvailableOperation(commandContext, planItemInstanceEntity));
265272
}
273+
274+
@Override
275+
public void planResumePlanItemInstanceOperation(PlanItemInstanceEntity planItemInstanceEntity) {
276+
addOperation(new ResumePlanItemInstanceOperation(commandContext, planItemInstanceEntity));
277+
}
266278

267279
@Override
268280
public void planTriggerPlanItemInstanceOperation(PlanItemInstanceEntity planItemInstanceEntity) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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.cmmn.engine.impl.agenda.operation;
14+
15+
import java.util.List;
16+
17+
import org.flowable.cmmn.api.runtime.PlanItemInstanceState;
18+
import org.flowable.cmmn.engine.CmmnEngineConfiguration;
19+
import org.flowable.cmmn.engine.impl.persistence.entity.PlanItemInstanceEntity;
20+
import org.flowable.cmmn.engine.impl.util.CommandContextUtil;
21+
import org.flowable.cmmn.model.PlanItemDefinition;
22+
import org.flowable.cmmn.model.PlanItemTransition;
23+
import org.flowable.cmmn.model.TimerEventListener;
24+
import org.flowable.common.engine.api.FlowableIllegalStateException;
25+
import org.flowable.common.engine.impl.interceptor.CommandContext;
26+
import org.flowable.job.service.impl.persistence.entity.SuspendedJobEntity;
27+
28+
/**
29+
* @author Tijs Rademakers
30+
*/
31+
public class ResumePlanItemInstanceOperation extends AbstractChangePlanItemInstanceStateOperation {
32+
33+
public ResumePlanItemInstanceOperation(CommandContext commandContext, PlanItemInstanceEntity planItemInstanceEntity) {
34+
super(commandContext, planItemInstanceEntity);
35+
}
36+
37+
@Override
38+
public String getLifeCycleTransition() {
39+
return PlanItemTransition.SUSPEND;
40+
}
41+
42+
@Override
43+
public String getNewState() {
44+
return PlanItemInstanceState.AVAILABLE;
45+
}
46+
47+
@Override
48+
protected void internalExecute() {
49+
planItemInstanceEntity.setLastAvailableTime(getCurrentTime(commandContext));
50+
51+
PlanItemDefinition planItemDefinition = planItemInstanceEntity.getPlanItem().getPlanItemDefinition();
52+
if (planItemDefinition instanceof TimerEventListener) {
53+
CmmnEngineConfiguration cmmnEngineConfiguration = CommandContextUtil.getCmmnEngineConfiguration(commandContext);
54+
List<SuspendedJobEntity> suspendedJobs = cmmnEngineConfiguration.getJobServiceConfiguration().getSuspendedJobEntityManager().findJobsBySubScopeId(planItemInstanceEntity.getId());
55+
if (suspendedJobs != null && !suspendedJobs.isEmpty()) {
56+
cmmnEngineConfiguration.getJobServiceConfiguration().getJobService().activateSuspendedJob(suspendedJobs.get(0));
57+
}
58+
}
59+
60+
CommandContextUtil.getCmmnHistoryManager(commandContext).recordPlanItemInstanceAvailable(planItemInstanceEntity);
61+
}
62+
63+
@Override
64+
public boolean isStateNotChanged(String oldState, String newState) {
65+
if (oldState != null && !PlanItemInstanceState.SUSPENDED.equals(oldState)) {
66+
throw new FlowableIllegalStateException("plan item instance can only be resumed if the state is suspended");
67+
}
68+
69+
return oldState != null && oldState.equals(newState) && abortOperationIfNewStateEqualsOldState();
70+
}
71+
72+
@Override
73+
public boolean abortOperationIfNewStateEqualsOldState() {
74+
return true;
75+
}
76+
77+
@Override
78+
public String getOperationName() {
79+
return null; // Default one is ok.
80+
}
81+
82+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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.cmmn.engine.impl.agenda.operation;
14+
15+
import java.util.List;
16+
17+
import org.flowable.cmmn.api.runtime.PlanItemInstanceState;
18+
import org.flowable.cmmn.engine.CmmnEngineConfiguration;
19+
import org.flowable.cmmn.engine.impl.persistence.entity.PlanItemInstanceEntity;
20+
import org.flowable.cmmn.engine.impl.util.CommandContextUtil;
21+
import org.flowable.cmmn.model.PlanItemDefinition;
22+
import org.flowable.cmmn.model.PlanItemTransition;
23+
import org.flowable.cmmn.model.TimerEventListener;
24+
import org.flowable.common.engine.api.FlowableIllegalStateException;
25+
import org.flowable.common.engine.impl.interceptor.CommandContext;
26+
import org.flowable.job.service.impl.persistence.entity.TimerJobEntity;
27+
28+
public class SuspendPlanItemInstanceOperation extends AbstractChangePlanItemInstanceStateOperation {
29+
30+
public SuspendPlanItemInstanceOperation(CommandContext commandContext, PlanItemInstanceEntity planItemInstanceEntity) {
31+
super(commandContext, planItemInstanceEntity);
32+
}
33+
34+
@Override
35+
public String getNewState() {
36+
return PlanItemInstanceState.SUSPENDED;
37+
}
38+
39+
@Override
40+
public String getLifeCycleTransition() {
41+
return PlanItemTransition.SUSPEND;
42+
}
43+
44+
@Override
45+
protected void internalExecute() {
46+
planItemInstanceEntity.setLastSuspendedTime(getCurrentTime(commandContext));
47+
48+
PlanItemDefinition planItemDefinition = planItemInstanceEntity.getPlanItem().getPlanItemDefinition();
49+
if (planItemDefinition instanceof TimerEventListener) {
50+
CmmnEngineConfiguration cmmnEngineConfiguration = CommandContextUtil.getCmmnEngineConfiguration(commandContext);
51+
List<TimerJobEntity> timerJobs = cmmnEngineConfiguration.getJobServiceConfiguration().getTimerJobEntityManager().findJobsByScopeIdAndSubScopeId(
52+
planItemInstanceEntity.getCaseInstanceId(), planItemInstanceEntity.getId());
53+
if (timerJobs != null && !timerJobs.isEmpty()) {
54+
cmmnEngineConfiguration.getJobServiceConfiguration().getJobService().moveJobToSuspendedJob(timerJobs.get(0));
55+
}
56+
}
57+
58+
CommandContextUtil.getCmmnHistoryManager(commandContext).recordPlanItemInstanceSuspended(planItemInstanceEntity);
59+
}
60+
61+
@Override
62+
public boolean isStateNotChanged(String oldState, String newState) {
63+
if (oldState != null && oldState.equals(newState)) {
64+
throw new FlowableIllegalStateException("plan item instance is already suspended");
65+
}
66+
67+
return false;
68+
}
69+
70+
@Override
71+
public boolean abortOperationIfNewStateEqualsOldState() {
72+
return true;
73+
}
74+
75+
@Override
76+
public String getOperationName() {
77+
return "[Suspend plan item]";
78+
}
79+
}

modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/behavior/impl/CaseTaskActivityBehavior.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,17 @@ public void deleteChildEntity(CommandContext commandContext, DelegatePlanItemIns
230230
caseInstance, CaseInstanceState.TERMINATED, cmmnEngineConfiguration.getClock().getCurrentTime());
231231
caseInstanceEntityManager.delete(caseInstance.getId(), cascade, null);
232232
}
233-
233+
234+
// This is not the regular termination through the agenda, but the historic plan item state still needs to be correct.
235+
// The child case instance needs to be deleted synchronously (not deferred via the agenda) to preserve entity link cleanup ordering.
236+
PlanItemInstanceEntity planItemInstanceEntity = (PlanItemInstanceEntity) delegatePlanItemInstance;
237+
if (!PlanItemInstanceState.TERMINATED.equals(planItemInstanceEntity.getState())) {
238+
planItemInstanceEntity.setState(PlanItemInstanceState.TERMINATED);
239+
planItemInstanceEntity.setEndedTime(CommandContextUtil.getCmmnEngineConfiguration(commandContext).getClock().getCurrentTime());
240+
planItemInstanceEntity.setTerminatedTime(planItemInstanceEntity.getEndedTime());
241+
CommandContextUtil.getCmmnHistoryManager(commandContext).recordPlanItemInstanceTerminated(planItemInstanceEntity);
242+
}
243+
234244
} else {
235245
throw new FlowableException("Can only delete a child entity for a plan item with reference type " + ReferenceTypes.PLAN_ITEM_CHILD_CASE + " for " + delegatePlanItemInstance);
236246
}

modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/behavior/impl/ProcessTaskActivityBehavior.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,18 @@ protected void deleteProcessInstance(CommandContext commandContext, DelegatePlan
179179
@Override
180180
public void deleteChildEntity(CommandContext commandContext, DelegatePlanItemInstance delegatePlanItemInstance, boolean cascade) {
181181
if (ReferenceTypes.PLAN_ITEM_CHILD_PROCESS.equals(delegatePlanItemInstance.getReferenceType())) {
182-
delegatePlanItemInstance.setState(PlanItemInstanceState.TERMINATED); // This is not the regular termination, but the state still needs to be correct
182+
// This is not the regular termination through the agenda, but the historic plan item state still needs to be correct.
183+
// The state needs to be set before deleting the process instance, because the process instance deletion triggers
184+
// a ChildProcessInstanceStateChangeCallback which re-enters the CMMN engine to terminate the plan item.
185+
// With the state already set to TERMINATED, this callback becomes a no-op and avoids re-triggering repetition rules.
186+
PlanItemInstanceEntity planItemInstanceEntity = (PlanItemInstanceEntity) delegatePlanItemInstance;
187+
if (!PlanItemInstanceState.TERMINATED.equals(planItemInstanceEntity.getState())) {
188+
planItemInstanceEntity.setState(PlanItemInstanceState.TERMINATED);
189+
planItemInstanceEntity.setEndedTime(CommandContextUtil.getCmmnEngineConfiguration(commandContext).getClock().getCurrentTime());
190+
planItemInstanceEntity.setTerminatedTime(planItemInstanceEntity.getEndedTime());
191+
CommandContextUtil.getCmmnHistoryManager(commandContext).recordPlanItemInstanceTerminated(planItemInstanceEntity);
192+
}
193+
183194
deleteProcessInstance(commandContext, delegatePlanItemInstance);
184195
} else {
185196
throw new FlowableException("Can only delete a child entity for a plan item with reference type " + ReferenceTypes.PLAN_ITEM_CHILD_PROCESS + " for " + delegatePlanItemInstance);

0 commit comments

Comments
 (0)