From 997d5eb8d8d72d1cc25e3b2d0f97026d574d4ea0 Mon Sep 17 00:00:00 2001 From: Tijs Rademakers Date: Mon, 9 Mar 2026 13:25:10 +0100 Subject: [PATCH 1/3] Adding support for suspending a timer event listener --- .../PlanItemInstanceTransitionBuilder.java | 10 ++ .../engine/impl/agenda/CmmnEngineAgenda.java | 4 + .../impl/agenda/DefaultCmmnEngineAgenda.java | 11 +++ ...ePlanItemInstanceToAvailableOperation.java | 30 ++++++ .../SuspendPlanItemInstanceOperation.java | 96 +++++++++++++++++++ .../cmmn/engine/impl/cmd/ActivateTaskCmd.java | 19 ++++ .../impl/cmd/ResumePlanItemInstanceCmd.java | 30 ++++++ .../impl/cmd/SuspendPlanItemInstanceCmd.java | 30 ++++++ .../cmmn/engine/impl/cmd/SuspendTaskCmd.java | 20 ++++ ...PlanItemInstanceTransitionBuilderImpl.java | 14 +++ .../eventlistener/TimerEventListenerTest.java | 89 +++++++++++++++++ .../cmmn/test/runtime/HumanTaskTest.java | 21 ++++ ...nerTest.testSuspendTimerEventListener.cmmn | 20 ++++ 13 files changed, 394 insertions(+) create mode 100644 modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/operation/SuspendPlanItemInstanceOperation.java create mode 100644 modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/cmd/ResumePlanItemInstanceCmd.java create mode 100644 modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/cmd/SuspendPlanItemInstanceCmd.java create mode 100644 modules/flowable-cmmn-engine/src/test/resources/org/flowable/cmmn/test/eventlistener/TimerEventListenerTest.testSuspendTimerEventListener.cmmn diff --git a/modules/flowable-cmmn-api/src/main/java/org/flowable/cmmn/api/runtime/PlanItemInstanceTransitionBuilder.java b/modules/flowable-cmmn-api/src/main/java/org/flowable/cmmn/api/runtime/PlanItemInstanceTransitionBuilder.java index 16b97554b7a..32a6dd57974 100644 --- a/modules/flowable-cmmn-api/src/main/java/org/flowable/cmmn/api/runtime/PlanItemInstanceTransitionBuilder.java +++ b/modules/flowable-cmmn-api/src/main/java/org/flowable/cmmn/api/runtime/PlanItemInstanceTransitionBuilder.java @@ -113,6 +113,16 @@ public interface PlanItemInstanceTransitionBuilder { * Starts a plan item instance, this typically will executes it associated behavior. */ void start(); + + /** + * Suspend a plan item instance. + */ + void suspend(); + + /** + * Sets the plan item instance to available state. + */ + void resume(); /** * Manually terminates a plan item instance. diff --git a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/CmmnEngineAgenda.java b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/CmmnEngineAgenda.java index 5423f6eff63..ea9a4e0c5ea 100644 --- a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/CmmnEngineAgenda.java +++ b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/CmmnEngineAgenda.java @@ -74,11 +74,15 @@ public interface CmmnEngineAgenda extends Agenda { void planExitPlanItemInstanceOperation(PlanItemInstanceEntity planItemInstanceEntity, String exitCriterionId, String exitType, String exitEventType); + void planSuspendPlanItemInstanceOperation(PlanItemInstanceEntity planItemInstanceEntity); + void planTerminatePlanItemInstanceOperation(PlanItemInstanceEntity planItemInstanceEntity, String exitType, String exitEventType); void planTriggerPlanItemInstanceOperation(PlanItemInstanceEntity planItemInstanceEntity); void planChangePlanItemInstanceToAvailableOperation(PlanItemInstanceEntity planItemInstanceEntity); + + void planChangePlanItemInstanceToAvailableOperationAndEnableSuspendedJobs(PlanItemInstanceEntity planItemInstanceEntity); void planCompleteCaseInstanceOperation(CaseInstanceEntity caseInstanceEntity); diff --git a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/DefaultCmmnEngineAgenda.java b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/DefaultCmmnEngineAgenda.java index 80c18b700f0..738251975c9 100644 --- a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/DefaultCmmnEngineAgenda.java +++ b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/DefaultCmmnEngineAgenda.java @@ -42,6 +42,7 @@ import org.flowable.cmmn.engine.impl.agenda.operation.ReactivatePlanItemInstanceOperation; import org.flowable.cmmn.engine.impl.agenda.operation.ReactivatePlanModelInstanceOperation; import org.flowable.cmmn.engine.impl.agenda.operation.StartPlanItemInstanceOperation; +import org.flowable.cmmn.engine.impl.agenda.operation.SuspendPlanItemInstanceOperation; import org.flowable.cmmn.engine.impl.agenda.operation.TerminateCaseInstanceOperation; import org.flowable.cmmn.engine.impl.agenda.operation.TerminatePlanItemInstanceOperation; import org.flowable.cmmn.engine.impl.agenda.operation.TriggerPlanItemInstanceOperation; @@ -253,6 +254,11 @@ public void planOccurPlanItemInstanceOperation(PlanItemInstanceEntity planItemIn public void planExitPlanItemInstanceOperation(PlanItemInstanceEntity planItemInstanceEntity, String exitCriterionId, String exitType, String exitEventType) { addOperation(new ExitPlanItemInstanceOperation(commandContext, planItemInstanceEntity, exitCriterionId, exitType, exitEventType)); } + + @Override + public void planSuspendPlanItemInstanceOperation(PlanItemInstanceEntity planItemInstanceEntity) { + addOperation(new SuspendPlanItemInstanceOperation(commandContext, planItemInstanceEntity)); + } @Override public void planTerminatePlanItemInstanceOperation(PlanItemInstanceEntity planItemInstanceEntity, String exitType, String exitEventType) { @@ -263,6 +269,11 @@ public void planTerminatePlanItemInstanceOperation(PlanItemInstanceEntity planIt public void planChangePlanItemInstanceToAvailableOperation(PlanItemInstanceEntity planItemInstanceEntity) { addOperation(new ChangePlanItemInstanceToAvailableOperation(commandContext, planItemInstanceEntity)); } + + @Override + public void planChangePlanItemInstanceToAvailableOperationAndEnableSuspendedJobs(PlanItemInstanceEntity planItemInstanceEntity) { + addOperation(new ChangePlanItemInstanceToAvailableOperation(commandContext, planItemInstanceEntity, true)); + } @Override public void planTriggerPlanItemInstanceOperation(PlanItemInstanceEntity planItemInstanceEntity) { diff --git a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/operation/ChangePlanItemInstanceToAvailableOperation.java b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/operation/ChangePlanItemInstanceToAvailableOperation.java index 5eeb581ccd3..f6d4a261c1f 100644 --- a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/operation/ChangePlanItemInstanceToAvailableOperation.java +++ b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/operation/ChangePlanItemInstanceToAvailableOperation.java @@ -12,21 +12,37 @@ */ package org.flowable.cmmn.engine.impl.agenda.operation; +import java.util.List; + import org.flowable.cmmn.api.runtime.PlanItemInstanceState; +import org.flowable.cmmn.engine.CmmnEngineConfiguration; import org.flowable.cmmn.engine.impl.persistence.entity.PlanItemInstanceEntity; import org.flowable.cmmn.engine.impl.util.CommandContextUtil; +import org.flowable.cmmn.model.PlanItemDefinition; import org.flowable.cmmn.model.PlanItemTransition; +import org.flowable.cmmn.model.TimerEventListener; import org.flowable.common.engine.impl.interceptor.CommandContext; +import org.flowable.job.api.Job; +import org.flowable.job.service.impl.SuspendedJobQueryImpl; +import org.flowable.job.service.impl.persistence.entity.SuspendedJobEntity; /** * @author Tijs Rademakers */ public class ChangePlanItemInstanceToAvailableOperation extends AbstractChangePlanItemInstanceStateOperation { + protected boolean enableSuspendedJobs; + public ChangePlanItemInstanceToAvailableOperation(CommandContext commandContext, PlanItemInstanceEntity planItemInstanceEntity) { super(commandContext, planItemInstanceEntity); } + public ChangePlanItemInstanceToAvailableOperation(CommandContext commandContext, PlanItemInstanceEntity planItemInstanceEntity, boolean enableSuspendedJobs) { + super(commandContext, planItemInstanceEntity); + + this.enableSuspendedJobs = enableSuspendedJobs; + } + @Override public String getLifeCycleTransition() { return PlanItemTransition.CREATE; @@ -40,6 +56,20 @@ public String getNewState() { @Override protected void internalExecute() { planItemInstanceEntity.setLastAvailableTime(getCurrentTime(commandContext)); + + if (enableSuspendedJobs) { + PlanItemDefinition planItemDefinition = planItemInstanceEntity.getPlanItem().getPlanItemDefinition(); + if (planItemDefinition instanceof TimerEventListener) { + CmmnEngineConfiguration cmmnEngineConfiguration = CommandContextUtil.getCmmnEngineConfiguration(commandContext); + SuspendedJobQueryImpl suspendedJobQuery = new SuspendedJobQueryImpl(commandContext, cmmnEngineConfiguration.getJobServiceConfiguration()); + suspendedJobQuery.subScopeId(planItemInstanceEntity.getId()); + List suspendedJobs = cmmnEngineConfiguration.getJobServiceConfiguration().getSuspendedJobEntityManager().findJobsByQueryCriteria(suspendedJobQuery); + if (suspendedJobs != null && !suspendedJobs.isEmpty()) { + cmmnEngineConfiguration.getJobServiceConfiguration().getJobService().activateSuspendedJob((SuspendedJobEntity) suspendedJobs.get(0)); + } + } + } + CommandContextUtil.getCmmnHistoryManager(commandContext).recordPlanItemInstanceAvailable(planItemInstanceEntity); } diff --git a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/operation/SuspendPlanItemInstanceOperation.java b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/operation/SuspendPlanItemInstanceOperation.java new file mode 100644 index 00000000000..9d372ca234f --- /dev/null +++ b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/operation/SuspendPlanItemInstanceOperation.java @@ -0,0 +1,96 @@ +/* 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.cmmn.engine.impl.agenda.operation; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.flowable.cmmn.api.runtime.PlanItemInstanceState; +import org.flowable.cmmn.engine.CmmnEngineConfiguration; +import org.flowable.cmmn.engine.impl.persistence.entity.PlanItemInstanceEntity; +import org.flowable.cmmn.engine.impl.util.CommandContextUtil; +import org.flowable.cmmn.model.PlanItemDefinition; +import org.flowable.cmmn.model.PlanItemTransition; +import org.flowable.cmmn.model.TimerEventListener; +import org.flowable.common.engine.impl.interceptor.CommandContext; +import org.flowable.job.api.Job; +import org.flowable.job.service.impl.TimerJobQueryImpl; +import org.flowable.job.service.impl.persistence.entity.AbstractRuntimeJobEntity; + +public class SuspendPlanItemInstanceOperation extends AbstractMovePlanItemInstanceToTerminalStateOperation { + + public SuspendPlanItemInstanceOperation(CommandContext commandContext, PlanItemInstanceEntity planItemInstanceEntity) { + super(commandContext, planItemInstanceEntity); + } + + @Override + public String getNewState() { + return PlanItemInstanceState.SUSPENDED; + } + + @Override + public String getLifeCycleTransition() { + return PlanItemTransition.SUSPEND; + } + + @Override + public boolean isEvaluateRepetitionRule() { + return false; + } + + @Override + protected boolean shouldAggregateForSingleInstance() { + return false; + } + + @Override + protected boolean shouldAggregateForMultipleInstances() { + return false; + } + + @Override + protected void internalExecute() { + planItemInstanceEntity.setLastSuspendedTime(getCurrentTime(commandContext)); + + PlanItemDefinition planItemDefinition = planItemInstanceEntity.getPlanItem().getPlanItemDefinition(); + if (planItemDefinition instanceof TimerEventListener) { + CmmnEngineConfiguration cmmnEngineConfiguration = CommandContextUtil.getCmmnEngineConfiguration(commandContext); + TimerJobQueryImpl timerJobQuery = new TimerJobQueryImpl(commandContext, cmmnEngineConfiguration.getJobServiceConfiguration()); + timerJobQuery.subScopeId(planItemInstanceEntity.getId()); + List timerJobs = cmmnEngineConfiguration.getJobServiceConfiguration().getTimerJobEntityManager().findJobsByQueryCriteria(timerJobQuery); + if (timerJobs != null && !timerJobs.isEmpty()) { + cmmnEngineConfiguration.getJobServiceConfiguration().getJobService().moveJobToSuspendedJob((AbstractRuntimeJobEntity) timerJobs.get(0)); + } + } + + CommandContextUtil.getCmmnHistoryManager(commandContext).recordPlanItemInstanceSuspended(planItemInstanceEntity); + } + + @Override + protected Map getAsyncLeaveTransitionMetadata() { + Map metadata = new HashMap<>(); + metadata.put(OperationSerializationMetadata.FIELD_PLAN_ITEM_INSTANCE_ID, planItemInstanceEntity.getId()); + return metadata; + } + + @Override + public boolean abortOperationIfNewStateEqualsOldState() { + return true; + } + + @Override + public String getOperationName() { + return "[Suspend plan item]"; + } +} diff --git a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/cmd/ActivateTaskCmd.java b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/cmd/ActivateTaskCmd.java index 323b1380780..d2eab672c84 100644 --- a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/cmd/ActivateTaskCmd.java +++ b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/cmd/ActivateTaskCmd.java @@ -13,8 +13,14 @@ package org.flowable.cmmn.engine.impl.cmd; import java.util.Date; +import java.util.List; +import org.flowable.cmmn.api.runtime.PlanItemInstance; +import org.flowable.cmmn.api.runtime.PlanItemInstanceQuery; +import org.flowable.cmmn.api.runtime.PlanItemInstanceState; import org.flowable.cmmn.engine.CmmnEngineConfiguration; +import org.flowable.cmmn.engine.impl.persistence.entity.PlanItemInstanceEntity; +import org.flowable.cmmn.engine.impl.runtime.PlanItemInstanceQueryImpl; import org.flowable.cmmn.engine.impl.util.CommandContextUtil; import org.flowable.common.engine.api.FlowableException; import org.flowable.common.engine.api.FlowableIllegalArgumentException; @@ -72,6 +78,19 @@ public Void execute(CommandContext commandContext) { } task.setSuspensionState(SuspensionState.ACTIVE.getStateCode()); + PlanItemInstanceQuery planItemInstanceQuery = new PlanItemInstanceQueryImpl(commandContext, cmmnEngineConfiguration); + planItemInstanceQuery.caseInstanceId(task.getScopeId()) + .planItemDefinitionId(task.getTaskDefinitionKey()) + .planItemInstanceReferenceId(task.getId()); + List planItemInstances = cmmnEngineConfiguration.getPlanItemInstanceEntityManager().findByCriteria(planItemInstanceQuery); + + if (planItemInstances != null && !planItemInstances.isEmpty()) { + PlanItemInstanceEntity planItemInstanceEntity = (PlanItemInstanceEntity) planItemInstances.get(0); + planItemInstanceEntity.setState(PlanItemInstanceState.ACTIVE); + + cmmnEngineConfiguration.getCmmnHistoryManager().recordPlanItemInstanceUpdated(planItemInstanceEntity); + } + HistoricTaskService historicTaskService = cmmnEngineConfiguration.getTaskServiceConfiguration().getHistoricTaskService(); historicTaskService.recordTaskInfoChange(task, updateTime, cmmnEngineConfiguration); diff --git a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/cmd/ResumePlanItemInstanceCmd.java b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/cmd/ResumePlanItemInstanceCmd.java new file mode 100644 index 00000000000..70c88418428 --- /dev/null +++ b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/cmd/ResumePlanItemInstanceCmd.java @@ -0,0 +1,30 @@ +/* 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.cmmn.engine.impl.cmd; + +import org.flowable.cmmn.engine.impl.persistence.entity.PlanItemInstanceEntity; +import org.flowable.cmmn.engine.impl.util.CommandContextUtil; +import org.flowable.common.engine.impl.interceptor.CommandContext; + +public class ResumePlanItemInstanceCmd extends AbstractNeedsPlanItemInstanceCmd { + + public ResumePlanItemInstanceCmd(String planItemInstanceId) { + super(planItemInstanceId); + } + + @Override + protected void internalExecute(CommandContext commandContext, PlanItemInstanceEntity planItemInstanceEntity) { + CommandContextUtil.getAgenda(commandContext).planChangePlanItemInstanceToAvailableOperationAndEnableSuspendedJobs(planItemInstanceEntity); + } + +} diff --git a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/cmd/SuspendPlanItemInstanceCmd.java b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/cmd/SuspendPlanItemInstanceCmd.java new file mode 100644 index 00000000000..c4b15a321dc --- /dev/null +++ b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/cmd/SuspendPlanItemInstanceCmd.java @@ -0,0 +1,30 @@ +/* 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.cmmn.engine.impl.cmd; + +import org.flowable.cmmn.engine.impl.persistence.entity.PlanItemInstanceEntity; +import org.flowable.cmmn.engine.impl.util.CommandContextUtil; +import org.flowable.common.engine.impl.interceptor.CommandContext; + +public class SuspendPlanItemInstanceCmd extends AbstractNeedsPlanItemInstanceCmd { + + public SuspendPlanItemInstanceCmd(String planItemInstanceId) { + super(planItemInstanceId); + } + + @Override + protected void internalExecute(CommandContext commandContext, PlanItemInstanceEntity planItemInstanceEntity) { + CommandContextUtil.getAgenda(commandContext).planSuspendPlanItemInstanceOperation(planItemInstanceEntity); + } + +} diff --git a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/cmd/SuspendTaskCmd.java b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/cmd/SuspendTaskCmd.java index a3d196fbf5c..b70425c13da 100644 --- a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/cmd/SuspendTaskCmd.java +++ b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/cmd/SuspendTaskCmd.java @@ -13,8 +13,14 @@ package org.flowable.cmmn.engine.impl.cmd; import java.util.Date; +import java.util.List; +import org.flowable.cmmn.api.runtime.PlanItemInstance; +import org.flowable.cmmn.api.runtime.PlanItemInstanceQuery; +import org.flowable.cmmn.api.runtime.PlanItemInstanceState; import org.flowable.cmmn.engine.CmmnEngineConfiguration; +import org.flowable.cmmn.engine.impl.persistence.entity.PlanItemInstanceEntity; +import org.flowable.cmmn.engine.impl.runtime.PlanItemInstanceQueryImpl; import org.flowable.cmmn.engine.impl.util.CommandContextUtil; import org.flowable.common.engine.impl.db.SuspensionState; import org.flowable.common.engine.impl.interceptor.CommandContext; @@ -44,6 +50,20 @@ protected Void execute(CommandContext commandContext, TaskEntity task) { task.setState(Task.SUSPENDED); task.setSuspensionState(SuspensionState.SUSPENDED.getStateCode()); + PlanItemInstanceQuery planItemInstanceQuery = new PlanItemInstanceQueryImpl(commandContext, cmmnEngineConfiguration); + planItemInstanceQuery.caseInstanceId(task.getScopeId()) + .planItemDefinitionId(task.getTaskDefinitionKey()) + .planItemInstanceReferenceId(task.getId()); + List planItemInstances = cmmnEngineConfiguration.getPlanItemInstanceEntityManager().findByCriteria(planItemInstanceQuery); + + if (planItemInstances != null && !planItemInstances.isEmpty()) { + PlanItemInstanceEntity planItemInstanceEntity = (PlanItemInstanceEntity) planItemInstances.get(0); + planItemInstanceEntity.setState(PlanItemInstanceState.SUSPENDED); + planItemInstanceEntity.setLastSuspendedTime(updateTime); + + cmmnEngineConfiguration.getCmmnHistoryManager().recordPlanItemInstanceSuspended(planItemInstanceEntity); + } + HistoricTaskService historicTaskService = cmmnEngineConfiguration.getTaskServiceConfiguration().getHistoricTaskService(); historicTaskService.recordTaskInfoChange(task, updateTime, cmmnEngineConfiguration); diff --git a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/runtime/PlanItemInstanceTransitionBuilderImpl.java b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/runtime/PlanItemInstanceTransitionBuilderImpl.java index b4d86587eb7..e57d900bed6 100644 --- a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/runtime/PlanItemInstanceTransitionBuilderImpl.java +++ b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/runtime/PlanItemInstanceTransitionBuilderImpl.java @@ -19,7 +19,9 @@ import org.flowable.cmmn.engine.impl.cmd.CompleteStagePlanItemInstanceCmd; import org.flowable.cmmn.engine.impl.cmd.DisablePlanItemInstanceCmd; import org.flowable.cmmn.engine.impl.cmd.EnablePlanItemInstanceCmd; +import org.flowable.cmmn.engine.impl.cmd.ResumePlanItemInstanceCmd; import org.flowable.cmmn.engine.impl.cmd.StartPlanItemInstanceCmd; +import org.flowable.cmmn.engine.impl.cmd.SuspendPlanItemInstanceCmd; import org.flowable.cmmn.engine.impl.cmd.TerminatePlanItemInstanceCmd; import org.flowable.cmmn.engine.impl.cmd.TriggerPlanItemInstanceCmd; import org.flowable.common.engine.api.FlowableIllegalArgumentException; @@ -195,6 +197,18 @@ public void start() { formInfo, localVariables, transientVariables, childTaskVariables, childTaskFormVariables, childTaskFormOutcome, childTaskFormInfo)); } + + @Override + public void suspend() { + validateChildTaskVariablesNotSet(); + commandExecutor.execute(new SuspendPlanItemInstanceCmd(planItemInstanceId)); + } + + @Override + public void resume() { + validateChildTaskVariablesNotSet(); + commandExecutor.execute(new ResumePlanItemInstanceCmd(planItemInstanceId)); + } @Override public void terminate() { diff --git a/modules/flowable-cmmn-engine/src/test/java/org/flowable/cmmn/test/eventlistener/TimerEventListenerTest.java b/modules/flowable-cmmn-engine/src/test/java/org/flowable/cmmn/test/eventlistener/TimerEventListenerTest.java index 4b9c74f56ce..7615811ae4f 100644 --- a/modules/flowable-cmmn-engine/src/test/java/org/flowable/cmmn/test/eventlistener/TimerEventListenerTest.java +++ b/modules/flowable-cmmn-engine/src/test/java/org/flowable/cmmn/test/eventlistener/TimerEventListenerTest.java @@ -931,4 +931,93 @@ public void testTimerEventListenerInstanceRescheduleWithDateValue() { ); } } + + @Test + @CmmnDeployment + public void testSuspendTimerEventListener() { + CaseInstance caseInstance = cmmnRuntimeService.createCaseInstanceBuilder().caseDefinitionKey("testSuspendTimerEventListener").start(); + assertThat(caseInstance).isNotNull(); + + // Verify timer event listener is available and a timer job exists + PlanItemInstance timerPlanItemInstance = cmmnRuntimeService.createPlanItemInstanceQuery() + .planItemDefinitionType(PlanItemDefinitionType.TIMER_EVENT_LISTENER) + .planItemInstanceStateAvailable() + .singleResult(); + assertThat(timerPlanItemInstance).isNotNull(); + assertThat(timerPlanItemInstance.getState()).isEqualTo(PlanItemInstanceState.AVAILABLE); + + assertThat(cmmnManagementService.createTimerJobQuery().scopeId(caseInstance.getId()).scopeType(ScopeTypes.CMMN).count()).isEqualTo(1); + + // Suspend the timer event listener via the command executor + String planItemInstanceId = timerPlanItemInstance.getId(); + cmmnRuntimeService.createPlanItemInstanceTransitionBuilder(planItemInstanceId).suspend(); + + // Verify the timer event listener is in suspended state + PlanItemInstance suspendedPlanItemInstance = cmmnRuntimeService.createPlanItemInstanceQuery() + .planItemDefinitionType(PlanItemDefinitionType.TIMER_EVENT_LISTENER) + .planItemInstanceState(PlanItemInstanceState.SUSPENDED) + .singleResult(); + assertThat(suspendedPlanItemInstance).isNotNull(); + assertThat(suspendedPlanItemInstance.getState()).isEqualTo(PlanItemInstanceState.SUSPENDED); + assertThat(suspendedPlanItemInstance.getLastSuspendedTime()).isNotNull(); + + // Verify the available query no longer returns the timer + assertThat(cmmnRuntimeService.createPlanItemInstanceQuery() + .planItemDefinitionType(PlanItemDefinitionType.TIMER_EVENT_LISTENER) + .planItemInstanceStateAvailable() + .singleResult()).isNull(); + + // Verify all plan items: timer should be suspended, human task should still be available + assertThat(cmmnRuntimeService.createPlanItemInstanceQuery().caseInstanceId(caseInstance.getId()).list()) + .extracting(PlanItemInstance::getPlanItemDefinitionType, PlanItemInstance::getPlanItemDefinitionId, PlanItemInstance::getState) + .containsExactlyInAnyOrder( + tuple(PlanItemDefinitionType.TIMER_EVENT_LISTENER, "timerListener", PlanItemInstanceState.SUSPENDED), + tuple(PlanItemDefinitionType.HUMAN_TASK, "taskA", PlanItemInstanceState.AVAILABLE) + ); + + Job suspendedJob = cmmnManagementService.createSuspendedJobQuery().planItemInstanceId(suspendedPlanItemInstance.getId()).singleResult(); + assertThat(suspendedJob).isNotNull(); + + assertThat(cmmnManagementService.createTimerJobQuery().planItemInstanceId(suspendedPlanItemInstance.getId()).count()).isZero(); + + if (CmmnHistoryTestHelper.isHistoryLevelAtLeast(HistoryLevel.ACTIVITY, cmmnEngineConfiguration)) { + HistoricPlanItemInstance historicTimerPlanItemInstance = cmmnHistoryService.createHistoricPlanItemInstanceQuery() + .planItemInstanceDefinitionType(PlanItemDefinitionType.TIMER_EVENT_LISTENER) + .singleResult(); + assertThat(historicTimerPlanItemInstance).isNotNull(); + assertThat(historicTimerPlanItemInstance.getState()).isEqualTo(PlanItemInstanceState.SUSPENDED); + assertThat(historicTimerPlanItemInstance.getLastSuspendedTime()).isNotNull(); + } + + cmmnRuntimeService.createPlanItemInstanceTransitionBuilder(planItemInstanceId).resume(); + + PlanItemInstance activatedPlanItemInstance = cmmnRuntimeService.createPlanItemInstanceQuery() + .planItemDefinitionType(PlanItemDefinitionType.TIMER_EVENT_LISTENER) + .planItemInstanceState(PlanItemInstanceState.AVAILABLE) + .singleResult(); + assertThat(activatedPlanItemInstance).isNotNull(); + assertThat(activatedPlanItemInstance.getState()).isEqualTo(PlanItemInstanceState.AVAILABLE); + + suspendedJob = cmmnManagementService.createSuspendedJobQuery().planItemInstanceId(suspendedPlanItemInstance.getId()).singleResult(); + assertThat(suspendedJob).isNull(); + + assertThat(cmmnManagementService.createTimerJobQuery().planItemInstanceId(suspendedPlanItemInstance.getId()).count()).isEqualTo(1); + + if (CmmnHistoryTestHelper.isHistoryLevelAtLeast(HistoryLevel.ACTIVITY, cmmnEngineConfiguration)) { + HistoricPlanItemInstance historicTimerPlanItemInstance = cmmnHistoryService.createHistoricPlanItemInstanceQuery() + .planItemInstanceDefinitionType(PlanItemDefinitionType.TIMER_EVENT_LISTENER) + .singleResult(); + assertThat(historicTimerPlanItemInstance).isNotNull(); + assertThat(historicTimerPlanItemInstance.getState()).isEqualTo(PlanItemInstanceState.AVAILABLE); + } + + Job timerJob = cmmnManagementService.createTimerJobQuery().planItemInstanceId(suspendedPlanItemInstance.getId()).singleResult(); + Job executableJob = cmmnManagementService.moveTimerToExecutableJob(timerJob.getId()); + cmmnManagementService.executeJob(executableJob.getId()); + + Task task = cmmnTaskService.createTaskQuery().caseInstanceId(caseInstance.getId()).singleResult(); + cmmnTaskService.complete(task.getId()); + + assertCaseInstanceEnded(caseInstance); + } } \ No newline at end of file diff --git a/modules/flowable-cmmn-engine/src/test/java/org/flowable/cmmn/test/runtime/HumanTaskTest.java b/modules/flowable-cmmn-engine/src/test/java/org/flowable/cmmn/test/runtime/HumanTaskTest.java index 89afa7dd953..f621fad4b7f 100644 --- a/modules/flowable-cmmn-engine/src/test/java/org/flowable/cmmn/test/runtime/HumanTaskTest.java +++ b/modules/flowable-cmmn-engine/src/test/java/org/flowable/cmmn/test/runtime/HumanTaskTest.java @@ -13,6 +13,7 @@ package org.flowable.cmmn.test.runtime; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.tuple; import java.util.Arrays; @@ -27,11 +28,15 @@ import org.flowable.cmmn.api.runtime.CaseInstance; import org.flowable.cmmn.api.runtime.PlanItemDefinitionType; import org.flowable.cmmn.api.runtime.PlanItemInstance; +import org.flowable.cmmn.api.runtime.PlanItemInstanceState; import org.flowable.cmmn.engine.test.CmmnDeployment; import org.flowable.cmmn.engine.test.impl.CmmnHistoryTestHelper; import org.flowable.cmmn.engine.test.impl.CmmnJobTestHelper; import org.flowable.cmmn.engine.test.impl.CmmnTestHelper; import org.flowable.cmmn.test.FlowableCmmnTestCase; +import org.flowable.common.engine.api.FlowableIllegalArgumentException; +import org.flowable.common.engine.api.FlowableIllegalStateException; +import org.flowable.common.engine.api.FlowableObjectNotFoundException; import org.flowable.common.engine.api.constant.ReferenceTypes; import org.flowable.common.engine.api.scope.ScopeTypes; import org.flowable.common.engine.impl.history.HistoryLevel; @@ -512,13 +517,29 @@ public void testFillTaskLifecycleValues() { assertThat(task.getSuspendedTime()).isNotNull(); assertThat(task.getSuspendedBy()).isEqualTo("gonzo"); + PlanItemInstance taskPlanItemInstance = cmmnRuntimeService.createPlanItemInstanceQuery().planItemDefinitionId("theTask").singleResult(); + assertThat(taskPlanItemInstance).isNotNull(); + assertThat(taskPlanItemInstance.getState()).isEqualTo(PlanItemInstanceState.SUSPENDED); + assertThat(taskPlanItemInstance.getLastSuspendedTime()).isNotNull(); + if (CmmnHistoryTestHelper.isHistoryLevelAtLeast(HistoryLevel.AUDIT, cmmnEngineConfiguration)) { HistoricTaskInstance historicTaskInstance = cmmnHistoryService.createHistoricTaskInstanceQuery().taskId(task.getId()).singleResult(); assertThat(historicTaskInstance.getState()).isEqualTo(Task.SUSPENDED); assertThat(historicTaskInstance.getSuspendedTime()).isNotNull(); assertThat(historicTaskInstance.getSuspendedBy()).isEqualTo("gonzo"); + + HistoricPlanItemInstance historicPlanItemInstance = cmmnHistoryService.createHistoricPlanItemInstanceQuery().planItemInstanceReferenceId(task.getId()).singleResult(); + assertThat(historicPlanItemInstance).isNotNull(); + assertThat(historicPlanItemInstance.getState()).isEqualTo(PlanItemInstanceState.SUSPENDED); + assertThat(historicPlanItemInstance.getLastSuspendedTime()).isNotNull(); } + final String taskId = task.getId(); + assertThatThrownBy(() -> { + cmmnTaskService.complete(taskId, "kermit"); + }).isInstanceOf(FlowableIllegalStateException.class) + .hasMessageContaining("Can only trigger a plan item that is in the ACTIVE state"); + cmmnTaskService.activateTask(task.getId(), "kermit"); task = cmmnTaskService.createTaskQuery().caseInstanceId(caseInstance.getId()).singleResult(); assertThat(task.getState()).isEqualTo(Task.IN_PROGRESS); diff --git a/modules/flowable-cmmn-engine/src/test/resources/org/flowable/cmmn/test/eventlistener/TimerEventListenerTest.testSuspendTimerEventListener.cmmn b/modules/flowable-cmmn-engine/src/test/resources/org/flowable/cmmn/test/eventlistener/TimerEventListenerTest.testSuspendTimerEventListener.cmmn new file mode 100644 index 00000000000..4a162d6f472 --- /dev/null +++ b/modules/flowable-cmmn-engine/src/test/resources/org/flowable/cmmn/test/eventlistener/TimerEventListenerTest.testSuspendTimerEventListener.cmmn @@ -0,0 +1,20 @@ + + + + + + + + + + + occur + + + + + + + + + From 7410a53fce7559ff9fa114f48769ea0e9746bd58 Mon Sep 17 00:00:00 2001 From: Tijs Rademakers Date: Mon, 9 Mar 2026 21:01:52 +0100 Subject: [PATCH 2/3] Update after review --- .../engine/impl/agenda/CmmnEngineAgenda.java | 2 +- .../impl/agenda/DefaultCmmnEngineAgenda.java | 5 +- ...ePlanItemInstanceToAvailableOperation.java | 30 ------- .../ResumePlanItemInstanceOperation.java | 82 +++++++++++++++++++ .../SuspendPlanItemInstanceOperation.java | 43 +++------- .../impl/cmd/ResumePlanItemInstanceCmd.java | 2 +- .../eventlistener/TimerEventListenerTest.java | 11 +++ .../entity/SuspendedJobEntityManager.java | 5 ++ .../entity/SuspendedJobEntityManagerImpl.java | 5 ++ .../entity/data/SuspendedJobDataManager.java | 2 + .../impl/MybatisSuspendedJobDataManager.java | 8 ++ .../SuspendedJobsBySubScopeIdMatcher.java | 25 ++++++ .../db/mapping/entity/SuspendedJob.xml | 7 ++ 13 files changed, 163 insertions(+), 64 deletions(-) create mode 100644 modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/operation/ResumePlanItemInstanceOperation.java create mode 100644 modules/flowable-job-service/src/main/java/org/flowable/job/service/impl/persistence/entity/data/impl/cachematcher/SuspendedJobsBySubScopeIdMatcher.java diff --git a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/CmmnEngineAgenda.java b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/CmmnEngineAgenda.java index ea9a4e0c5ea..0f9779b2f55 100644 --- a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/CmmnEngineAgenda.java +++ b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/CmmnEngineAgenda.java @@ -82,7 +82,7 @@ public interface CmmnEngineAgenda extends Agenda { void planChangePlanItemInstanceToAvailableOperation(PlanItemInstanceEntity planItemInstanceEntity); - void planChangePlanItemInstanceToAvailableOperationAndEnableSuspendedJobs(PlanItemInstanceEntity planItemInstanceEntity); + void planResumePlanItemInstanceOperation(PlanItemInstanceEntity planItemInstanceEntity); void planCompleteCaseInstanceOperation(CaseInstanceEntity caseInstanceEntity); diff --git a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/DefaultCmmnEngineAgenda.java b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/DefaultCmmnEngineAgenda.java index 738251975c9..3963d756677 100644 --- a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/DefaultCmmnEngineAgenda.java +++ b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/DefaultCmmnEngineAgenda.java @@ -41,6 +41,7 @@ import org.flowable.cmmn.engine.impl.agenda.operation.ReactivateCaseInstanceOperation; import org.flowable.cmmn.engine.impl.agenda.operation.ReactivatePlanItemInstanceOperation; import org.flowable.cmmn.engine.impl.agenda.operation.ReactivatePlanModelInstanceOperation; +import org.flowable.cmmn.engine.impl.agenda.operation.ResumePlanItemInstanceOperation; import org.flowable.cmmn.engine.impl.agenda.operation.StartPlanItemInstanceOperation; import org.flowable.cmmn.engine.impl.agenda.operation.SuspendPlanItemInstanceOperation; import org.flowable.cmmn.engine.impl.agenda.operation.TerminateCaseInstanceOperation; @@ -271,8 +272,8 @@ public void planChangePlanItemInstanceToAvailableOperation(PlanItemInstanceEntit } @Override - public void planChangePlanItemInstanceToAvailableOperationAndEnableSuspendedJobs(PlanItemInstanceEntity planItemInstanceEntity) { - addOperation(new ChangePlanItemInstanceToAvailableOperation(commandContext, planItemInstanceEntity, true)); + public void planResumePlanItemInstanceOperation(PlanItemInstanceEntity planItemInstanceEntity) { + addOperation(new ResumePlanItemInstanceOperation(commandContext, planItemInstanceEntity)); } @Override diff --git a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/operation/ChangePlanItemInstanceToAvailableOperation.java b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/operation/ChangePlanItemInstanceToAvailableOperation.java index f6d4a261c1f..5eeb581ccd3 100644 --- a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/operation/ChangePlanItemInstanceToAvailableOperation.java +++ b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/operation/ChangePlanItemInstanceToAvailableOperation.java @@ -12,37 +12,21 @@ */ package org.flowable.cmmn.engine.impl.agenda.operation; -import java.util.List; - import org.flowable.cmmn.api.runtime.PlanItemInstanceState; -import org.flowable.cmmn.engine.CmmnEngineConfiguration; import org.flowable.cmmn.engine.impl.persistence.entity.PlanItemInstanceEntity; import org.flowable.cmmn.engine.impl.util.CommandContextUtil; -import org.flowable.cmmn.model.PlanItemDefinition; import org.flowable.cmmn.model.PlanItemTransition; -import org.flowable.cmmn.model.TimerEventListener; import org.flowable.common.engine.impl.interceptor.CommandContext; -import org.flowable.job.api.Job; -import org.flowable.job.service.impl.SuspendedJobQueryImpl; -import org.flowable.job.service.impl.persistence.entity.SuspendedJobEntity; /** * @author Tijs Rademakers */ public class ChangePlanItemInstanceToAvailableOperation extends AbstractChangePlanItemInstanceStateOperation { - protected boolean enableSuspendedJobs; - public ChangePlanItemInstanceToAvailableOperation(CommandContext commandContext, PlanItemInstanceEntity planItemInstanceEntity) { super(commandContext, planItemInstanceEntity); } - public ChangePlanItemInstanceToAvailableOperation(CommandContext commandContext, PlanItemInstanceEntity planItemInstanceEntity, boolean enableSuspendedJobs) { - super(commandContext, planItemInstanceEntity); - - this.enableSuspendedJobs = enableSuspendedJobs; - } - @Override public String getLifeCycleTransition() { return PlanItemTransition.CREATE; @@ -56,20 +40,6 @@ public String getNewState() { @Override protected void internalExecute() { planItemInstanceEntity.setLastAvailableTime(getCurrentTime(commandContext)); - - if (enableSuspendedJobs) { - PlanItemDefinition planItemDefinition = planItemInstanceEntity.getPlanItem().getPlanItemDefinition(); - if (planItemDefinition instanceof TimerEventListener) { - CmmnEngineConfiguration cmmnEngineConfiguration = CommandContextUtil.getCmmnEngineConfiguration(commandContext); - SuspendedJobQueryImpl suspendedJobQuery = new SuspendedJobQueryImpl(commandContext, cmmnEngineConfiguration.getJobServiceConfiguration()); - suspendedJobQuery.subScopeId(planItemInstanceEntity.getId()); - List suspendedJobs = cmmnEngineConfiguration.getJobServiceConfiguration().getSuspendedJobEntityManager().findJobsByQueryCriteria(suspendedJobQuery); - if (suspendedJobs != null && !suspendedJobs.isEmpty()) { - cmmnEngineConfiguration.getJobServiceConfiguration().getJobService().activateSuspendedJob((SuspendedJobEntity) suspendedJobs.get(0)); - } - } - } - CommandContextUtil.getCmmnHistoryManager(commandContext).recordPlanItemInstanceAvailable(planItemInstanceEntity); } diff --git a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/operation/ResumePlanItemInstanceOperation.java b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/operation/ResumePlanItemInstanceOperation.java new file mode 100644 index 00000000000..0745eeff2da --- /dev/null +++ b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/operation/ResumePlanItemInstanceOperation.java @@ -0,0 +1,82 @@ +/* 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.cmmn.engine.impl.agenda.operation; + +import java.util.List; + +import org.flowable.cmmn.api.runtime.PlanItemInstanceState; +import org.flowable.cmmn.engine.CmmnEngineConfiguration; +import org.flowable.cmmn.engine.impl.persistence.entity.PlanItemInstanceEntity; +import org.flowable.cmmn.engine.impl.util.CommandContextUtil; +import org.flowable.cmmn.model.PlanItemDefinition; +import org.flowable.cmmn.model.PlanItemTransition; +import org.flowable.cmmn.model.TimerEventListener; +import org.flowable.common.engine.api.FlowableIllegalStateException; +import org.flowable.common.engine.impl.interceptor.CommandContext; +import org.flowable.job.service.impl.persistence.entity.SuspendedJobEntity; + +/** + * @author Tijs Rademakers + */ +public class ResumePlanItemInstanceOperation extends AbstractChangePlanItemInstanceStateOperation { + + public ResumePlanItemInstanceOperation(CommandContext commandContext, PlanItemInstanceEntity planItemInstanceEntity) { + super(commandContext, planItemInstanceEntity); + } + + @Override + public String getLifeCycleTransition() { + return PlanItemTransition.SUSPEND; + } + + @Override + public String getNewState() { + return PlanItemInstanceState.AVAILABLE; + } + + @Override + protected void internalExecute() { + planItemInstanceEntity.setLastAvailableTime(getCurrentTime(commandContext)); + + PlanItemDefinition planItemDefinition = planItemInstanceEntity.getPlanItem().getPlanItemDefinition(); + if (planItemDefinition instanceof TimerEventListener) { + CmmnEngineConfiguration cmmnEngineConfiguration = CommandContextUtil.getCmmnEngineConfiguration(commandContext); + List suspendedJobs = cmmnEngineConfiguration.getJobServiceConfiguration().getSuspendedJobEntityManager().findJobsBySubScopeId(planItemInstanceEntity.getId()); + if (suspendedJobs != null && !suspendedJobs.isEmpty()) { + cmmnEngineConfiguration.getJobServiceConfiguration().getJobService().activateSuspendedJob(suspendedJobs.get(0)); + } + } + + CommandContextUtil.getCmmnHistoryManager(commandContext).recordPlanItemInstanceAvailable(planItemInstanceEntity); + } + + @Override + public boolean isStateNotChanged(String oldState, String newState) { + if (oldState != null && !PlanItemInstanceState.SUSPENDED.equals(oldState)) { + throw new FlowableIllegalStateException("plan item instance can only be resumed if the state is suspended"); + } + + return oldState != null && oldState.equals(newState) && abortOperationIfNewStateEqualsOldState(); + } + + @Override + public boolean abortOperationIfNewStateEqualsOldState() { + return true; + } + + @Override + public String getOperationName() { + return null; // Default one is ok. + } + +} diff --git a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/operation/SuspendPlanItemInstanceOperation.java b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/operation/SuspendPlanItemInstanceOperation.java index 9d372ca234f..6d7e60c685a 100644 --- a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/operation/SuspendPlanItemInstanceOperation.java +++ b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/operation/SuspendPlanItemInstanceOperation.java @@ -12,9 +12,7 @@ */ package org.flowable.cmmn.engine.impl.agenda.operation; -import java.util.HashMap; import java.util.List; -import java.util.Map; import org.flowable.cmmn.api.runtime.PlanItemInstanceState; import org.flowable.cmmn.engine.CmmnEngineConfiguration; @@ -23,12 +21,11 @@ import org.flowable.cmmn.model.PlanItemDefinition; import org.flowable.cmmn.model.PlanItemTransition; import org.flowable.cmmn.model.TimerEventListener; +import org.flowable.common.engine.api.FlowableIllegalStateException; import org.flowable.common.engine.impl.interceptor.CommandContext; -import org.flowable.job.api.Job; -import org.flowable.job.service.impl.TimerJobQueryImpl; -import org.flowable.job.service.impl.persistence.entity.AbstractRuntimeJobEntity; +import org.flowable.job.service.impl.persistence.entity.TimerJobEntity; -public class SuspendPlanItemInstanceOperation extends AbstractMovePlanItemInstanceToTerminalStateOperation { +public class SuspendPlanItemInstanceOperation extends AbstractChangePlanItemInstanceStateOperation { public SuspendPlanItemInstanceOperation(CommandContext commandContext, PlanItemInstanceEntity planItemInstanceEntity) { super(commandContext, planItemInstanceEntity); @@ -43,21 +40,6 @@ public String getNewState() { public String getLifeCycleTransition() { return PlanItemTransition.SUSPEND; } - - @Override - public boolean isEvaluateRepetitionRule() { - return false; - } - - @Override - protected boolean shouldAggregateForSingleInstance() { - return false; - } - - @Override - protected boolean shouldAggregateForMultipleInstances() { - return false; - } @Override protected void internalExecute() { @@ -66,22 +48,23 @@ protected void internalExecute() { PlanItemDefinition planItemDefinition = planItemInstanceEntity.getPlanItem().getPlanItemDefinition(); if (planItemDefinition instanceof TimerEventListener) { CmmnEngineConfiguration cmmnEngineConfiguration = CommandContextUtil.getCmmnEngineConfiguration(commandContext); - TimerJobQueryImpl timerJobQuery = new TimerJobQueryImpl(commandContext, cmmnEngineConfiguration.getJobServiceConfiguration()); - timerJobQuery.subScopeId(planItemInstanceEntity.getId()); - List timerJobs = cmmnEngineConfiguration.getJobServiceConfiguration().getTimerJobEntityManager().findJobsByQueryCriteria(timerJobQuery); + List timerJobs = cmmnEngineConfiguration.getJobServiceConfiguration().getTimerJobEntityManager().findJobsByScopeIdAndSubScopeId( + planItemInstanceEntity.getCaseInstanceId(), planItemInstanceEntity.getId()); if (timerJobs != null && !timerJobs.isEmpty()) { - cmmnEngineConfiguration.getJobServiceConfiguration().getJobService().moveJobToSuspendedJob((AbstractRuntimeJobEntity) timerJobs.get(0)); + cmmnEngineConfiguration.getJobServiceConfiguration().getJobService().moveJobToSuspendedJob(timerJobs.get(0)); } } CommandContextUtil.getCmmnHistoryManager(commandContext).recordPlanItemInstanceSuspended(planItemInstanceEntity); } - + @Override - protected Map getAsyncLeaveTransitionMetadata() { - Map metadata = new HashMap<>(); - metadata.put(OperationSerializationMetadata.FIELD_PLAN_ITEM_INSTANCE_ID, planItemInstanceEntity.getId()); - return metadata; + public boolean isStateNotChanged(String oldState, String newState) { + if (oldState != null && oldState.equals(newState)) { + throw new FlowableIllegalStateException("plan item instance is already suspended"); + } + + return false; } @Override diff --git a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/cmd/ResumePlanItemInstanceCmd.java b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/cmd/ResumePlanItemInstanceCmd.java index 70c88418428..8cb5dd8e4f7 100644 --- a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/cmd/ResumePlanItemInstanceCmd.java +++ b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/cmd/ResumePlanItemInstanceCmd.java @@ -24,7 +24,7 @@ public ResumePlanItemInstanceCmd(String planItemInstanceId) { @Override protected void internalExecute(CommandContext commandContext, PlanItemInstanceEntity planItemInstanceEntity) { - CommandContextUtil.getAgenda(commandContext).planChangePlanItemInstanceToAvailableOperationAndEnableSuspendedJobs(planItemInstanceEntity); + CommandContextUtil.getAgenda(commandContext).planResumePlanItemInstanceOperation(planItemInstanceEntity); } } diff --git a/modules/flowable-cmmn-engine/src/test/java/org/flowable/cmmn/test/eventlistener/TimerEventListenerTest.java b/modules/flowable-cmmn-engine/src/test/java/org/flowable/cmmn/test/eventlistener/TimerEventListenerTest.java index 7615811ae4f..4ffc13f5165 100644 --- a/modules/flowable-cmmn-engine/src/test/java/org/flowable/cmmn/test/eventlistener/TimerEventListenerTest.java +++ b/modules/flowable-cmmn-engine/src/test/java/org/flowable/cmmn/test/eventlistener/TimerEventListenerTest.java @@ -39,6 +39,7 @@ import org.flowable.cmmn.model.Stage; import org.flowable.cmmn.model.TimerEventListener; import org.flowable.cmmn.test.FlowableCmmnTestCase; +import org.flowable.common.engine.api.FlowableIllegalStateException; import org.flowable.common.engine.api.FlowableObjectNotFoundException; import org.flowable.common.engine.api.scope.ScopeTypes; import org.flowable.common.engine.impl.history.HistoryLevel; @@ -989,6 +990,11 @@ public void testSuspendTimerEventListener() { assertThat(historicTimerPlanItemInstance.getLastSuspendedTime()).isNotNull(); } + assertThatThrownBy(() -> { + cmmnRuntimeService.createPlanItemInstanceTransitionBuilder(planItemInstanceId).suspend(); + }).isInstanceOf(FlowableIllegalStateException.class) + .hasMessageContaining("plan item instance is already suspended"); + cmmnRuntimeService.createPlanItemInstanceTransitionBuilder(planItemInstanceId).resume(); PlanItemInstance activatedPlanItemInstance = cmmnRuntimeService.createPlanItemInstanceQuery() @@ -1011,6 +1017,11 @@ public void testSuspendTimerEventListener() { assertThat(historicTimerPlanItemInstance.getState()).isEqualTo(PlanItemInstanceState.AVAILABLE); } + assertThatThrownBy(() -> { + cmmnRuntimeService.createPlanItemInstanceTransitionBuilder(planItemInstanceId).resume(); + }).isInstanceOf(FlowableIllegalStateException.class) + .hasMessageContaining("plan item instance can only be resumed if the state is suspended"); + Job timerJob = cmmnManagementService.createTimerJobQuery().planItemInstanceId(suspendedPlanItemInstance.getId()).singleResult(); Job executableJob = cmmnManagementService.moveTimerToExecutableJob(timerJob.getId()); cmmnManagementService.executeJob(executableJob.getId()); diff --git a/modules/flowable-job-service/src/main/java/org/flowable/job/service/impl/persistence/entity/SuspendedJobEntityManager.java b/modules/flowable-job-service/src/main/java/org/flowable/job/service/impl/persistence/entity/SuspendedJobEntityManager.java index 8b741e08c8c..490e0a8b2ea 100644 --- a/modules/flowable-job-service/src/main/java/org/flowable/job/service/impl/persistence/entity/SuspendedJobEntityManager.java +++ b/modules/flowable-job-service/src/main/java/org/flowable/job/service/impl/persistence/entity/SuspendedJobEntityManager.java @@ -38,6 +38,11 @@ public interface SuspendedJobEntityManager extends EntityManager findJobsByProcessInstanceId(String id); + + /** + * Returns all {@link SuspendedJobEntity} instances related to a sub scope id. + */ + List findJobsBySubScopeId(String id); /** * Executes a {@link JobQueryImpl} and returns the matching {@link SuspendedJobEntity} instances. diff --git a/modules/flowable-job-service/src/main/java/org/flowable/job/service/impl/persistence/entity/SuspendedJobEntityManagerImpl.java b/modules/flowable-job-service/src/main/java/org/flowable/job/service/impl/persistence/entity/SuspendedJobEntityManagerImpl.java index 3fc0ee10107..88fcd15c8cb 100644 --- a/modules/flowable-job-service/src/main/java/org/flowable/job/service/impl/persistence/entity/SuspendedJobEntityManagerImpl.java +++ b/modules/flowable-job-service/src/main/java/org/flowable/job/service/impl/persistence/entity/SuspendedJobEntityManagerImpl.java @@ -47,6 +47,11 @@ public List findJobsByExecutionId(String id) { public List findJobsByProcessInstanceId(String id) { return dataManager.findJobsByProcessInstanceId(id); } + + @Override + public List findJobsBySubScopeId(String id) { + return dataManager.findJobsBySubScopeId(id); + } @Override public List findJobsByQueryCriteria(SuspendedJobQueryImpl jobQuery) { diff --git a/modules/flowable-job-service/src/main/java/org/flowable/job/service/impl/persistence/entity/data/SuspendedJobDataManager.java b/modules/flowable-job-service/src/main/java/org/flowable/job/service/impl/persistence/entity/data/SuspendedJobDataManager.java index c8e89b714a3..dc5fc454901 100644 --- a/modules/flowable-job-service/src/main/java/org/flowable/job/service/impl/persistence/entity/data/SuspendedJobDataManager.java +++ b/modules/flowable-job-service/src/main/java/org/flowable/job/service/impl/persistence/entity/data/SuspendedJobDataManager.java @@ -29,6 +29,8 @@ public interface SuspendedJobDataManager extends DataManager List findJobsByExecutionId(String executionId); List findJobsByProcessInstanceId(String processInstanceId); + + List findJobsBySubScopeId(String subScopeId); List findJobsByQueryCriteria(SuspendedJobQueryImpl jobQuery); diff --git a/modules/flowable-job-service/src/main/java/org/flowable/job/service/impl/persistence/entity/data/impl/MybatisSuspendedJobDataManager.java b/modules/flowable-job-service/src/main/java/org/flowable/job/service/impl/persistence/entity/data/impl/MybatisSuspendedJobDataManager.java index e832708ec62..3d17919a2b7 100644 --- a/modules/flowable-job-service/src/main/java/org/flowable/job/service/impl/persistence/entity/data/impl/MybatisSuspendedJobDataManager.java +++ b/modules/flowable-job-service/src/main/java/org/flowable/job/service/impl/persistence/entity/data/impl/MybatisSuspendedJobDataManager.java @@ -28,6 +28,7 @@ import org.flowable.job.service.impl.persistence.entity.data.SuspendedJobDataManager; import org.flowable.job.service.impl.persistence.entity.data.impl.cachematcher.JobByCorrelationIdMatcher; import org.flowable.job.service.impl.persistence.entity.data.impl.cachematcher.SuspendedJobsByExecutionIdMatcher; +import org.flowable.job.service.impl.persistence.entity.data.impl.cachematcher.SuspendedJobsBySubScopeIdMatcher; /** * @author Tijs Rademakers @@ -35,6 +36,7 @@ public class MybatisSuspendedJobDataManager extends AbstractDataManager implements SuspendedJobDataManager { protected CachedEntityMatcher suspendedJobsByExecutionIdMatcher = new SuspendedJobsByExecutionIdMatcher(); + protected CachedEntityMatcher suspendedJobsBySubScopeIdMatcher = new SuspendedJobsBySubScopeIdMatcher(); protected SingleCachedEntityMatcher suspendedJobByCorrelationIdMatcher = new JobByCorrelationIdMatcher<>(); protected JobServiceConfiguration jobServiceConfiguration; @@ -87,6 +89,12 @@ public List findJobsByExecutionId(String executionId) { public List findJobsByProcessInstanceId(final String processInstanceId) { return getDbSqlSession().selectList("selectSuspendedJobsByProcessInstanceId", processInstanceId); } + + @Override + public List findJobsBySubScopeId(String subScopeId) { + DbSqlSession dbSqlSession = getDbSqlSession(); + return getList(dbSqlSession, "selectSuspendedJobsBySubScopeId", subScopeId, suspendedJobsBySubScopeIdMatcher, true); + } @Override public void updateJobTenantIdForDeployment(String deploymentId, String newTenantId) { diff --git a/modules/flowable-job-service/src/main/java/org/flowable/job/service/impl/persistence/entity/data/impl/cachematcher/SuspendedJobsBySubScopeIdMatcher.java b/modules/flowable-job-service/src/main/java/org/flowable/job/service/impl/persistence/entity/data/impl/cachematcher/SuspendedJobsBySubScopeIdMatcher.java new file mode 100644 index 00000000000..75e29cbbf7e --- /dev/null +++ b/modules/flowable-job-service/src/main/java/org/flowable/job/service/impl/persistence/entity/data/impl/cachematcher/SuspendedJobsBySubScopeIdMatcher.java @@ -0,0 +1,25 @@ +/* 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.job.service.impl.persistence.entity.data.impl.cachematcher; + +import org.flowable.common.engine.impl.persistence.cache.CachedEntityMatcherAdapter; +import org.flowable.job.service.impl.persistence.entity.SuspendedJobEntity; + +public class SuspendedJobsBySubScopeIdMatcher extends CachedEntityMatcherAdapter { + + @Override + public boolean isRetained(SuspendedJobEntity jobEntity, Object param) { + return jobEntity.getSubScopeId() != null && jobEntity.getSubScopeId().equals(param); + } + +} \ No newline at end of file diff --git a/modules/flowable-job-service/src/main/resources/org/flowable/job/service/db/mapping/entity/SuspendedJob.xml b/modules/flowable-job-service/src/main/resources/org/flowable/job/service/db/mapping/entity/SuspendedJob.xml index 3a5792b1a45..1f25b202a3e 100644 --- a/modules/flowable-job-service/src/main/resources/org/flowable/job/service/db/mapping/entity/SuspendedJob.xml +++ b/modules/flowable-job-service/src/main/resources/org/flowable/job/service/db/mapping/entity/SuspendedJob.xml @@ -62,6 +62,13 @@ from ${prefix}ACT_RU_SUSPENDED_JOB J where J.PROCESS_INSTANCE_ID_ = #{parameter, jdbcType=NVARCHAR} + + From 76c3950a3ee4f71bb25a08288634de45c0e7f53c Mon Sep 17 00:00:00 2001 From: Tijs Rademakers Date: Tue, 10 Mar 2026 22:47:16 +0100 Subject: [PATCH 3/3] Implemented delete logic for suspended timer jobs --- .../TimerEventListenerActivityBehaviour.java | 21 ++++- .../cmmn/engine/impl/cmd/ActivateTaskCmd.java | 14 +--- .../cmmn/engine/impl/cmd/SuspendTaskCmd.java | 11 +-- .../entity/PlanItemInstanceEntityManager.java | 2 + .../PlanItemInstanceEntityManagerImpl.java | 27 +++++-- .../data/PlanItemInstanceDataManager.java | 2 + ...ybatisPlanItemInstanceDataManagerImpl.java | 18 +++++ .../db/mapping/entity/PlanItemInstance.xml | 13 ++- .../eventlistener/TimerEventListenerTest.java | 80 +++++++++++++++++++ ...tenerTest.testActiveTimerAndExitStage.cmmn | 26 ++++++ ...enerTest.testSuspendTimerAndExitStage.cmmn | 26 ++++++ 11 files changed, 209 insertions(+), 31 deletions(-) create mode 100644 modules/flowable-cmmn-engine/src/test/resources/org/flowable/cmmn/test/eventlistener/TimerEventListenerTest.testActiveTimerAndExitStage.cmmn create mode 100644 modules/flowable-cmmn-engine/src/test/resources/org/flowable/cmmn/test/eventlistener/TimerEventListenerTest.testSuspendTimerAndExitStage.cmmn diff --git a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/behavior/impl/TimerEventListenerActivityBehaviour.java b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/behavior/impl/TimerEventListenerActivityBehaviour.java index 7e02ee55e04..3455ba75b43 100644 --- a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/behavior/impl/TimerEventListenerActivityBehaviour.java +++ b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/behavior/impl/TimerEventListenerActivityBehaviour.java @@ -21,6 +21,7 @@ import org.apache.commons.lang3.StringUtils; import org.flowable.cmmn.api.delegate.DelegatePlanItemInstance; +import org.flowable.cmmn.api.runtime.PlanItemInstanceState; import org.flowable.cmmn.engine.CmmnEngineConfiguration; import org.flowable.cmmn.engine.impl.behavior.CmmnActivityBehavior; import org.flowable.cmmn.engine.impl.behavior.CoreCmmnActivityBehavior; @@ -45,6 +46,8 @@ import org.flowable.common.engine.impl.util.DateUtil; import org.flowable.job.service.JobServiceConfiguration; import org.flowable.job.service.impl.persistence.entity.JobEntity; +import org.flowable.job.service.impl.persistence.entity.SuspendedJobEntity; +import org.flowable.job.service.impl.persistence.entity.SuspendedJobEntityManager; import org.flowable.job.service.impl.persistence.entity.TimerJobEntity; import org.flowable.job.service.impl.persistence.entity.TimerJobEntityManager; import org.joda.time.DateTime; @@ -73,7 +76,12 @@ public void onStateTransition(CommandContext commandContext, DelegatePlanItemIns || PlanItemTransition.TERMINATE.equals(transition) || PlanItemTransition.EXIT.equals(transition)) { - removeTimerJob(commandContext, (PlanItemInstanceEntity) planItemInstance); + if (PlanItemInstanceState.SUSPENDED.equals(planItemInstance.getState())) { + removeSuspendedJob(commandContext, (PlanItemInstanceEntity) planItemInstance); + + } else { + removeTimerJob(commandContext, (PlanItemInstanceEntity) planItemInstance); + } } } @@ -214,11 +222,20 @@ protected void scheduleTimerJob(CommandContext commandContext, PlanItemInstanceE protected void removeTimerJob(CommandContext commandContext, PlanItemInstanceEntity planItemInstanceEntity) { TimerJobEntityManager timerJobEntityManager = CommandContextUtil.getCmmnEngineConfiguration(commandContext).getJobServiceConfiguration().getTimerJobEntityManager(); List timerJobsEntities = timerJobEntityManager - .findJobsByScopeIdAndSubScopeId(planItemInstanceEntity.getCaseInstanceId(), planItemInstanceEntity.getId()); + .findJobsByScopeIdAndSubScopeId(planItemInstanceEntity.getCaseInstanceId(), planItemInstanceEntity.getId()); for (TimerJobEntity timerJobEntity : timerJobsEntities) { timerJobEntityManager.delete(timerJobEntity); } } + + protected void removeSuspendedJob(CommandContext commandContext, PlanItemInstanceEntity planItemInstanceEntity) { + SuspendedJobEntityManager suspendedJobEntityManager = CommandContextUtil.getCmmnEngineConfiguration(commandContext).getJobServiceConfiguration().getSuspendedJobEntityManager(); + List suspendedJobsEntities = suspendedJobEntityManager + .findJobsBySubScopeId(planItemInstanceEntity.getId()); + for (SuspendedJobEntity suspendedJobEntity : suspendedJobsEntities) { + suspendedJobEntityManager.delete(suspendedJobEntity); + } + } protected Object resolveTimerExpression(CommandContext commandContext, PlanItemInstanceEntity planItemInstanceEntity) { ExpressionManager expressionManager = CommandContextUtil.getExpressionManager(commandContext); diff --git a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/cmd/ActivateTaskCmd.java b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/cmd/ActivateTaskCmd.java index d2eab672c84..1e78a778329 100644 --- a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/cmd/ActivateTaskCmd.java +++ b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/cmd/ActivateTaskCmd.java @@ -15,12 +15,9 @@ import java.util.Date; import java.util.List; -import org.flowable.cmmn.api.runtime.PlanItemInstance; -import org.flowable.cmmn.api.runtime.PlanItemInstanceQuery; import org.flowable.cmmn.api.runtime.PlanItemInstanceState; import org.flowable.cmmn.engine.CmmnEngineConfiguration; import org.flowable.cmmn.engine.impl.persistence.entity.PlanItemInstanceEntity; -import org.flowable.cmmn.engine.impl.runtime.PlanItemInstanceQueryImpl; import org.flowable.cmmn.engine.impl.util.CommandContextUtil; import org.flowable.common.engine.api.FlowableException; import org.flowable.common.engine.api.FlowableIllegalArgumentException; @@ -78,17 +75,12 @@ public Void execute(CommandContext commandContext) { } task.setSuspensionState(SuspensionState.ACTIVE.getStateCode()); - PlanItemInstanceQuery planItemInstanceQuery = new PlanItemInstanceQueryImpl(commandContext, cmmnEngineConfiguration); - planItemInstanceQuery.caseInstanceId(task.getScopeId()) - .planItemDefinitionId(task.getTaskDefinitionKey()) - .planItemInstanceReferenceId(task.getId()); - List planItemInstances = cmmnEngineConfiguration.getPlanItemInstanceEntityManager().findByCriteria(planItemInstanceQuery); + List planItemInstances = cmmnEngineConfiguration.getPlanItemInstanceEntityManager().findByReferenceId(task.getId()); if (planItemInstances != null && !planItemInstances.isEmpty()) { - PlanItemInstanceEntity planItemInstanceEntity = (PlanItemInstanceEntity) planItemInstances.get(0); - planItemInstanceEntity.setState(PlanItemInstanceState.ACTIVE); + planItemInstances.get(0).setState(PlanItemInstanceState.ACTIVE); - cmmnEngineConfiguration.getCmmnHistoryManager().recordPlanItemInstanceUpdated(planItemInstanceEntity); + cmmnEngineConfiguration.getCmmnHistoryManager().recordPlanItemInstanceUpdated(planItemInstances.get(0)); } HistoricTaskService historicTaskService = cmmnEngineConfiguration.getTaskServiceConfiguration().getHistoricTaskService(); diff --git a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/cmd/SuspendTaskCmd.java b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/cmd/SuspendTaskCmd.java index b70425c13da..3b33d41c9d1 100644 --- a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/cmd/SuspendTaskCmd.java +++ b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/cmd/SuspendTaskCmd.java @@ -15,12 +15,9 @@ import java.util.Date; import java.util.List; -import org.flowable.cmmn.api.runtime.PlanItemInstance; -import org.flowable.cmmn.api.runtime.PlanItemInstanceQuery; import org.flowable.cmmn.api.runtime.PlanItemInstanceState; import org.flowable.cmmn.engine.CmmnEngineConfiguration; import org.flowable.cmmn.engine.impl.persistence.entity.PlanItemInstanceEntity; -import org.flowable.cmmn.engine.impl.runtime.PlanItemInstanceQueryImpl; import org.flowable.cmmn.engine.impl.util.CommandContextUtil; import org.flowable.common.engine.impl.db.SuspensionState; import org.flowable.common.engine.impl.interceptor.CommandContext; @@ -50,14 +47,10 @@ protected Void execute(CommandContext commandContext, TaskEntity task) { task.setState(Task.SUSPENDED); task.setSuspensionState(SuspensionState.SUSPENDED.getStateCode()); - PlanItemInstanceQuery planItemInstanceQuery = new PlanItemInstanceQueryImpl(commandContext, cmmnEngineConfiguration); - planItemInstanceQuery.caseInstanceId(task.getScopeId()) - .planItemDefinitionId(task.getTaskDefinitionKey()) - .planItemInstanceReferenceId(task.getId()); - List planItemInstances = cmmnEngineConfiguration.getPlanItemInstanceEntityManager().findByCriteria(planItemInstanceQuery); + List planItemInstances = cmmnEngineConfiguration.getPlanItemInstanceEntityManager().findByReferenceId(task.getId()); if (planItemInstances != null && !planItemInstances.isEmpty()) { - PlanItemInstanceEntity planItemInstanceEntity = (PlanItemInstanceEntity) planItemInstances.get(0); + PlanItemInstanceEntity planItemInstanceEntity = planItemInstances.get(0); planItemInstanceEntity.setState(PlanItemInstanceState.SUSPENDED); planItemInstanceEntity.setLastSuspendedTime(updateTime); diff --git a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/persistence/entity/PlanItemInstanceEntityManager.java b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/persistence/entity/PlanItemInstanceEntityManager.java index 6f98f1f2ec9..ff0a01c5c9f 100644 --- a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/persistence/entity/PlanItemInstanceEntityManager.java +++ b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/persistence/entity/PlanItemInstanceEntityManager.java @@ -47,6 +47,8 @@ public interface PlanItemInstanceEntityManager extends EntityManager findByStagePlanItemInstanceId(String stagePlanItemInstanceId); List findByCaseInstanceIdAndPlanItemId(String caseInstanceId, String planItemId); + + List findByReferenceId(String referenceId); List findByStageInstanceIdAndPlanItemId(String stageInstanceId, String planItemId); diff --git a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/persistence/entity/PlanItemInstanceEntityManagerImpl.java b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/persistence/entity/PlanItemInstanceEntityManagerImpl.java index 8b73b68ee53..61080a4ab97 100644 --- a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/persistence/entity/PlanItemInstanceEntityManagerImpl.java +++ b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/persistence/entity/PlanItemInstanceEntityManagerImpl.java @@ -53,6 +53,8 @@ import org.flowable.identitylink.service.impl.persistence.entity.IdentityLinkEntityManager; import org.flowable.job.service.impl.persistence.entity.ExternalWorkerJobEntity; import org.flowable.job.service.impl.persistence.entity.ExternalWorkerJobEntityManager; +import org.flowable.job.service.impl.persistence.entity.SuspendedJobEntity; +import org.flowable.job.service.impl.persistence.entity.SuspendedJobEntityManager; import org.flowable.job.service.impl.persistence.entity.TimerJobEntity; import org.flowable.job.service.impl.persistence.entity.TimerJobEntityManager; import org.flowable.task.service.impl.persistence.entity.TaskEntity; @@ -420,6 +422,11 @@ public List findByStagePlanItemInstanceId(String stagePl public List findByCaseInstanceIdAndPlanItemId(String caseInstanceId, String planitemId) { return dataManager.findByCaseInstanceIdAndPlanItemId(caseInstanceId, planitemId); } + + @Override + public List findByReferenceId(String referenceId) { + return dataManager.findByReferenceId(referenceId); + } @Override public List findByStageInstanceIdAndPlanItemId(String stageInstanceId, String planItemId) { @@ -529,11 +536,21 @@ public void delete(PlanItemInstanceEntity planItemInstanceEntity, boolean fireEv } if (planItemInstanceEntity.getPlanItemDefinitionType().equals(PlanItemDefinitionType.TIMER_EVENT_LISTENER)) { - TimerJobEntityManager timerJobEntityManager = engineConfiguration.getJobServiceConfiguration().getTimerJobEntityManager(); - List timerJobsEntities = timerJobEntityManager - .findJobsByScopeIdAndSubScopeId(planItemInstanceEntity.getCaseInstanceId(), planItemInstanceEntity.getId()); - for (TimerJobEntity timerJobEntity : timerJobsEntities) { - timerJobEntityManager.delete(timerJobEntity); + if (PlanItemInstanceState.SUSPENDED.equals(planItemInstanceEntity.getState())) { + SuspendedJobEntityManager suspendedJobEntityManager = engineConfiguration.getJobServiceConfiguration().getSuspendedJobEntityManager(); + List suspendedJobsEntities = suspendedJobEntityManager + .findJobsBySubScopeId(planItemInstanceEntity.getId()); + for (SuspendedJobEntity suspendedJobEntity : suspendedJobsEntities) { + suspendedJobEntityManager.delete(suspendedJobEntity); + } + + } else { + TimerJobEntityManager timerJobEntityManager = engineConfiguration.getJobServiceConfiguration().getTimerJobEntityManager(); + List timerJobsEntities = timerJobEntityManager + .findJobsByScopeIdAndSubScopeId(planItemInstanceEntity.getCaseInstanceId(), planItemInstanceEntity.getId()); + for (TimerJobEntity timerJobEntity : timerJobsEntities) { + timerJobEntityManager.delete(timerJobEntity); + } } } diff --git a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/persistence/entity/data/PlanItemInstanceDataManager.java b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/persistence/entity/data/PlanItemInstanceDataManager.java index 6d47f9ea3d3..0b74319dcb8 100644 --- a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/persistence/entity/data/PlanItemInstanceDataManager.java +++ b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/persistence/entity/data/PlanItemInstanceDataManager.java @@ -30,6 +30,8 @@ public interface PlanItemInstanceDataManager extends DataManager findByCaseInstanceIdAndPlanItemId(String caseInstanceId, String planitemId); + List findByReferenceId(String referenceId); + List findByStageInstanceIdAndPlanItemId(String stageInstanceId, String planItemId); List findByCaseInstanceIdAndTypeAndState(String caseInstanceId, List planItemDefinitionTypes, diff --git a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/persistence/entity/data/impl/MybatisPlanItemInstanceDataManagerImpl.java b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/persistence/entity/data/impl/MybatisPlanItemInstanceDataManagerImpl.java index de7219d915f..7d14b830527 100644 --- a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/persistence/entity/data/impl/MybatisPlanItemInstanceDataManagerImpl.java +++ b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/persistence/entity/data/impl/MybatisPlanItemInstanceDataManagerImpl.java @@ -43,6 +43,9 @@ public class MybatisPlanItemInstanceDataManagerImpl extends AbstractCmmnDataMana protected PlanItemInstanceByCaseInstanceIdAndTypeAndStateCachedEntityMatcher planItemInstanceByCaseInstanceIdAndTypeAndStateCachedEntityMatcher = new PlanItemInstanceByCaseInstanceIdAndTypeAndStateCachedEntityMatcher(); + + protected PlanItemInstanceByReferenceIdCachedEntityMatcher planItemInstanceByReferenceIdCachedEntityMatcher = + new PlanItemInstanceByReferenceIdCachedEntityMatcher(); protected PlanItemInstanceByStagePlanItemInstanceIdCachedEntityMatcher planItemInstanceByStagePlanItemInstanceIdCachedEntityMatcher = new PlanItemInstanceByStagePlanItemInstanceIdCachedEntityMatcher(); @@ -101,6 +104,11 @@ public List findByCaseInstanceIdAndPlanItemId(String cas params.put("planItemId", planitemId); return getList("selectPlanItemInstancesByCaseInstanceIdAndPlanItemId", params, planItemInstanceByCaseInstanceIdAndPlanItemIdCachedEntityMatcher); } + + @Override + public List findByReferenceId(String referenceId) { + return getList("selectPlanItemInstancesByReferenceId", referenceId, planItemInstanceByReferenceIdCachedEntityMatcher, true); + } @Override public List findByStageInstanceIdAndPlanItemId(String stageInstanceId, String planItemId) { @@ -172,6 +180,16 @@ public boolean isRetained(PlanItemInstanceEntity entity, Object param) { } } + + public static class PlanItemInstanceByReferenceIdCachedEntityMatcher extends CachedEntityMatcherAdapter { + + @Override + public boolean isRetained(PlanItemInstanceEntity entity, Object param) { + String referenceId = (String) param; + return referenceId.equals(entity.getReferenceId()); + } + + } public static class PlanItemInstanceByCaseInstanceIdAndPlanItemIdCachedEntityMatcher extends CachedEntityMatcherAdapter { diff --git a/modules/flowable-cmmn-engine/src/main/resources/org/flowable/cmmn/db/mapping/entity/PlanItemInstance.xml b/modules/flowable-cmmn-engine/src/main/resources/org/flowable/cmmn/db/mapping/entity/PlanItemInstance.xml index 5b8fafda665..46309f27c04 100644 --- a/modules/flowable-cmmn-engine/src/main/resources/org/flowable/cmmn/db/mapping/entity/PlanItemInstance.xml +++ b/modules/flowable-cmmn-engine/src/main/resources/org/flowable/cmmn/db/mapping/entity/PlanItemInstance.xml @@ -358,11 +358,16 @@ select * from ${prefix}ACT_CMMN_RU_PLAN_ITEM_INST RES where CASE_INST_ID_ = #{parameter.caseInstanceId, jdbcType=VARCHAR} and ELEMENT_ID_ = #{parameter.planItemId, jdbcType=VARCHAR} + + - +