From 3c58869038cff49fd207fe88e7e630719b6b841d Mon Sep 17 00:00:00 2001 From: Tijs Rademakers Date: Fri, 30 Aug 2024 13:46:02 +0200 Subject: [PATCH 1/8] Automatically terminate plan item instances for which a plan item definition doesn't exist in the target cmmn definition with a case migration --- .../AbstractCmmnDynamicStateManager.java | 31 +++++ .../migration/CaseInstanceMigrationTest.java | 124 ++++++++++++++++++ 2 files changed, 155 insertions(+) diff --git a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/runtime/AbstractCmmnDynamicStateManager.java b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/runtime/AbstractCmmnDynamicStateManager.java index e9837e3d3c4..61eef26eb90 100644 --- a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/runtime/AbstractCmmnDynamicStateManager.java +++ b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/runtime/AbstractCmmnDynamicStateManager.java @@ -133,6 +133,8 @@ protected void doMovePlanItemState(CaseInstanceChangeState caseInstanceChangeSta executeTerminatePlanItemInstances(caseInstanceChangeState, caseInstance, commandContext); + executeTerminateNonExistingPlanItemInstancesInTargetCmmnModel(caseInstanceChangeState, commandContext); + setCaseDefinitionIdForPlanItemInstances(currentPlanItemInstances, caseInstanceChangeState.getCaseDefinitionToMigrateTo()); executeChangePlanItemIds(caseInstanceChangeState, originalCaseDefinitionId, commandContext); @@ -835,6 +837,35 @@ protected void executeTerminatePlanItemInstances(CaseInstanceChangeState caseIns } } + protected void executeTerminateNonExistingPlanItemInstancesInTargetCmmnModel(CaseInstanceChangeState caseInstanceChangeState, CommandContext commandContext) { + if (caseInstanceChangeState.getCaseDefinitionToMigrateTo() != null) { + CmmnModel targetCmmnModel = CaseDefinitionUtil.getCmmnModel(caseInstanceChangeState.getCaseDefinitionToMigrateTo().getId()); + List excludePlanItemDefinitionIds = new ArrayList<>(); + for (TerminatePlanItemDefinitionMapping planItemDefinitionMapping : caseInstanceChangeState.getTerminatePlanItemDefinitions()) { + excludePlanItemDefinitionIds.add(planItemDefinitionMapping.getPlanItemDefinitionId()); + } + + for (ChangePlanItemDefinitionWithNewTargetIdsMapping newTargetIdsMapping : caseInstanceChangeState.getChangePlanItemDefinitionWithNewTargetIds()) { + excludePlanItemDefinitionIds.add(newTargetIdsMapping.getExistingPlanItemDefinitionId()); + } + + for (ChangePlanItemIdWithDefinitionIdMapping definitionIdMapping : caseInstanceChangeState.getChangePlanItemIdsWithDefinitionId()) { + excludePlanItemDefinitionIds.add(definitionIdMapping.getExistingPlanItemDefinitionId()); + } + + for (String currentPlanItemDefinitionId : caseInstanceChangeState.getCurrentPlanItemInstances().keySet()) { + if (!excludePlanItemDefinitionIds.contains(currentPlanItemDefinitionId) && targetCmmnModel.findPlanItemDefinition(currentPlanItemDefinitionId) == null) { + for (PlanItemInstanceEntity currentPlanItemInstance : caseInstanceChangeState.getCurrentPlanItemInstances().get(currentPlanItemDefinitionId)) { + if (!PlanItemInstanceState.TERMINAL_STATES.contains(currentPlanItemInstance.getState())) { + terminatePlanItemInstance(currentPlanItemInstance, commandContext); + caseInstanceChangeState.addTerminatedPlanItemInstance(currentPlanItemInstance.getPlanItemDefinitionId(), currentPlanItemInstance); + } + } + } + } + } + } + protected abstract boolean isDirectPlanItemDefinitionMigration(PlanItemDefinition currentPlanItemDefinition, PlanItemDefinition newPlanItemDefinition); protected Map> retrievePlanItemInstances(String caseInstanceId) { diff --git a/modules/flowable-cmmn-engine/src/test/java/org/flowable/cmmn/test/migration/CaseInstanceMigrationTest.java b/modules/flowable-cmmn-engine/src/test/java/org/flowable/cmmn/test/migration/CaseInstanceMigrationTest.java index 37f14ea8f65..ed86e51d963 100644 --- a/modules/flowable-cmmn-engine/src/test/java/org/flowable/cmmn/test/migration/CaseInstanceMigrationTest.java +++ b/modules/flowable-cmmn-engine/src/test/java/org/flowable/cmmn/test/migration/CaseInstanceMigrationTest.java @@ -439,6 +439,130 @@ void withSimpleOneTaskCaseChangingOnlyTaskProperties() { } } } + + @Test + void withAutomatedMigrationFromTwoTasksToOneTask() { + // Arrange + deployCaseDefinition("test1", "org/flowable/cmmn/test/migration/two-task.cmmn.xml"); + CaseInstance caseInstance = cmmnRuntimeService.createCaseInstanceBuilder().caseDefinitionKey("testCase").start(); + CaseDefinition destinationDefinition = deployCaseDefinition("test1", "org/flowable/cmmn/test/migration/one-task.cmmn.xml"); + + // Act + cmmnMigrationService.createCaseInstanceMigrationBuilder() + .migrateToCaseDefinition(destinationDefinition.getId()) + .migrate(caseInstance.getId()); + + // Assert + CaseInstance caseInstanceAfterMigration = cmmnRuntimeService.createCaseInstanceQuery() + .caseInstanceId(caseInstance.getId()) + .singleResult(); + assertThat(caseInstanceAfterMigration.getCaseDefinitionId()).isEqualTo(destinationDefinition.getId()); + assertThat(caseInstanceAfterMigration.getCaseDefinitionVersion()).isEqualTo(2); + assertThat(caseInstanceAfterMigration.getCaseDefinitionDeploymentId()).isEqualTo(destinationDefinition.getDeploymentId()); + List planItemInstances = cmmnRuntimeService.createPlanItemInstanceQuery() + .caseInstanceId(caseInstance.getId()) + .list(); + assertThat(planItemInstances).hasSize(1); + assertThat(planItemInstances) + .extracting(PlanItemInstance::getCaseDefinitionId) + .containsOnly(destinationDefinition.getId()); + assertThat(planItemInstances) + .extracting(PlanItemInstance::getName) + .containsExactlyInAnyOrder("Task 1"); + assertThat(planItemInstances) + .extracting(PlanItemInstance::getState) + .containsOnly(PlanItemInstanceState.ACTIVE); + + List tasks = cmmnTaskService.createTaskQuery().caseInstanceId(caseInstance.getId()).list(); + assertThat(tasks).hasSize(1); + assertThat(tasks.get(0).getScopeDefinitionId()).isEqualTo(destinationDefinition.getId()); + cmmnTaskService.complete(tasks.get(0).getId()); + + assertThat(cmmnRuntimeService.createCaseInstanceQuery().caseInstanceId(caseInstance.getId()).count()).isZero(); + + if (CmmnHistoryTestHelper.isHistoryLevelAtLeast(HistoryLevel.ACTIVITY, cmmnEngineConfiguration)) { + assertThat(cmmnHistoryService.createHistoricCaseInstanceQuery().caseInstanceId(caseInstance.getId()).count()).isEqualTo(1); + assertThat(cmmnHistoryService.createHistoricCaseInstanceQuery().caseInstanceId(caseInstance.getId()).singleResult().getCaseDefinitionId()) + .isEqualTo(destinationDefinition.getId()); + + List historicPlanItemInstances = cmmnHistoryService.createHistoricPlanItemInstanceQuery() + .planItemInstanceCaseInstanceId(caseInstance.getId()).list(); + assertThat(historicPlanItemInstances).hasSize(2); + for (HistoricPlanItemInstance historicPlanItemInstance : historicPlanItemInstances) { + assertThat(historicPlanItemInstance.getCaseDefinitionId()).isEqualTo(destinationDefinition.getId()); + } + + List historicTasks = cmmnHistoryService.createHistoricTaskInstanceQuery().caseInstanceId(caseInstance.getId()).list(); + assertThat(historicTasks).hasSize(2); + for (HistoricTaskInstance historicTask : historicTasks) { + assertThat(historicTask.getScopeDefinitionId()).isEqualTo(destinationDefinition.getId()); + } + } + } + + @Test + void withActivateTaskFromTwoTasksToOneTask() { + // Arrange + deployCaseDefinition("test1", "org/flowable/cmmn/test/migration/two-task.cmmn.xml"); + CaseInstance caseInstance = cmmnRuntimeService.createCaseInstanceBuilder().caseDefinitionKey("testCase").start(); + CaseDefinition destinationDefinition = deployCaseDefinition("test1", "org/flowable/cmmn/test/migration/one-task.cmmn.xml"); + + Task task = cmmnTaskService.createTaskQuery().caseInstanceId(caseInstance.getId()).taskDefinitionKey("humanTask1").singleResult(); + cmmnTaskService.complete(task.getId()); + + // Act + cmmnMigrationService.createCaseInstanceMigrationBuilder() + .migrateToCaseDefinition(destinationDefinition.getId()) + .addActivatePlanItemDefinitionMapping(PlanItemDefinitionMappingBuilder.createActivatePlanItemDefinitionMappingFor("humanTask1")) + .migrate(caseInstance.getId()); + + // Assert + CaseInstance caseInstanceAfterMigration = cmmnRuntimeService.createCaseInstanceQuery() + .caseInstanceId(caseInstance.getId()) + .singleResult(); + assertThat(caseInstanceAfterMigration.getCaseDefinitionId()).isEqualTo(destinationDefinition.getId()); + assertThat(caseInstanceAfterMigration.getCaseDefinitionVersion()).isEqualTo(2); + assertThat(caseInstanceAfterMigration.getCaseDefinitionDeploymentId()).isEqualTo(destinationDefinition.getDeploymentId()); + List planItemInstances = cmmnRuntimeService.createPlanItemInstanceQuery() + .caseInstanceId(caseInstance.getId()) + .list(); + assertThat(planItemInstances).hasSize(1); + assertThat(planItemInstances) + .extracting(PlanItemInstance::getCaseDefinitionId) + .containsOnly(destinationDefinition.getId()); + assertThat(planItemInstances) + .extracting(PlanItemInstance::getName) + .containsExactlyInAnyOrder("Task 1"); + assertThat(planItemInstances) + .extracting(PlanItemInstance::getState) + .containsOnly(PlanItemInstanceState.ACTIVE); + + List tasks = cmmnTaskService.createTaskQuery().caseInstanceId(caseInstance.getId()).list(); + assertThat(tasks).hasSize(1); + assertThat(tasks.get(0).getScopeDefinitionId()).isEqualTo(destinationDefinition.getId()); + cmmnTaskService.complete(tasks.get(0).getId()); + + assertThat(cmmnRuntimeService.createCaseInstanceQuery().caseInstanceId(caseInstance.getId()).count()).isZero(); + + if (CmmnHistoryTestHelper.isHistoryLevelAtLeast(HistoryLevel.ACTIVITY, cmmnEngineConfiguration)) { + assertThat(cmmnHistoryService.createHistoricCaseInstanceQuery().caseInstanceId(caseInstance.getId()).count()).isEqualTo(1); + assertThat(cmmnHistoryService.createHistoricCaseInstanceQuery().caseInstanceId(caseInstance.getId()).singleResult().getCaseDefinitionId()) + .isEqualTo(destinationDefinition.getId()); + + List historicPlanItemInstances = cmmnHistoryService.createHistoricPlanItemInstanceQuery() + .planItemInstanceCaseInstanceId(caseInstance.getId()).list(); + assertThat(historicPlanItemInstances).hasSize(3); + for (HistoricPlanItemInstance historicPlanItemInstance : historicPlanItemInstances) { + assertThat(historicPlanItemInstance.getCaseDefinitionId()).isEqualTo(destinationDefinition.getId()); + } + + List historicTasks = cmmnHistoryService.createHistoricTaskInstanceQuery().caseInstanceId(caseInstance.getId()).list(); + assertThat(historicTasks).hasSize(3); + for (HistoricTaskInstance historicTask : historicTasks) { + assertThat(historicTask.getScopeDefinitionId()).isEqualTo(destinationDefinition.getId()); + } + } + } @Test void withTwoTasksIntroducingANewStageAroundSecondTask() { From c1adb7bf82ea2dba6488edd0e3c66b2edbd4b150 Mon Sep 17 00:00:00 2001 From: Joram Barrez Date: Fri, 30 Aug 2024 14:43:53 +0200 Subject: [PATCH 2/8] Fix bug: stage plan item instance automatically being completed when setting variable through runtimeService in a lifecycleListener --- .../impl/agenda/DefaultCmmnEngineAgenda.java | 2 + ...tChangePlanItemInstanceStateOperation.java | 9 + .../AbstractEvaluationCriteriaOperation.java | 2 +- .../impl/agenda/operation/CmmnOperation.java | 7 + .../entity/PlanItemInstanceEntity.java | 5 +- .../entity/PlanItemInstanceEntityImpl.java | 12 ++ .../test/AbstractFlowableCmmnTestCase.java | 3 +- .../test/delegate/TestSetVariableBean.java | 28 +++ .../test/runtime/StageCompletionTest.java | 65 ++++++ .../src/test/resources/flowable.cmmn.cfg.xml | 5 +- ...estSetVariableInLifecycleListener.cmmn.xml | 193 ++++++++++++++++++ 11 files changed, 327 insertions(+), 4 deletions(-) create mode 100644 modules/flowable-cmmn-engine/src/test/java/org/flowable/cmmn/test/delegate/TestSetVariableBean.java create mode 100644 modules/flowable-cmmn-engine/src/test/java/org/flowable/cmmn/test/runtime/StageCompletionTest.java create mode 100644 modules/flowable-cmmn-engine/src/test/resources/org/flowable/cmmn/test/runtime/StageCompletionTest.testSetVariableInLifecycleListener.cmmn.xml 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 66aae05a5fd..b4438edfe9d 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 @@ -69,6 +69,8 @@ public DefaultCmmnEngineAgenda(CommandContext commandContext) { } public void addOperation(CmmnOperation operation) { + + operation.onPlanned(); int operationIndex = getOperationIndex(operation); if (operationIndex >= 0) { diff --git a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/operation/AbstractChangePlanItemInstanceStateOperation.java b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/operation/AbstractChangePlanItemInstanceStateOperation.java index 2616ea28d2b..745d095ec96 100644 --- a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/operation/AbstractChangePlanItemInstanceStateOperation.java +++ b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/operation/AbstractChangePlanItemInstanceStateOperation.java @@ -34,6 +34,12 @@ public AbstractChangePlanItemInstanceStateOperation(CommandContext commandContex super(commandContext, planItemInstanceEntity); } + @Override + public void onPlanned() { + // The plan item is marked as being 'in flux'. After the state is changed, the flag is changed back (see below). + this.planItemInstanceEntity.setStateChangeUnprocessed(true); + } + @Override public void run() { String oldState = planItemInstanceEntity.getState(); @@ -53,12 +59,15 @@ public void run() { } planItemInstanceEntity.setState(newState); + CmmnEngineConfiguration cmmnEngineConfiguration =CommandContextUtil.getCmmnEngineConfiguration(commandContext); cmmnEngineConfiguration.getListenerNotificationHelper().executeLifecycleListeners( commandContext, planItemInstanceEntity, oldState, getNewState()); CommandContextUtil.getAgenda(commandContext).planEvaluateCriteriaOperation(planItemInstanceEntity.getCaseInstanceId(), createPlanItemLifeCycleEvent()); internalExecute(); + + planItemInstanceEntity.setStateChangeUnprocessed(false); if (CommandContextUtil.getCmmnEngineConfiguration(commandContext).isLoggingSessionEnabled()) { String loggingType = null; diff --git a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/operation/AbstractEvaluationCriteriaOperation.java b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/operation/AbstractEvaluationCriteriaOperation.java index 152be2b31b2..7f8f4e4dcb7 100644 --- a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/operation/AbstractEvaluationCriteriaOperation.java +++ b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/operation/AbstractEvaluationCriteriaOperation.java @@ -145,7 +145,7 @@ public boolean evaluateForCompletion(PlanItemInstanceEntity planItemInstanceEnti } else if (planItem.getPlanItemDefinition() instanceof Stage) { - if (PlanItemInstanceState.ACTIVE.equals(state)) { + if (PlanItemInstanceState.ACTIVE.equals(state) && !planItemInstanceEntity.isStateChangeUnprocessed()) { boolean criteriaChangeOrActiveChildrenForStage = evaluatePlanItemsCriteria(planItemInstanceEntity, null); if (criteriaChangeOrActiveChildrenForStage) { evaluationResult.markCriteriaChanged(); diff --git a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/operation/CmmnOperation.java b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/operation/CmmnOperation.java index b81ed4f42ce..e4f8f2e6fbf 100644 --- a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/operation/CmmnOperation.java +++ b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/operation/CmmnOperation.java @@ -53,6 +53,13 @@ public CmmnOperation(CommandContext commandContext) { this.commandContext = commandContext; } + /** + * Called when the operation is planned on the agenda (but not yet executed) + */ + public void onPlanned() { + // No-op by default + } + /** * @return The id of the case instance related to this operation. */ diff --git a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/persistence/entity/PlanItemInstanceEntity.java b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/persistence/entity/PlanItemInstanceEntity.java index 0ab87907f19..2d1553a9b85 100644 --- a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/persistence/entity/PlanItemInstanceEntity.java +++ b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/persistence/entity/PlanItemInstanceEntity.java @@ -27,6 +27,9 @@ public interface PlanItemInstanceEntity extends Entity, HasRevision, DelegatePla VariableScope getParentVariableScope(); boolean isPlannedForActivationInMigration(); - void setPlannedForActivationInMigration(boolean plannedForActivationInMigration); + + boolean isStateChangeUnprocessed(); + void setStateChangeUnprocessed(boolean stateChangeUnprocessed); + } diff --git a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/persistence/entity/PlanItemInstanceEntityImpl.java b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/persistence/entity/PlanItemInstanceEntityImpl.java index 94df68d8934..ef9409dc6cd 100644 --- a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/persistence/entity/PlanItemInstanceEntityImpl.java +++ b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/persistence/entity/PlanItemInstanceEntityImpl.java @@ -94,6 +94,8 @@ public class PlanItemInstanceEntityImpl extends AbstractCmmnEngineVariableScopeE protected FlowableListener currentFlowableListener; // Only set when executing an plan item lifecycle listener protected boolean plannedForActivationInMigration; + protected boolean stateChangeUnprocessed; // only set to true when an agenda operation is planned and this has not been executed yet + public PlanItemInstanceEntityImpl() { } @@ -657,6 +659,16 @@ public void setPlannedForActivationInMigration(boolean plannedForActivationInMig this.plannedForActivationInMigration = plannedForActivationInMigration; } + @Override + public boolean isStateChangeUnprocessed() { + return stateChangeUnprocessed; + } + + @Override + public void setStateChangeUnprocessed(boolean stateChangeUnprocessed) { + this.stateChangeUnprocessed = stateChangeUnprocessed; + } + @Override public String toString() { StringBuilder stringBuilder = new StringBuilder(); diff --git a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/test/AbstractFlowableCmmnTestCase.java b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/test/AbstractFlowableCmmnTestCase.java index 80a43c4c8ec..5c7a4f34aa6 100644 --- a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/test/AbstractFlowableCmmnTestCase.java +++ b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/test/AbstractFlowableCmmnTestCase.java @@ -267,7 +267,8 @@ protected void assertPlanItemInstanceState(List planItemInstan .collect(Collectors.toList()); if (planItemInstanceStates.isEmpty()) { - fail("No plan item instances found with name " + name); + List planItemInstanceNames = planItemInstances.stream().map(PlanItemInstance::getName).collect(Collectors.toList()); + fail("No plan item instances found with name " + name + ", following names were found:" + String.join(",", planItemInstanceNames)); } assertEquals("Incorrect number of states found: " + planItemInstanceStates, states.length, planItemInstanceStates.size()); diff --git a/modules/flowable-cmmn-engine/src/test/java/org/flowable/cmmn/test/delegate/TestSetVariableBean.java b/modules/flowable-cmmn-engine/src/test/java/org/flowable/cmmn/test/delegate/TestSetVariableBean.java new file mode 100644 index 00000000000..90540b6421c --- /dev/null +++ b/modules/flowable-cmmn-engine/src/test/java/org/flowable/cmmn/test/delegate/TestSetVariableBean.java @@ -0,0 +1,28 @@ +/* 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.test.delegate; + +import org.flowable.cmmn.api.CmmnRuntimeService; +import org.flowable.cmmn.engine.impl.util.CommandContextUtil; + +/** + * @author Joram Barrez + */ +public class TestSetVariableBean { + + public void setVariable(String caseInstanceId, String variableName, Object variableValue) { + CmmnRuntimeService cmmnRuntimeService = CommandContextUtil.getCmmnEngineConfiguration().getCmmnRuntimeService(); + cmmnRuntimeService.setVariable(caseInstanceId, variableName, variableValue); + } + +} diff --git a/modules/flowable-cmmn-engine/src/test/java/org/flowable/cmmn/test/runtime/StageCompletionTest.java b/modules/flowable-cmmn-engine/src/test/java/org/flowable/cmmn/test/runtime/StageCompletionTest.java new file mode 100644 index 00000000000..9eeb192c0cf --- /dev/null +++ b/modules/flowable-cmmn-engine/src/test/java/org/flowable/cmmn/test/runtime/StageCompletionTest.java @@ -0,0 +1,65 @@ +/* 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.test.runtime; + +import java.util.List; + +import org.flowable.cmmn.api.runtime.CaseInstance; +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.FlowableCmmnTestCase; +import org.flowable.task.api.Task; +import org.junit.Test; + +/** + * @author Joram Barrez + */ +public class StageCompletionTest extends FlowableCmmnTestCase { + + @Test + @CmmnDeployment + public void testSetVariableInLifecycleListener() { + + /* + * This test has a case which has a stage with a lifecycle listener that sets a variable through the cmmnRuntimeService. + * Before introducing the flag 'isStateChangeUnprocessed' on PlanItemInstanceEntity, this would lead to the following problem: + * + * startCaseInstance --> new CommandContext --> plan operations for initialization + moving stage plan item instance to state 'active' + * setVariable, through a service, will reuse the existing commandContext. However, the SetVariableCmd plans an explicit evaluation operation (for good reasons) + * + * When the evaluation operations executes, the stage has moved into the 'active' state, however when the lifecycle listener executes + * no child plan item instances are created yet. The logic deems correctly that this constellation means the stage should complete. + * + * Introducing the stateChangeUnprocessed flag on the stage plan item instance fixes this problem: it avoids looking into stages that + * are still being initialized but have a setup that would otherwise automatically complete the stage plan item instance. + */ + + CaseInstance caseInstance = cmmnRuntimeService.createCaseInstanceBuilder().caseDefinitionKey("myCase").start(); + List planItemInstances = cmmnRuntimeService.createPlanItemInstanceQuery().caseInstanceId(caseInstance.getId()).list(); + assertPlanItemInstanceState(caseInstance, "stageWithLifecycleListener", PlanItemInstanceState.AVAILABLE); + + // Triggering the user event listener activates the stage + cmmnRuntimeService.completeUserEventListenerInstance(planItemInstances.stream() + .filter(planItemInstance -> "A".equalsIgnoreCase(planItemInstance.getName())).findAny().get().getId()); + + assertPlanItemInstanceState(caseInstance, "stageWithLifecycleListener", PlanItemInstanceState.ACTIVE); + + // Completing the user tasks should complete the case instance + Task task = cmmnTaskService.createTaskQuery().caseInstanceId(caseInstance.getId()).singleResult(); + cmmnTaskService.complete(task.getId()); + + assertCaseInstanceEnded(caseInstance); + } + +} diff --git a/modules/flowable-cmmn-engine/src/test/resources/flowable.cmmn.cfg.xml b/modules/flowable-cmmn-engine/src/test/resources/flowable.cmmn.cfg.xml index 6c244497dcd..0f8bce19e70 100644 --- a/modules/flowable-cmmn-engine/src/test/resources/flowable.cmmn.cfg.xml +++ b/modules/flowable-cmmn-engine/src/test/resources/flowable.cmmn.cfg.xml @@ -59,6 +59,7 @@ + @@ -70,5 +71,7 @@ - + + + diff --git a/modules/flowable-cmmn-engine/src/test/resources/org/flowable/cmmn/test/runtime/StageCompletionTest.testSetVariableInLifecycleListener.cmmn.xml b/modules/flowable-cmmn-engine/src/test/resources/org/flowable/cmmn/test/runtime/StageCompletionTest.testSetVariableInLifecycleListener.cmmn.xml new file mode 100644 index 00000000000..d357244d747 --- /dev/null +++ b/modules/flowable-cmmn-engine/src/test/resources/org/flowable/cmmn/test/runtime/StageCompletionTest.testSetVariableInLifecycleListener.cmmn.xml @@ -0,0 +1,193 @@ + + + + + + + + + + + + + + + + + + + + + + + complete + + + + + + + + complete + + + + + + + + occur + + + + + + + + occur + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 600d013611215684d6d1b7992eed85a81dd1fed5 Mon Sep 17 00:00:00 2001 From: Joram Barrez Date: Fri, 30 Aug 2024 15:03:49 +0200 Subject: [PATCH 3/8] Small improvement to last commit c1adb7bf82ea2dba6488edd0e3c66b2edbd4b150 : set completable to false to indicate correct state --- .../AbstractEvaluationCriteriaOperation.java | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/operation/AbstractEvaluationCriteriaOperation.java b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/operation/AbstractEvaluationCriteriaOperation.java index 7f8f4e4dcb7..41856e008fe 100644 --- a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/operation/AbstractEvaluationCriteriaOperation.java +++ b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/agenda/operation/AbstractEvaluationCriteriaOperation.java @@ -145,18 +145,24 @@ public boolean evaluateForCompletion(PlanItemInstanceEntity planItemInstanceEnti } else if (planItem.getPlanItemDefinition() instanceof Stage) { - if (PlanItemInstanceState.ACTIVE.equals(state) && !planItemInstanceEntity.isStateChangeUnprocessed()) { - boolean criteriaChangeOrActiveChildrenForStage = evaluatePlanItemsCriteria(planItemInstanceEntity, null); - if (criteriaChangeOrActiveChildrenForStage) { - evaluationResult.markCriteriaChanged(); - planItemInstanceEntity.setCompletable(false); // an active child = stage cannot be completed anymore - } else { - Stage stage = (Stage) planItem.getPlanItemDefinition(); - if (isStageCompletable(planItemInstanceEntity, stage)) { + if (PlanItemInstanceState.ACTIVE.equals(state)) { + + if (!planItemInstanceEntity.isStateChangeUnprocessed()) { // when the stage plan item instance state is not stable yet, don't evaluate yet + boolean criteriaChangeOrActiveChildrenForStage = evaluatePlanItemsCriteria(planItemInstanceEntity, null); + if (criteriaChangeOrActiveChildrenForStage) { evaluationResult.markCriteriaChanged(); - CommandContextUtil.getAgenda(commandContext).planCompletePlanItemInstanceOperation(planItemInstanceEntity); + planItemInstanceEntity.setCompletable(false); // an active child = stage cannot be completed anymore + } else { + Stage stage = (Stage) planItem.getPlanItemDefinition(); + if (isStageCompletable(planItemInstanceEntity, stage)) { + evaluationResult.markCriteriaChanged(); + CommandContextUtil.getAgenda(commandContext).planCompletePlanItemInstanceOperation(planItemInstanceEntity); + } } + } else { + planItemInstanceEntity.setCompletable(false); } + } } else if (PlanItemInstanceState.ACTIVE.equals(state)) { // check, if the plan item can be ignored for further processing and if so, immediately return From b89cad95cec974cd29942fe405772e527e5bca6b Mon Sep 17 00:00:00 2001 From: Bas Claessen <35045227+basclaessen@users.noreply.github.com> Date: Mon, 2 Sep 2024 09:16:00 +0200 Subject: [PATCH 4/8] Fixed missing subscriptions after migration of an active embedded non interrupting event sub process (#3928) (#3929) * Fixed missing subscriptions after migration of an active non interrupting event sub process (#3928) * Added missing package import * In case of a timer event, only process event sub process when no timer job is present * Added testcases and fix when migrating an event sub process with two started sub processes * Fixed imports --- .../dynamic/AbstractDynamicStateManager.java | 26 +- .../dynamic/MoveExecutionEntityContainer.java | 9 + ...nceMigrationEventRegistryConsumerTest.java | 122 ++++ ...ationEventRegistryEventSubprocessTest.java | 253 +++++++ ...sInstanceMigrationEventSubProcessTest.java | 685 ++++++++++++++++++ ...-eventregistry-event-subprocess.bpmn20.xml | 28 + 6 files changed, 1119 insertions(+), 4 deletions(-) create mode 100644 modules/flowable-engine/src/test/java/org/flowable/engine/test/api/runtime/migration/AbstractProcessInstanceMigrationEventRegistryConsumerTest.java create mode 100644 modules/flowable-engine/src/test/java/org/flowable/engine/test/api/runtime/migration/ProcessInstanceMigrationEventRegistryEventSubprocessTest.java create mode 100644 modules/flowable-engine/src/test/resources/org/flowable/engine/test/api/runtime/migration/non-interrupting-eventregistry-event-subprocess.bpmn20.xml diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/dynamic/AbstractDynamicStateManager.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/dynamic/AbstractDynamicStateManager.java index 63f6140142b..61d03b26c77 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/dynamic/AbstractDynamicStateManager.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/dynamic/AbstractDynamicStateManager.java @@ -654,11 +654,24 @@ protected void doMoveExecutionState(ProcessInstanceChangeState processInstanceCh } protected void processPendingEventSubProcessesStartEvents(ProcessInstanceChangeState processInstanceChangeState, CommandContext commandContext) { - ProcessInstanceHelper processInstanceHelper = CommandContextUtil.getProcessEngineConfiguration(commandContext).getProcessInstanceHelper(); + ProcessEngineConfigurationImpl processEngineConfiguration = CommandContextUtil.getProcessEngineConfiguration(commandContext); + ProcessInstanceHelper processInstanceHelper = processEngineConfiguration.getProcessInstanceHelper(); + EventSubscriptionService eventSubscriptionService = processEngineConfiguration.getEventSubscriptionServiceConfiguration().getEventSubscriptionService(); + ManagementService managementService = processEngineConfiguration.getManagementService(); + for (Map.Entry pendingStartEventEntry : processInstanceChangeState.getPendingEventSubProcessesStartEvents().entrySet()) { StartEvent startEvent = pendingStartEventEntry.getKey(); ExecutionEntity parentExecution = pendingStartEventEntry.getValue(); - if (!processInstanceChangeState.getCreatedEmbeddedSubProcesses().containsKey(startEvent.getSubProcess().getId())) { + EventDefinition eventDefinition = startEvent.getEventDefinitions().isEmpty() ? null : startEvent.getEventDefinitions().get(0); + + //Process event sub process when no subscriptions/timer jobs are found + boolean processEventSubProcess = false; + if (eventDefinition instanceof TimerEventDefinition) { + processEventSubProcess = managementService.createTimerJobQuery().executionId(parentExecution.getId()).list().isEmpty(); + } else { + processEventSubProcess = eventSubscriptionService.findEventSubscriptionsByExecution(parentExecution.getId()).isEmpty(); + } + if (processEventSubProcess) { processInstanceHelper.processEventSubProcess(parentExecution, (EventSubProcess) startEvent.getSubProcess(), commandContext); } } @@ -809,7 +822,10 @@ protected List createEmbeddedSubProcessAndExecutions(Collection // Build the subProcess hierarchy for (SubProcess subProcess : subProcessesToCreate.values()) { - if (!processInstanceChangeState.getCreatedEmbeddedSubProcesses().containsKey(subProcess.getId())) { + if (subProcess instanceof EventSubProcess) { + ExecutionEntity embeddedSubProcess = createEmbeddedSubProcessHierarchy(subProcess, defaultContinueParentExecution, subProcessesToCreate, movingExecutionIds, processInstanceChangeState, commandContext); + moveExecutionEntityContainer.addCreatedEventSubProcess(subProcess.getId(), embeddedSubProcess); + } else if (!processInstanceChangeState.getCreatedEmbeddedSubProcesses().containsKey(subProcess.getId())) { ExecutionEntity embeddedSubProcess = createEmbeddedSubProcessHierarchy(subProcess, defaultContinueParentExecution, subProcessesToCreate, movingExecutionIds, processInstanceChangeState, commandContext); processInstanceChangeState.addCreatedEmbeddedSubProcess(subProcess.getId(), embeddedSubProcess); } @@ -820,7 +836,9 @@ protected List createEmbeddedSubProcessAndExecutions(Collection for (FlowElementMoveEntry flowElementMoveEntry : moveToFlowElements) { FlowElement newFlowElement = flowElementMoveEntry.getNewFlowElement(); ExecutionEntity parentExecution; - if (newFlowElement.getSubProcess() != null && processInstanceChangeState.getCreatedEmbeddedSubProcesses().containsKey(newFlowElement.getSubProcess().getId())) { + if (newFlowElement.getSubProcess() != null && moveExecutionEntityContainer.getCreatedEventSubProcess(newFlowElement.getSubProcess().getId()) != null) { + parentExecution = moveExecutionEntityContainer.getCreatedEventSubProcess(newFlowElement.getSubProcess().getId()); + } else if (newFlowElement.getSubProcess() != null && processInstanceChangeState.getCreatedEmbeddedSubProcesses().containsKey(newFlowElement.getSubProcess().getId())) { parentExecution = processInstanceChangeState.getCreatedEmbeddedSubProcesses().get(newFlowElement.getSubProcess().getId()); } else if ((newFlowElement instanceof Task || newFlowElement instanceof CallActivity) && isFlowElementMultiInstance(newFlowElement) && !movingExecutions.get(0).isMultiInstanceRoot() && diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/dynamic/MoveExecutionEntityContainer.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/dynamic/MoveExecutionEntityContainer.java index 6abb2b08869..66e0eb97aaa 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/dynamic/MoveExecutionEntityContainer.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/dynamic/MoveExecutionEntityContainer.java @@ -46,6 +46,7 @@ public class MoveExecutionEntityContainer { protected Map currentActivityToNewElementMap = new LinkedHashMap<>(); protected Map> flowElementLocalVariableMap = new HashMap<>(); protected List newExecutionIds = new ArrayList<>(); + protected Map createdEventSubProcesses = new HashMap<>(); public MoveExecutionEntityContainer(List executions, List moveToActivityIds) { this.executions = executions; @@ -216,6 +217,14 @@ public void addNewExecutionId(String executionId) { this.newExecutionIds.add(executionId); } + public ExecutionEntity getCreatedEventSubProcess(String processDefinitionId) { + return createdEventSubProcesses.get(processDefinitionId); + } + + public void addCreatedEventSubProcess(String processDefinitionId, ExecutionEntity executionEntity) { + createdEventSubProcesses.put(processDefinitionId, executionEntity); + } + public Map> getFlowElementLocalVariableMap() { return flowElementLocalVariableMap; } diff --git a/modules/flowable-engine/src/test/java/org/flowable/engine/test/api/runtime/migration/AbstractProcessInstanceMigrationEventRegistryConsumerTest.java b/modules/flowable-engine/src/test/java/org/flowable/engine/test/api/runtime/migration/AbstractProcessInstanceMigrationEventRegistryConsumerTest.java new file mode 100644 index 00000000000..14ab5a5de91 --- /dev/null +++ b/modules/flowable-engine/src/test/java/org/flowable/engine/test/api/runtime/migration/AbstractProcessInstanceMigrationEventRegistryConsumerTest.java @@ -0,0 +1,122 @@ +/* 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.engine.test.api.runtime.migration; + +import java.util.List; +import java.util.Map; + +import org.flowable.common.engine.impl.interceptor.EngineConfigurationConstants; +import org.flowable.eventregistry.api.EventDeployment; +import org.flowable.eventregistry.api.EventRegistry; +import org.flowable.eventregistry.api.EventRepositoryService; +import org.flowable.eventregistry.api.InboundEventChannelAdapter; +import org.flowable.eventregistry.impl.EventRegistryEngineConfiguration; +import org.flowable.eventregistry.model.InboundChannelModel; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; + +/** + * Provides a test channel and test events. + * + * @author Bas Claessen + */ +public abstract class AbstractProcessInstanceMigrationEventRegistryConsumerTest extends AbstractProcessInstanceMigrationTest { + + protected TestInboundEventChannelAdapter inboundEventChannelAdapter; + + @BeforeEach + public void setUp() throws Exception { + inboundEventChannelAdapter = setupTestChannel(); + + getEventRepositoryService().createEventModelBuilder() + .key("myEvent") + .resourceName("myEvent.event") + .deploy(); + } + + @AfterEach + public void tearDown() throws Exception { + EventRepositoryService eventRepositoryService = getEventRepositoryService(); + List deployments = eventRepositoryService.createDeploymentQuery().list(); + for (EventDeployment eventDeployment : deployments) { + eventRepositoryService.deleteDeployment(eventDeployment.getId()); + } + deleteDeployments(); + } + + protected TestInboundEventChannelAdapter setupTestChannel() { + TestInboundEventChannelAdapter inboundEventChannelAdapter = new TestInboundEventChannelAdapter(); + Map beans = getEventRegistryEngineConfiguration().getExpressionManager().getBeans(); + beans.put("inboundEventChannelAdapter", inboundEventChannelAdapter); + + getEventRepositoryService().createInboundChannelModelBuilder() + .key("test-channel") + .resourceName("testChannel.channel") + .channelAdapter("${inboundEventChannelAdapter}") + .jsonDeserializer() + .detectEventKeyUsingJsonField("type") + .jsonFieldsMapDirectlyToPayload() + .deploy(); + + return inboundEventChannelAdapter; + } + + protected EventRepositoryService getEventRepositoryService() { + return getEventRegistryEngineConfiguration().getEventRepositoryService(); + } + + protected EventRegistryEngineConfiguration getEventRegistryEngineConfiguration() { + return (EventRegistryEngineConfiguration) processEngineConfiguration.getEngineConfigurations() + .get(EngineConfigurationConstants.KEY_EVENT_REGISTRY_CONFIG); + } + + protected static class TestInboundEventChannelAdapter implements InboundEventChannelAdapter { + + public InboundChannelModel inboundChannelModel; + public EventRegistry eventRegistry; + protected ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public void setInboundChannelModel(InboundChannelModel inboundChannelModel) { + this.inboundChannelModel = inboundChannelModel; + } + + @Override + public void setEventRegistry(EventRegistry eventRegistry) { + this.eventRegistry = eventRegistry; + } + + public void triggerTestEvent() { + ObjectNode eventNode = createTestEventNode(); + triggerTestEvent(eventNode); + } + + public void triggerTestEvent(ObjectNode eventNode) { + try { + eventRegistry.eventReceived(inboundChannelModel, objectMapper.writeValueAsString(eventNode)); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + protected ObjectNode createTestEventNode() { + ObjectNode json = objectMapper.createObjectNode(); + json.put("type", "myEvent"); + return json; + } + } +} diff --git a/modules/flowable-engine/src/test/java/org/flowable/engine/test/api/runtime/migration/ProcessInstanceMigrationEventRegistryEventSubprocessTest.java b/modules/flowable-engine/src/test/java/org/flowable/engine/test/api/runtime/migration/ProcessInstanceMigrationEventRegistryEventSubprocessTest.java new file mode 100644 index 00000000000..4ffcdb27974 --- /dev/null +++ b/modules/flowable-engine/src/test/java/org/flowable/engine/test/api/runtime/migration/ProcessInstanceMigrationEventRegistryEventSubprocessTest.java @@ -0,0 +1,253 @@ +/* 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.engine.test.api.runtime.migration; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.assertj.core.groups.Tuple; +import org.flowable.engine.migration.ProcessInstanceMigrationBuilder; +import org.flowable.engine.migration.ProcessInstanceMigrationValidationResult; +import org.flowable.engine.repository.ProcessDefinition; +import org.flowable.engine.runtime.Execution; +import org.flowable.engine.runtime.ProcessInstance; +import org.flowable.eventsubscription.api.EventSubscription; +import org.flowable.eventsubscription.service.impl.EventSubscriptionQueryImpl; +import org.flowable.task.api.Task; +import org.junit.jupiter.api.Test; + +/** + * @author Bas Claessen + */ +public class ProcessInstanceMigrationEventRegistryEventSubprocessTest extends AbstractProcessInstanceMigrationEventRegistryConsumerTest { + + @Test + public void testMigrateNonInterruptingEventRegistryEventSubProcess() { + //Deploy first version of the process + ProcessDefinition version1ProcessDef = deployProcessDefinition("my deploy", + "org/flowable/engine/test/api/runtime/migration/non-interrupting-eventregistry-event-subprocess.bpmn20.xml"); + + //Start an instance of the first version of the process for migration + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(version1ProcessDef.getKey()); + + //Deploy second version of the same process + ProcessDefinition version2ProcessDef = deployProcessDefinition("my deploy", + "org/flowable/engine/test/api/runtime/migration/non-interrupting-eventregistry-event-subprocess.bpmn20.xml"); + assertThat(version1ProcessDef.getId()).isNotEqualTo(version2ProcessDef.getId()); + + //Confirm the state to migrate + List executions = runtimeService.createExecutionQuery().processInstanceId(processInstance.getId()).onlyChildExecutions().list(); + assertThat(executions) + .extracting(Execution::getActivityId) + .containsExactlyInAnyOrder("processTask", "eventSubProcessStart"); + assertThat(executions) + .extracting("processDefinitionId") + .containsOnly(version1ProcessDef.getId()); + List tasks = taskService.createTaskQuery().processInstanceId(processInstance.getId()).list(); + assertThat(tasks) + .extracting(Task::getTaskDefinitionKey, Task::getProcessDefinitionId) + .containsExactlyInAnyOrder(Tuple.tuple("processTask", version1ProcessDef.getId())); + List eventSubscriptions = runtimeService.createEventSubscriptionQuery().processInstanceId(processInstance.getId()).list(); + assertThat(eventSubscriptions) + .extracting(EventSubscription::getEventType, EventSubscription::getActivityId, EventSubscription::getProcessDefinitionId) + .containsExactlyInAnyOrder(Tuple.tuple("myEvent", "eventSubProcessStart", version1ProcessDef.getId())); + + //Migrate to the other processDefinition + ProcessInstanceMigrationBuilder processInstanceMigrationBuilder = processMigrationService.createProcessInstanceMigrationBuilder() + .migrateToProcessDefinition(version2ProcessDef.getId()); + + ProcessInstanceMigrationValidationResult processInstanceMigrationValidationResult = processInstanceMigrationBuilder + .validateMigration(processInstance.getId()); + assertThat(processInstanceMigrationValidationResult.isMigrationValid()).isTrue(); + + processInstanceMigrationBuilder.migrate(processInstance.getId()); + + //Confirm + executions = runtimeService.createExecutionQuery().processInstanceId(processInstance.getId()).onlyChildExecutions().list(); + assertThat(executions) + .extracting(Execution::getActivityId) + .containsExactlyInAnyOrder("processTask", "eventSubProcessStart"); + assertThat(executions) + .extracting("processDefinitionId") + .containsOnly(version2ProcessDef.getId()); + tasks = taskService.createTaskQuery().processInstanceId(processInstance.getId()).list(); + assertThat(tasks) + .extracting(Task::getTaskDefinitionKey, Task::getProcessDefinitionId) + .containsExactlyInAnyOrder(Tuple.tuple("processTask", version2ProcessDef.getId())); + + eventSubscriptions = runtimeService.createEventSubscriptionQuery().processInstanceId(processInstance.getId()).list(); + assertThat(eventSubscriptions) + .extracting(EventSubscription::getEventType, EventSubscription::getActivityId, EventSubscription::getProcessDefinitionId) + .containsExactlyInAnyOrder(Tuple.tuple("myEvent", "eventSubProcessStart", version2ProcessDef.getId())); + + completeProcessInstanceTasks(processInstance.getId()); + assertProcessEnded(processInstance.getId()); + } + + @Test + public void testMigrateNonInterruptingEventRegistryEventSubProcessWithStartedSubProcess() { + //Deploy first version of the process + ProcessDefinition version1ProcessDef = deployProcessDefinition("my deploy", + "org/flowable/engine/test/api/runtime/migration/non-interrupting-eventregistry-event-subprocess.bpmn20.xml"); + + //Start an instance of the first version of the process for migration + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(version1ProcessDef.getKey()); + + //Deploy second version of the same process + ProcessDefinition version2ProcessDef = deployProcessDefinition("my deploy", + "org/flowable/engine/test/api/runtime/migration/non-interrupting-eventregistry-event-subprocess.bpmn20.xml"); + assertThat(version1ProcessDef.getId()).isNotEqualTo(version2ProcessDef.getId()); + + //Trigger event + inboundEventChannelAdapter.triggerTestEvent(); + + //Confirm the state to migrate + List executions = runtimeService.createExecutionQuery().processInstanceId(processInstance.getId()).onlyChildExecutions().list(); + assertThat(executions) + .extracting(Execution::getActivityId) + .containsExactlyInAnyOrder("processTask", "eventSubProcessStart", "eventSubProcess", "eventSubProcessTask"); + assertThat(executions) + .extracting("processDefinitionId") + .containsOnly(version1ProcessDef.getId()); + List tasks = taskService.createTaskQuery().processInstanceId(processInstance.getId()).list(); + assertThat(tasks) + .extracting(Task::getTaskDefinitionKey, Task::getProcessDefinitionId) + .containsExactlyInAnyOrder( + Tuple.tuple("processTask", version1ProcessDef.getId()), + Tuple.tuple("eventSubProcessTask", version1ProcessDef.getId()) + ); + List eventSubscriptions = runtimeService.createEventSubscriptionQuery().processInstanceId(processInstance.getId()).list(); + assertThat(eventSubscriptions) + .extracting(EventSubscription::getEventType, EventSubscription::getActivityId, EventSubscription::getProcessDefinitionId) + .containsExactlyInAnyOrder(Tuple.tuple("myEvent", "eventSubProcessStart", version1ProcessDef.getId())); + + //Migrate to the other processDefinition + ProcessInstanceMigrationBuilder processInstanceMigrationBuilder = processMigrationService.createProcessInstanceMigrationBuilder() + .migrateToProcessDefinition(version2ProcessDef.getId()); + + ProcessInstanceMigrationValidationResult processInstanceMigrationValidationResult = processInstanceMigrationBuilder + .validateMigration(processInstance.getId()); + assertThat(processInstanceMigrationValidationResult.isMigrationValid()).isTrue(); + + processInstanceMigrationBuilder.migrate(processInstance.getId()); + + //Confirm + executions = runtimeService.createExecutionQuery().processInstanceId(processInstance.getId()).onlyChildExecutions().list(); + assertThat(executions) + .extracting(Execution::getActivityId) + .containsExactlyInAnyOrder("processTask", "eventSubProcessStart", "eventSubProcess", "eventSubProcessTask"); + assertThat(executions) + .extracting("processDefinitionId") + .containsOnly(version2ProcessDef.getId()); + tasks = taskService.createTaskQuery().processInstanceId(processInstance.getId()).list(); + assertThat(tasks) + .extracting(Task::getTaskDefinitionKey, Task::getProcessDefinitionId) + .containsExactlyInAnyOrder( + Tuple.tuple("processTask", version2ProcessDef.getId()), + Tuple.tuple("eventSubProcessTask", version2ProcessDef.getId()) + ); + + eventSubscriptions = runtimeService.createEventSubscriptionQuery().processInstanceId(processInstance.getId()).list(); + assertThat(eventSubscriptions) + .extracting(EventSubscription::getEventType, EventSubscription::getActivityId, EventSubscription::getProcessDefinitionId) + .containsExactlyInAnyOrder(Tuple.tuple("myEvent", "eventSubProcessStart", version2ProcessDef.getId())); + + completeProcessInstanceTasks(processInstance.getId()); + assertProcessEnded(processInstance.getId()); + } + + @Test + public void testMigrateNonInterruptingEventRegistryEventSubProcessWithTwoStartedSubProcess() { + //Deploy first version of the process + ProcessDefinition version1ProcessDef = deployProcessDefinition("my deploy", + "org/flowable/engine/test/api/runtime/migration/non-interrupting-eventregistry-event-subprocess.bpmn20.xml"); + + //Start an instance of the first version of the process for migration + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(version1ProcessDef.getKey()); + + //Deploy second version of the same process + ProcessDefinition version2ProcessDef = deployProcessDefinition("my deploy", + "org/flowable/engine/test/api/runtime/migration/non-interrupting-eventregistry-event-subprocess.bpmn20.xml"); + assertThat(version1ProcessDef.getId()).isNotEqualTo(version2ProcessDef.getId()); + + //Trigger event to create first sub process + inboundEventChannelAdapter.triggerTestEvent(); + //Trigger event to create second sub process + inboundEventChannelAdapter.triggerTestEvent(); + + //Confirm the state to migrate + List executions = runtimeService.createExecutionQuery().processInstanceId(processInstance.getId()).onlyChildExecutions().list(); + assertThat(executions) + .extracting(Execution::getActivityId) + .containsExactlyInAnyOrder( + "processTask", "eventSubProcessStart", "eventSubProcess", "eventSubProcessTask", "eventSubProcess", "eventSubProcessTask" + ); + assertThat(executions) + .extracting("processDefinitionId") + .containsOnly(version1ProcessDef.getId()); + List tasks = taskService.createTaskQuery().processInstanceId(processInstance.getId()).list(); + assertThat(tasks) + .extracting(Task::getTaskDefinitionKey, Task::getProcessDefinitionId) + .containsExactlyInAnyOrder( + Tuple.tuple("processTask", version1ProcessDef.getId()), + Tuple.tuple("eventSubProcessTask", version1ProcessDef.getId()), + Tuple.tuple("eventSubProcessTask", version1ProcessDef.getId()) + ); + List eventSubscriptions = runtimeService.createEventSubscriptionQuery().processInstanceId(processInstance.getId()).list(); + assertThat(eventSubscriptions) + .extracting(EventSubscription::getEventType, EventSubscription::getActivityId, EventSubscription::getProcessDefinitionId) + .containsExactlyInAnyOrder(Tuple.tuple("myEvent", "eventSubProcessStart", version1ProcessDef.getId())); + + //Migrate to the other processDefinition + ProcessInstanceMigrationBuilder processInstanceMigrationBuilder = processMigrationService.createProcessInstanceMigrationBuilder() + .migrateToProcessDefinition(version2ProcessDef.getId()); + + ProcessInstanceMigrationValidationResult processInstanceMigrationValidationResult = processInstanceMigrationBuilder + .validateMigration(processInstance.getId()); + assertThat(processInstanceMigrationValidationResult.isMigrationValid()).isTrue(); + + processInstanceMigrationBuilder.migrate(processInstance.getId()); + + //Confirm + executions = runtimeService.createExecutionQuery().processInstanceId(processInstance.getId()).onlyChildExecutions().list(); + assertThat(executions) + .extracting(Execution::getActivityId) + .containsExactlyInAnyOrder( + "processTask", "eventSubProcessStart", "eventSubProcess", "eventSubProcessTask", "eventSubProcess", "eventSubProcessTask" + ); + assertThat(executions) + .extracting("processDefinitionId") + .containsOnly(version2ProcessDef.getId()); + tasks = taskService.createTaskQuery().processInstanceId(processInstance.getId()).list(); + assertThat(tasks) + .extracting(Task::getTaskDefinitionKey, Task::getProcessDefinitionId) + .containsExactlyInAnyOrder( + Tuple.tuple("processTask", version2ProcessDef.getId()), + Tuple.tuple("eventSubProcessTask", version2ProcessDef.getId()), + Tuple.tuple("eventSubProcessTask", version2ProcessDef.getId()) + ); + + eventSubscriptions = runtimeService.createEventSubscriptionQuery().processInstanceId(processInstance.getId()).list(); + assertThat(eventSubscriptions) + .extracting(EventSubscription::getEventType, EventSubscription::getActivityId, EventSubscription::getProcessDefinitionId) + .containsExactlyInAnyOrder(Tuple.tuple("myEvent", "eventSubProcessStart", version2ProcessDef.getId())); + + completeProcessInstanceTasks(processInstance.getId()); + assertProcessEnded(processInstance.getId()); + } + + private EventSubscriptionQueryImpl createEventSubscriptionQuery() { + return new EventSubscriptionQueryImpl(processEngineConfiguration.getCommandExecutor(), processEngineConfiguration.getEventSubscriptionServiceConfiguration()); + } +} diff --git a/modules/flowable-engine/src/test/java/org/flowable/engine/test/api/runtime/migration/ProcessInstanceMigrationEventSubProcessTest.java b/modules/flowable-engine/src/test/java/org/flowable/engine/test/api/runtime/migration/ProcessInstanceMigrationEventSubProcessTest.java index 27e415c097a..e9eb62e6800 100644 --- a/modules/flowable-engine/src/test/java/org/flowable/engine/test/api/runtime/migration/ProcessInstanceMigrationEventSubProcessTest.java +++ b/modules/flowable-engine/src/test/java/org/flowable/engine/test/api/runtime/migration/ProcessInstanceMigrationEventSubProcessTest.java @@ -19,6 +19,7 @@ import java.util.Arrays; import java.util.List; +import org.assertj.core.groups.Tuple; import org.flowable.common.engine.impl.history.HistoryLevel; import org.flowable.engine.history.HistoricActivityInstance; import org.flowable.engine.impl.test.HistoryTestHelper; @@ -267,6 +268,228 @@ public void testMigrateSimpleActivityToActivityInsideTimerEventSubProcessInNewDe checkTaskInstance(procWithSignal, processInstance, "eventSubProcessTask"); } + completeProcessInstanceTasks(processInstance.getId()); + completeProcessInstanceTasks(processInstance.getId()); + assertProcessEnded(processInstance.getId()); + } + + @Test + public void testMigrateNonInterruptingSignalEventSubProcess() { + //Deploy first version of the process + ProcessDefinition version1ProcessDef = deployProcessDefinition("my deploy", + "org/flowable/engine/test/api/runtime/migration/non-interrupting-signal-event-subprocess.bpmn20.xml"); + + //Start an instance of the first version of the process for migration + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(version1ProcessDef.getKey()); + + //Deploy second version of the same process + ProcessDefinition version2ProcessDef = deployProcessDefinition("my deploy", + "org/flowable/engine/test/api/runtime/migration/non-interrupting-signal-event-subprocess.bpmn20.xml"); + assertThat(version1ProcessDef.getId()).isNotEqualTo(version2ProcessDef.getId()); + + //Confirm the state to migrate + List executions = runtimeService.createExecutionQuery().processInstanceId(processInstance.getId()).onlyChildExecutions().list(); + assertThat(executions) + .extracting(Execution::getActivityId) + .containsExactlyInAnyOrder("processTask", "eventSubProcessStart"); + assertThat(executions) + .extracting("processDefinitionId") + .containsOnly(version1ProcessDef.getId()); + List tasks = taskService.createTaskQuery().processInstanceId(processInstance.getId()).list(); + assertThat(tasks) + .extracting(Task::getTaskDefinitionKey, Task::getProcessDefinitionId) + .containsExactlyInAnyOrder(Tuple.tuple("processTask", version1ProcessDef.getId())); + List eventSubscriptions = runtimeService.createEventSubscriptionQuery().processInstanceId(processInstance.getId()).list(); + assertThat(eventSubscriptions) + .extracting(EventSubscription::getEventName, EventSubscription::getActivityId, EventSubscription::getProcessDefinitionId) + .containsExactlyInAnyOrder(Tuple.tuple("eventSignal", "eventSubProcessStart", version1ProcessDef.getId())); + + changeStateEventListener.clear(); + + //Migrate to the other processDefinition + ProcessInstanceMigrationBuilder processInstanceMigrationBuilder = processMigrationService.createProcessInstanceMigrationBuilder() + .migrateToProcessDefinition(version2ProcessDef.getId()); + + ProcessInstanceMigrationValidationResult processInstanceMigrationValidationResult = processInstanceMigrationBuilder + .validateMigration(processInstance.getId()); + assertThat(processInstanceMigrationValidationResult.isMigrationValid()).isTrue(); + + processInstanceMigrationBuilder.migrate(processInstance.getId()); + + //Confirm + executions = runtimeService.createExecutionQuery().processInstanceId(processInstance.getId()).onlyChildExecutions().list(); + assertThat(executions) + .extracting(Execution::getActivityId) + .containsExactlyInAnyOrder("processTask", "eventSubProcessStart"); + assertThat(executions) + .extracting("processDefinitionId") + .containsOnly(version2ProcessDef.getId()); + tasks = taskService.createTaskQuery().processInstanceId(processInstance.getId()).list(); + assertThat(tasks) + .extracting(Task::getTaskDefinitionKey, Task::getProcessDefinitionId) + .containsExactlyInAnyOrder(Tuple.tuple("processTask", version2ProcessDef.getId())); + + eventSubscriptions = runtimeService.createEventSubscriptionQuery().processInstanceId(processInstance.getId()).list(); + assertThat(eventSubscriptions) + .extracting(EventSubscription::getEventName, EventSubscription::getActivityId, EventSubscription::getProcessDefinitionId) + .containsExactlyInAnyOrder(Tuple.tuple("eventSignal", "eventSubProcessStart", version2ProcessDef.getId())); + + completeProcessInstanceTasks(processInstance.getId()); + assertProcessEnded(processInstance.getId()); + } + + @Test + public void testMigrateNonInterruptingSignalEventSubProcessWithStartedSubProcess() { + //Deploy first version of the process + ProcessDefinition version1ProcessDef = deployProcessDefinition("my deploy", + "org/flowable/engine/test/api/runtime/migration/non-interrupting-signal-event-subprocess.bpmn20.xml"); + + //Start an instance of the first version of the process for migration + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(version1ProcessDef.getKey()); + + //Deploy second version of the same process + ProcessDefinition version2ProcessDef = deployProcessDefinition("my deploy", + "org/flowable/engine/test/api/runtime/migration/non-interrupting-signal-event-subprocess.bpmn20.xml"); + assertThat(version1ProcessDef.getId()).isNotEqualTo(version2ProcessDef.getId()); + + //Fire the signal + runtimeService.signalEventReceived("eventSignal"); + + //Confirm the state to migrate + List executions = runtimeService.createExecutionQuery().processInstanceId(processInstance.getId()).onlyChildExecutions().list(); + assertThat(executions) + .extracting(Execution::getActivityId) + .containsExactlyInAnyOrder("processTask", "eventSubProcessStart", "eventSubProcess", "eventSubProcessTask"); + assertThat(executions) + .extracting("processDefinitionId") + .containsOnly(version1ProcessDef.getId()); + List tasks = taskService.createTaskQuery().processInstanceId(processInstance.getId()).list(); + assertThat(tasks) + .extracting(Task::getTaskDefinitionKey, Task::getProcessDefinitionId) + .containsExactlyInAnyOrder( + Tuple.tuple("processTask", version1ProcessDef.getId()), + Tuple.tuple("eventSubProcessTask", version1ProcessDef.getId()) + ); + List eventSubscriptions = runtimeService.createEventSubscriptionQuery().processInstanceId(processInstance.getId()).list(); + assertThat(eventSubscriptions) + .extracting(EventSubscription::getEventName, EventSubscription::getActivityId, EventSubscription::getProcessDefinitionId) + .containsExactlyInAnyOrder(Tuple.tuple("eventSignal", "eventSubProcessStart", version1ProcessDef.getId())); + + changeStateEventListener.clear(); + + //Migrate to the other processDefinition + ProcessInstanceMigrationBuilder processInstanceMigrationBuilder = processMigrationService.createProcessInstanceMigrationBuilder() + .migrateToProcessDefinition(version2ProcessDef.getId()); + + ProcessInstanceMigrationValidationResult processInstanceMigrationValidationResult = processInstanceMigrationBuilder + .validateMigration(processInstance.getId()); + assertThat(processInstanceMigrationValidationResult.isMigrationValid()).isTrue(); + + processInstanceMigrationBuilder.migrate(processInstance.getId()); + + //Confirm + executions = runtimeService.createExecutionQuery().processInstanceId(processInstance.getId()).onlyChildExecutions().list(); + assertThat(executions) + .extracting(Execution::getActivityId) + .containsExactlyInAnyOrder("processTask", "eventSubProcessStart", "eventSubProcess", "eventSubProcessTask"); + assertThat(executions) + .extracting("processDefinitionId") + .containsOnly(version2ProcessDef.getId()); + tasks = taskService.createTaskQuery().processInstanceId(processInstance.getId()).list(); + assertThat(tasks) + .extracting(Task::getTaskDefinitionKey, Task::getProcessDefinitionId) + .containsExactlyInAnyOrder( + Tuple.tuple("processTask", version2ProcessDef.getId()), + Tuple.tuple("eventSubProcessTask", version2ProcessDef.getId()) + ); + + eventSubscriptions = runtimeService.createEventSubscriptionQuery().processInstanceId(processInstance.getId()).list(); + assertThat(eventSubscriptions) + .extracting(EventSubscription::getEventName, EventSubscription::getActivityId, EventSubscription::getProcessDefinitionId) + .containsExactlyInAnyOrder(Tuple.tuple("eventSignal", "eventSubProcessStart", version2ProcessDef.getId())); + + completeProcessInstanceTasks(processInstance.getId()); + assertProcessEnded(processInstance.getId()); + } + + @Test + public void testMigrateNonInterruptingSignalEventSubProcessWithTwoStartedSubProcess() { + //Deploy first version of the process + ProcessDefinition version1ProcessDef = deployProcessDefinition("my deploy", + "org/flowable/engine/test/api/runtime/migration/non-interrupting-signal-event-subprocess.bpmn20.xml"); + + //Start an instance of the first version of the process for migration + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(version1ProcessDef.getKey()); + + //Deploy second version of the same process + ProcessDefinition version2ProcessDef = deployProcessDefinition("my deploy", + "org/flowable/engine/test/api/runtime/migration/non-interrupting-signal-event-subprocess.bpmn20.xml"); + assertThat(version1ProcessDef.getId()).isNotEqualTo(version2ProcessDef.getId()); + + //Fire the signal to start the first sub process + runtimeService.signalEventReceived("eventSignal"); + //Fire the signal to start the second sub process + runtimeService.signalEventReceived("eventSignal"); + + //Confirm the state to migrate + List executions = runtimeService.createExecutionQuery().processInstanceId(processInstance.getId()).onlyChildExecutions().list(); + assertThat(executions) + .extracting(Execution::getActivityId) + .containsExactlyInAnyOrder( + "processTask", "eventSubProcessStart", "eventSubProcess", "eventSubProcessTask", "eventSubProcess", "eventSubProcessTask" + ); + assertThat(executions) + .extracting("processDefinitionId") + .containsOnly(version1ProcessDef.getId()); + List tasks = taskService.createTaskQuery().processInstanceId(processInstance.getId()).list(); + assertThat(tasks) + .extracting(Task::getTaskDefinitionKey, Task::getProcessDefinitionId) + .containsExactlyInAnyOrder( + Tuple.tuple("processTask", version1ProcessDef.getId()), + Tuple.tuple("eventSubProcessTask", version1ProcessDef.getId()), + Tuple.tuple("eventSubProcessTask", version1ProcessDef.getId()) + ); + List eventSubscriptions = runtimeService.createEventSubscriptionQuery().processInstanceId(processInstance.getId()).list(); + assertThat(eventSubscriptions) + .extracting(EventSubscription::getEventName, EventSubscription::getActivityId, EventSubscription::getProcessDefinitionId) + .containsExactlyInAnyOrder(Tuple.tuple("eventSignal", "eventSubProcessStart", version1ProcessDef.getId())); + + changeStateEventListener.clear(); + + //Migrate to the other processDefinition + ProcessInstanceMigrationBuilder processInstanceMigrationBuilder = processMigrationService.createProcessInstanceMigrationBuilder() + .migrateToProcessDefinition(version2ProcessDef.getId()); + + ProcessInstanceMigrationValidationResult processInstanceMigrationValidationResult = processInstanceMigrationBuilder + .validateMigration(processInstance.getId()); + assertThat(processInstanceMigrationValidationResult.isMigrationValid()).isTrue(); + + processInstanceMigrationBuilder.migrate(processInstance.getId()); + + //Confirm + executions = runtimeService.createExecutionQuery().processInstanceId(processInstance.getId()).onlyChildExecutions().list(); + assertThat(executions) + .extracting(Execution::getActivityId) + .containsExactlyInAnyOrder( + "processTask", "eventSubProcessStart", "eventSubProcess", "eventSubProcessTask", "eventSubProcess", "eventSubProcessTask" + ); + assertThat(executions) + .extracting("processDefinitionId") + .containsOnly(version2ProcessDef.getId()); + tasks = taskService.createTaskQuery().processInstanceId(processInstance.getId()).list(); + assertThat(tasks) + .extracting(Task::getTaskDefinitionKey, Task::getProcessDefinitionId) + .containsExactlyInAnyOrder( + Tuple.tuple("processTask", version2ProcessDef.getId()), + Tuple.tuple("eventSubProcessTask", version2ProcessDef.getId()), + Tuple.tuple("eventSubProcessTask", version2ProcessDef.getId()) + ); + + eventSubscriptions = runtimeService.createEventSubscriptionQuery().processInstanceId(processInstance.getId()).list(); + assertThat(eventSubscriptions) + .extracting(EventSubscription::getEventName, EventSubscription::getActivityId, EventSubscription::getProcessDefinitionId) + .containsExactlyInAnyOrder(Tuple.tuple("eventSignal", "eventSubProcessStart", version2ProcessDef.getId())); + completeProcessInstanceTasks(processInstance.getId()); assertProcessEnded(processInstance.getId()); } @@ -338,6 +561,231 @@ public void testMigrateSimpleActivityToActivityInsideNonInterruptingSignalEventS assertProcessEnded(processInstance.getId()); } + @Test + public void testMigrateNonInterruptingMessageEventSubProcess() { + //Deploy first version of the process + ProcessDefinition version1ProcessDef = deployProcessDefinition("my deploy", + "org/flowable/engine/test/api/runtime/migration/non-interrupting-message-event-subprocess.bpmn20.xml"); + + //Start an instance of the first version of the process for migration + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(version1ProcessDef.getKey()); + + //Deploy second version of the same process + ProcessDefinition version2ProcessDef = deployProcessDefinition("my deploy", + "org/flowable/engine/test/api/runtime/migration/non-interrupting-message-event-subprocess.bpmn20.xml"); + assertThat(version1ProcessDef.getId()).isNotEqualTo(version2ProcessDef.getId()); + + //Confirm the state to migrate + List executions = runtimeService.createExecutionQuery().processInstanceId(processInstance.getId()).onlyChildExecutions().list(); + assertThat(executions) + .extracting(Execution::getActivityId) + .containsExactlyInAnyOrder("processTask", "eventSubProcessStart"); + assertThat(executions) + .extracting("processDefinitionId") + .containsOnly(version1ProcessDef.getId()); + List tasks = taskService.createTaskQuery().processInstanceId(processInstance.getId()).list(); + assertThat(tasks) + .extracting(Task::getTaskDefinitionKey, Task::getProcessDefinitionId) + .containsExactlyInAnyOrder(Tuple.tuple("processTask", version1ProcessDef.getId())); + List eventSubscriptions = runtimeService.createEventSubscriptionQuery().processInstanceId(processInstance.getId()).list(); + assertThat(eventSubscriptions) + .extracting(EventSubscription::getEventName, EventSubscription::getActivityId, EventSubscription::getProcessDefinitionId) + .containsExactlyInAnyOrder(Tuple.tuple("someMessage", "eventSubProcessStart", version1ProcessDef.getId())); + + changeStateEventListener.clear(); + + //Migrate to the other processDefinition + ProcessInstanceMigrationBuilder processInstanceMigrationBuilder = processMigrationService.createProcessInstanceMigrationBuilder() + .migrateToProcessDefinition(version2ProcessDef.getId()); + + ProcessInstanceMigrationValidationResult processInstanceMigrationValidationResult = processInstanceMigrationBuilder + .validateMigration(processInstance.getId()); + assertThat(processInstanceMigrationValidationResult.isMigrationValid()).isTrue(); + + processInstanceMigrationBuilder.migrate(processInstance.getId()); + + //Confirm + executions = runtimeService.createExecutionQuery().processInstanceId(processInstance.getId()).onlyChildExecutions().list(); + assertThat(executions) + .extracting(Execution::getActivityId) + .containsExactlyInAnyOrder("processTask", "eventSubProcessStart"); + assertThat(executions) + .extracting("processDefinitionId") + .containsOnly(version2ProcessDef.getId()); + tasks = taskService.createTaskQuery().processInstanceId(processInstance.getId()).list(); + assertThat(tasks) + .extracting(Task::getTaskDefinitionKey, Task::getProcessDefinitionId) + .containsExactlyInAnyOrder(Tuple.tuple("processTask", version2ProcessDef.getId())); + + eventSubscriptions = runtimeService.createEventSubscriptionQuery().processInstanceId(processInstance.getId()).list(); + assertThat(eventSubscriptions) + .extracting(EventSubscription::getEventName, EventSubscription::getActivityId, EventSubscription::getProcessDefinitionId) + .containsExactlyInAnyOrder(Tuple.tuple("someMessage", "eventSubProcessStart", version2ProcessDef.getId())); + + completeProcessInstanceTasks(processInstance.getId()); + assertProcessEnded(processInstance.getId()); + } + + @Test + public void testMigrateNonInterruptingMessageEventSubProcessWithStartedSubProcess() { + //Deploy first version of the process + ProcessDefinition version1ProcessDef = deployProcessDefinition("my deploy", + "org/flowable/engine/test/api/runtime/migration/non-interrupting-message-event-subprocess.bpmn20.xml"); + + //Start an instance of the first version of the process for migration + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(version1ProcessDef.getKey()); + + //Deploy second version of the same process + ProcessDefinition version2ProcessDef = deployProcessDefinition("my deploy", + "org/flowable/engine/test/api/runtime/migration/non-interrupting-message-event-subprocess.bpmn20.xml"); + assertThat(version1ProcessDef.getId()).isNotEqualTo(version2ProcessDef.getId()); + + //Trigger the event + Execution messageSubscriptionExecution = runtimeService.createExecutionQuery().processInstanceId(processInstance.getId()) + .messageEventSubscriptionName("someMessage").singleResult(); + runtimeService.messageEventReceived("someMessage", messageSubscriptionExecution.getId()); + + //Confirm the state to migrate + List executions = runtimeService.createExecutionQuery().processInstanceId(processInstance.getId()).onlyChildExecutions().list(); + assertThat(executions) + .extracting(Execution::getActivityId) + .containsExactlyInAnyOrder("processTask", "eventSubProcessStart", "eventSubProcess", "eventSubProcessTask"); + assertThat(executions) + .extracting("processDefinitionId") + .containsOnly(version1ProcessDef.getId()); + List tasks = taskService.createTaskQuery().processInstanceId(processInstance.getId()).list(); + assertThat(tasks) + .extracting(Task::getTaskDefinitionKey, Task::getProcessDefinitionId) + .containsExactlyInAnyOrder( + Tuple.tuple("processTask", version1ProcessDef.getId()), + Tuple.tuple("eventSubProcessTask", version1ProcessDef.getId()) + ); + List eventSubscriptions = runtimeService.createEventSubscriptionQuery().processInstanceId(processInstance.getId()).list(); + assertThat(eventSubscriptions) + .extracting(EventSubscription::getEventName, EventSubscription::getActivityId, EventSubscription::getProcessDefinitionId) + .containsExactlyInAnyOrder(Tuple.tuple("someMessage", "eventSubProcessStart", version1ProcessDef.getId())); + + changeStateEventListener.clear(); + + //Migrate to the other processDefinition + ProcessInstanceMigrationBuilder processInstanceMigrationBuilder = processMigrationService.createProcessInstanceMigrationBuilder() + .migrateToProcessDefinition(version2ProcessDef.getId()); + + ProcessInstanceMigrationValidationResult processInstanceMigrationValidationResult = processInstanceMigrationBuilder + .validateMigration(processInstance.getId()); + assertThat(processInstanceMigrationValidationResult.isMigrationValid()).isTrue(); + + processInstanceMigrationBuilder.migrate(processInstance.getId()); + + //Confirm + executions = runtimeService.createExecutionQuery().processInstanceId(processInstance.getId()).onlyChildExecutions().list(); + assertThat(executions) + .extracting(Execution::getActivityId) + .containsExactlyInAnyOrder("processTask", "eventSubProcessStart", "eventSubProcess", "eventSubProcessTask"); + assertThat(executions) + .extracting("processDefinitionId") + .containsOnly(version2ProcessDef.getId()); + tasks = taskService.createTaskQuery().processInstanceId(processInstance.getId()).list(); + assertThat(tasks) + .extracting(Task::getTaskDefinitionKey, Task::getProcessDefinitionId) + .containsExactlyInAnyOrder( + Tuple.tuple("processTask", version2ProcessDef.getId()), + Tuple.tuple("eventSubProcessTask", version2ProcessDef.getId()) + ); + + eventSubscriptions = runtimeService.createEventSubscriptionQuery().processInstanceId(processInstance.getId()).list(); + assertThat(eventSubscriptions) + .extracting(EventSubscription::getEventName, EventSubscription::getActivityId, EventSubscription::getProcessDefinitionId) + .containsExactlyInAnyOrder(Tuple.tuple("someMessage", "eventSubProcessStart", version2ProcessDef.getId())); + + completeProcessInstanceTasks(processInstance.getId()); + assertProcessEnded(processInstance.getId()); + } + + @Test + public void testMigrateNonInterruptingMessageEventSubProcessWithTwoStartedSubProcess() { + //Deploy first version of the process + ProcessDefinition version1ProcessDef = deployProcessDefinition("my deploy", + "org/flowable/engine/test/api/runtime/migration/non-interrupting-message-event-subprocess.bpmn20.xml"); + + //Start an instance of the first version of the process for migration + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(version1ProcessDef.getKey()); + + //Deploy second version of the same process + ProcessDefinition version2ProcessDef = deployProcessDefinition("my deploy", + "org/flowable/engine/test/api/runtime/migration/non-interrupting-message-event-subprocess.bpmn20.xml"); + assertThat(version1ProcessDef.getId()).isNotEqualTo(version2ProcessDef.getId()); + + Execution messageSubscriptionExecution = runtimeService.createExecutionQuery().processInstanceId(processInstance.getId()) + .messageEventSubscriptionName("someMessage").singleResult(); + //Trigger the event to create the first sub process + runtimeService.messageEventReceived("someMessage", messageSubscriptionExecution.getId()); + //Trigger the event to create the second sub process + runtimeService.messageEventReceived("someMessage", messageSubscriptionExecution.getId()); + + //Confirm the state to migrate + List executions = runtimeService.createExecutionQuery().processInstanceId(processInstance.getId()).onlyChildExecutions().list(); + assertThat(executions) + .extracting(Execution::getActivityId) + .containsExactlyInAnyOrder( + "processTask", "eventSubProcessStart", "eventSubProcess", "eventSubProcessTask", "eventSubProcess", "eventSubProcessTask" + ); + assertThat(executions) + .extracting("processDefinitionId") + .containsOnly(version1ProcessDef.getId()); + List tasks = taskService.createTaskQuery().processInstanceId(processInstance.getId()).list(); + assertThat(tasks) + .extracting(Task::getTaskDefinitionKey, Task::getProcessDefinitionId) + .containsExactlyInAnyOrder( + Tuple.tuple("processTask", version1ProcessDef.getId()), + Tuple.tuple("eventSubProcessTask", version1ProcessDef.getId()), + Tuple.tuple("eventSubProcessTask", version1ProcessDef.getId()) + ); + List eventSubscriptions = runtimeService.createEventSubscriptionQuery().processInstanceId(processInstance.getId()).list(); + assertThat(eventSubscriptions) + .extracting(EventSubscription::getEventName, EventSubscription::getActivityId, EventSubscription::getProcessDefinitionId) + .containsExactlyInAnyOrder(Tuple.tuple("someMessage", "eventSubProcessStart", version1ProcessDef.getId())); + + changeStateEventListener.clear(); + + //Migrate to the other processDefinition + ProcessInstanceMigrationBuilder processInstanceMigrationBuilder = processMigrationService.createProcessInstanceMigrationBuilder() + .migrateToProcessDefinition(version2ProcessDef.getId()); + + ProcessInstanceMigrationValidationResult processInstanceMigrationValidationResult = processInstanceMigrationBuilder + .validateMigration(processInstance.getId()); + assertThat(processInstanceMigrationValidationResult.isMigrationValid()).isTrue(); + + processInstanceMigrationBuilder.migrate(processInstance.getId()); + + //Confirm + executions = runtimeService.createExecutionQuery().processInstanceId(processInstance.getId()).onlyChildExecutions().list(); + assertThat(executions) + .extracting(Execution::getActivityId) + .containsExactlyInAnyOrder( + "processTask", "eventSubProcessStart", "eventSubProcess", "eventSubProcessTask", "eventSubProcess", "eventSubProcessTask" + ); + assertThat(executions) + .extracting("processDefinitionId") + .containsOnly(version2ProcessDef.getId()); + tasks = taskService.createTaskQuery().processInstanceId(processInstance.getId()).list(); + assertThat(tasks) + .extracting(Task::getTaskDefinitionKey, Task::getProcessDefinitionId) + .containsExactlyInAnyOrder( + Tuple.tuple("processTask", version2ProcessDef.getId()), + Tuple.tuple("eventSubProcessTask", version2ProcessDef.getId()), + Tuple.tuple("eventSubProcessTask", version2ProcessDef.getId()) + ); + + eventSubscriptions = runtimeService.createEventSubscriptionQuery().processInstanceId(processInstance.getId()).list(); + assertThat(eventSubscriptions) + .extracting(EventSubscription::getEventName, EventSubscription::getActivityId, EventSubscription::getProcessDefinitionId) + .containsExactlyInAnyOrder(Tuple.tuple("someMessage", "eventSubProcessStart", version2ProcessDef.getId())); + + completeProcessInstanceTasks(processInstance.getId()); + assertProcessEnded(processInstance.getId()); + } + @Test public void testMigrateSimpleActivityToActivityInsideNonInterruptingMessageEventSubProcessInNewDefinition() { ProcessDefinition procDefOneTask = deployProcessDefinition("my deploy", @@ -403,6 +851,243 @@ public void testMigrateSimpleActivityToActivityInsideNonInterruptingMessageEvent assertProcessEnded(processInstance.getId()); } + @Test + public void testMigrateNonInterruptingTimerEventSubProcess() { + //Deploy first version of the process + ProcessDefinition version1ProcessDef = deployProcessDefinition("my deploy", + "org/flowable/engine/test/api/runtime/migration/non-interrupting-timer-event-subprocess.bpmn20.xml"); + + //Start an instance of the first version of the process for migration + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(version1ProcessDef.getKey()); + + //Deploy second version of the same process + ProcessDefinition version2ProcessDef = deployProcessDefinition("my deploy", + "org/flowable/engine/test/api/runtime/migration/non-interrupting-timer-event-subprocess.bpmn20.xml"); + assertThat(version1ProcessDef.getId()).isNotEqualTo(version2ProcessDef.getId()); + + //Confirm the state to migrate + List executions = runtimeService.createExecutionQuery().processInstanceId(processInstance.getId()).onlyChildExecutions().list(); + assertThat(executions) + .extracting(Execution::getActivityId) + .containsExactlyInAnyOrder("processTask", "eventSubProcessStart"); + assertThat(executions) + .extracting("processDefinitionId") + .containsOnly(version1ProcessDef.getId()); + List tasks = taskService.createTaskQuery().processInstanceId(processInstance.getId()).list(); + assertThat(tasks) + .extracting(Task::getTaskDefinitionKey, Task::getProcessDefinitionId) + .containsExactlyInAnyOrder(Tuple.tuple("processTask", version1ProcessDef.getId())); + + List timerJobs = managementService.createTimerJobQuery().processInstanceId(processInstance.getId()).list(); + assertThat(timerJobs) + .extracting(Job::getProcessDefinitionId, Job::getElementId) + .containsExactlyInAnyOrder(Tuple.tuple(version1ProcessDef.getId(), "eventSubProcessStart")); + + changeStateEventListener.clear(); + + //Migrate to the other processDefinition + ProcessInstanceMigrationBuilder processInstanceMigrationBuilder = processMigrationService.createProcessInstanceMigrationBuilder() + .migrateToProcessDefinition(version2ProcessDef.getId()); + + ProcessInstanceMigrationValidationResult processInstanceMigrationValidationResult = processInstanceMigrationBuilder + .validateMigration(processInstance.getId()); + assertThat(processInstanceMigrationValidationResult.isMigrationValid()).isTrue(); + + processInstanceMigrationBuilder.migrate(processInstance.getId()); + + //Confirm + executions = runtimeService.createExecutionQuery().processInstanceId(processInstance.getId()).onlyChildExecutions().list(); + assertThat(executions) + .extracting(Execution::getActivityId) + .containsExactlyInAnyOrder("processTask", "eventSubProcessStart"); + assertThat(executions) + .extracting("processDefinitionId") + .containsOnly(version2ProcessDef.getId()); + tasks = taskService.createTaskQuery().processInstanceId(processInstance.getId()).list(); + assertThat(tasks) + .extracting(Task::getTaskDefinitionKey, Task::getProcessDefinitionId) + .containsExactlyInAnyOrder(Tuple.tuple("processTask", version2ProcessDef.getId())); + + timerJobs = managementService.createTimerJobQuery().processInstanceId(processInstance.getId()).list(); + assertThat(timerJobs) + .extracting(Job::getProcessDefinitionId, Job::getElementId) + .containsExactlyInAnyOrder(Tuple.tuple(version2ProcessDef.getId(), "eventSubProcessStart")); + + completeProcessInstanceTasks(processInstance.getId()); + assertProcessEnded(processInstance.getId()); + } + + @Test + public void testMigrateNonInterruptingTimerEventSubProcessWithStartedSubProcess() { + //Deploy first version of the process + ProcessDefinition version1ProcessDef = deployProcessDefinition("my deploy", + "org/flowable/engine/test/api/runtime/migration/non-interrupting-timer-event-subprocess.bpmn20.xml"); + + //Start an instance of the first version of the process for migration + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(version1ProcessDef.getKey()); + + //Deploy second version of the same process + ProcessDefinition version2ProcessDef = deployProcessDefinition("my deploy", + "org/flowable/engine/test/api/runtime/migration/non-interrupting-timer-event-subprocess.bpmn20.xml"); + assertThat(version1ProcessDef.getId()).isNotEqualTo(version2ProcessDef.getId()); + + //Trigger the timer job + List timerJobs = managementService.createTimerJobQuery().processInstanceId(processInstance.getId()).list(); + assertThat(timerJobs).hasSize(1); + + managementService.moveTimerToExecutableJob(timerJobs.get(0).getId()); + managementService.executeJob(timerJobs.get(0).getId()); + + //Confirm the state to migrate + List executions = runtimeService.createExecutionQuery().processInstanceId(processInstance.getId()).onlyChildExecutions().list(); + assertThat(executions) + .extracting(Execution::getActivityId) + .containsExactlyInAnyOrder("processTask", "eventSubProcessStart", "eventSubProcess", "eventSubProcessTask"); + assertThat(executions) + .extracting("processDefinitionId") + .containsOnly(version1ProcessDef.getId()); + List tasks = taskService.createTaskQuery().processInstanceId(processInstance.getId()).list(); + assertThat(tasks) + .extracting(Task::getTaskDefinitionKey, Task::getProcessDefinitionId) + .containsExactlyInAnyOrder( + Tuple.tuple("processTask", version1ProcessDef.getId()), + Tuple.tuple("eventSubProcessTask", version1ProcessDef.getId()) + ); + + timerJobs = managementService.createTimerJobQuery().processInstanceId(processInstance.getId()).list(); + assertThat(timerJobs) + .extracting(Job::getProcessDefinitionId, Job::getElementId) + .containsExactlyInAnyOrder(Tuple.tuple(version1ProcessDef.getId(), "eventSubProcessStart")); + + changeStateEventListener.clear(); + + //Migrate to the other processDefinition + ProcessInstanceMigrationBuilder processInstanceMigrationBuilder = processMigrationService.createProcessInstanceMigrationBuilder() + .migrateToProcessDefinition(version2ProcessDef.getId()); + + ProcessInstanceMigrationValidationResult processInstanceMigrationValidationResult = processInstanceMigrationBuilder + .validateMigration(processInstance.getId()); + assertThat(processInstanceMigrationValidationResult.isMigrationValid()).isTrue(); + + processInstanceMigrationBuilder.migrate(processInstance.getId()); + + //Confirm + executions = runtimeService.createExecutionQuery().processInstanceId(processInstance.getId()).onlyChildExecutions().list(); + assertThat(executions) + .extracting(Execution::getActivityId) + .containsExactlyInAnyOrder("processTask", "eventSubProcessStart", "eventSubProcess", "eventSubProcessTask"); + assertThat(executions) + .extracting("processDefinitionId") + .containsOnly(version2ProcessDef.getId()); + tasks = taskService.createTaskQuery().processInstanceId(processInstance.getId()).list(); + assertThat(tasks) + .extracting(Task::getTaskDefinitionKey, Task::getProcessDefinitionId) + .containsExactlyInAnyOrder( + Tuple.tuple("processTask", version2ProcessDef.getId()), + Tuple.tuple("eventSubProcessTask", version2ProcessDef.getId()) + ); + + timerJobs = managementService.createTimerJobQuery().processInstanceId(processInstance.getId()).list(); + assertThat(timerJobs) + .extracting(Job::getProcessDefinitionId, Job::getElementId) + .containsExactlyInAnyOrder(Tuple.tuple(version2ProcessDef.getId(), "eventSubProcessStart")); + + completeProcessInstanceTasks(processInstance.getId()); + assertProcessEnded(processInstance.getId()); + } + + @Test + public void testMigrateNonInterruptingTimerEventSubProcessWithTwoStartedSubProcess() { + //Deploy first version of the process + ProcessDefinition version1ProcessDef = deployProcessDefinition("my deploy", + "org/flowable/engine/test/api/runtime/migration/non-interrupting-timer-event-subprocess.bpmn20.xml"); + + //Start an instance of the first version of the process for migration + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(version1ProcessDef.getKey()); + + //Deploy second version of the same process + ProcessDefinition version2ProcessDef = deployProcessDefinition("my deploy", + "org/flowable/engine/test/api/runtime/migration/non-interrupting-timer-event-subprocess.bpmn20.xml"); + assertThat(version1ProcessDef.getId()).isNotEqualTo(version2ProcessDef.getId()); + + //Trigger the timer job to create the first sub process + List timerJobs = managementService.createTimerJobQuery().processInstanceId(processInstance.getId()).list(); + assertThat(timerJobs).hasSize(1); + + managementService.moveTimerToExecutableJob(timerJobs.get(0).getId()); + managementService.executeJob(timerJobs.get(0).getId()); + + //Trigger the timer job to create the second sub process + timerJobs = managementService.createTimerJobQuery().processInstanceId(processInstance.getId()).list(); + assertThat(timerJobs).hasSize(1); + + managementService.moveTimerToExecutableJob(timerJobs.get(0).getId()); + managementService.executeJob(timerJobs.get(0).getId()); + + //Confirm the state to migrate + List executions = runtimeService.createExecutionQuery().processInstanceId(processInstance.getId()).onlyChildExecutions().list(); + assertThat(executions) + .extracting(Execution::getActivityId) + .containsExactlyInAnyOrder( + "processTask", "eventSubProcessStart", "eventSubProcess", "eventSubProcessTask", "eventSubProcess", "eventSubProcessTask" + ); + assertThat(executions) + .extracting("processDefinitionId") + .containsOnly(version1ProcessDef.getId()); + List tasks = taskService.createTaskQuery().processInstanceId(processInstance.getId()).list(); + assertThat(tasks) + .extracting(Task::getTaskDefinitionKey, Task::getProcessDefinitionId) + .containsExactlyInAnyOrder( + Tuple.tuple("processTask", version1ProcessDef.getId()), + Tuple.tuple("eventSubProcessTask", version1ProcessDef.getId()), + Tuple.tuple("eventSubProcessTask", version1ProcessDef.getId()) + ); + + timerJobs = managementService.createTimerJobQuery().processInstanceId(processInstance.getId()).list(); + assertThat(timerJobs) + .extracting(Job::getProcessDefinitionId, Job::getElementId) + .containsExactlyInAnyOrder(Tuple.tuple(version1ProcessDef.getId(), "eventSubProcessStart")); + + changeStateEventListener.clear(); + + //Migrate to the other processDefinition + ProcessInstanceMigrationBuilder processInstanceMigrationBuilder = processMigrationService.createProcessInstanceMigrationBuilder() + .migrateToProcessDefinition(version2ProcessDef.getId()); + + ProcessInstanceMigrationValidationResult processInstanceMigrationValidationResult = processInstanceMigrationBuilder + .validateMigration(processInstance.getId()); + assertThat(processInstanceMigrationValidationResult.isMigrationValid()).isTrue(); + + processInstanceMigrationBuilder.migrate(processInstance.getId()); + + //Confirm + executions = runtimeService.createExecutionQuery().processInstanceId(processInstance.getId()).onlyChildExecutions().list(); + assertThat(executions) + .extracting(Execution::getActivityId) + .containsExactlyInAnyOrder( + "processTask", "eventSubProcessStart", "eventSubProcess", "eventSubProcessTask", "eventSubProcess", "eventSubProcessTask" + ); + assertThat(executions) + .extracting("processDefinitionId") + .containsOnly(version2ProcessDef.getId()); + tasks = taskService.createTaskQuery().processInstanceId(processInstance.getId()).list(); + assertThat(tasks) + .extracting(Task::getTaskDefinitionKey, Task::getProcessDefinitionId) + .containsExactlyInAnyOrder( + Tuple.tuple("processTask", version2ProcessDef.getId()), + Tuple.tuple("eventSubProcessTask", version2ProcessDef.getId()), + Tuple.tuple("eventSubProcessTask", version2ProcessDef.getId()) + ); + + timerJobs = managementService.createTimerJobQuery().processInstanceId(processInstance.getId()).list(); + assertThat(timerJobs) + .extracting(Job::getProcessDefinitionId, Job::getElementId) + .containsExactlyInAnyOrder(Tuple.tuple(version2ProcessDef.getId(), "eventSubProcessStart")); + + completeProcessInstanceTasks(processInstance.getId()); + assertProcessEnded(processInstance.getId()); + } + @Test public void testMigrateSimpleActivityToActivityInsideNonInterruptingTimerEventSubProcessInNewDefinition() { ProcessDefinition procDefOneTask = deployProcessDefinition("my deploy", diff --git a/modules/flowable-engine/src/test/resources/org/flowable/engine/test/api/runtime/migration/non-interrupting-eventregistry-event-subprocess.bpmn20.xml b/modules/flowable-engine/src/test/resources/org/flowable/engine/test/api/runtime/migration/non-interrupting-eventregistry-event-subprocess.bpmn20.xml new file mode 100644 index 00000000000..c17e1412d31 --- /dev/null +++ b/modules/flowable-engine/src/test/resources/org/flowable/engine/test/api/runtime/migration/non-interrupting-eventregistry-event-subprocess.bpmn20.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + myEvent + + + + + + + + + + From 42c9189ede207e695ec170f5f30ff7c07a4a1beb Mon Sep 17 00:00:00 2001 From: dbm Date: Thu, 29 Aug 2024 14:23:42 -0400 Subject: [PATCH 5/8] Chore: remove unused imports --- .../flowable/test/cmmn/converter/NameWithNewLineTest.java | 1 - .../cmmn/test/runtime/PlanItemInstanceQueryTest.java | 3 --- .../common/engine/impl/AbstractEngineConfiguration.java | 6 ------ .../java/org/flowable/common/engine/impl/util/DbUtil.java | 1 - .../servicetask/LeavingFutureJavaDelegateServiceTask.java | 4 ---- 5 files changed, 15 deletions(-) diff --git a/modules/flowable-cmmn-converter/src/test/java/org/flowable/test/cmmn/converter/NameWithNewLineTest.java b/modules/flowable-cmmn-converter/src/test/java/org/flowable/test/cmmn/converter/NameWithNewLineTest.java index 7a59295fde6..ded6f547810 100644 --- a/modules/flowable-cmmn-converter/src/test/java/org/flowable/test/cmmn/converter/NameWithNewLineTest.java +++ b/modules/flowable-cmmn-converter/src/test/java/org/flowable/test/cmmn/converter/NameWithNewLineTest.java @@ -16,7 +16,6 @@ import static org.flowable.cmmn.converter.CmmnXmlConstants.ATTRIBUTE_ELEMENT_NAME; import org.flowable.cmmn.model.CmmnModel; -import org.flowable.cmmn.model.ExtensionElement; import org.flowable.cmmn.model.PlanItemDefinition; import org.flowable.test.cmmn.converter.util.CmmnXmlConverterTest; diff --git a/modules/flowable-cmmn-engine/src/test/java/org/flowable/cmmn/test/runtime/PlanItemInstanceQueryTest.java b/modules/flowable-cmmn-engine/src/test/java/org/flowable/cmmn/test/runtime/PlanItemInstanceQueryTest.java index c3a524da287..d3715616bc8 100644 --- a/modules/flowable-cmmn-engine/src/test/java/org/flowable/cmmn/test/runtime/PlanItemInstanceQueryTest.java +++ b/modules/flowable-cmmn-engine/src/test/java/org/flowable/cmmn/test/runtime/PlanItemInstanceQueryTest.java @@ -13,10 +13,8 @@ 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 static org.flowable.cmmn.api.runtime.PlanItemInstanceState.ACTIVE; -import static org.flowable.cmmn.api.runtime.PlanItemInstanceState.AVAILABLE; import java.util.ArrayList; import java.util.Arrays; @@ -32,7 +30,6 @@ import org.flowable.cmmn.engine.PlanItemLocalizationManager; import org.flowable.cmmn.engine.test.CmmnDeployment; import org.flowable.cmmn.engine.test.FlowableCmmnTestCase; -import org.flowable.common.engine.api.FlowableIllegalArgumentException; import org.flowable.task.api.Task; import org.junit.Before; import org.junit.Test; diff --git a/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/AbstractEngineConfiguration.java b/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/AbstractEngineConfiguration.java index cc50a597431..358ee894d33 100755 --- a/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/AbstractEngineConfiguration.java +++ b/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/AbstractEngineConfiguration.java @@ -15,11 +15,6 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; -import java.sql.Connection; -import java.sql.DatabaseMetaData; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; import java.time.Duration; import java.util.ArrayList; import java.util.Collection; @@ -35,7 +30,6 @@ import javax.naming.InitialContext; import javax.sql.DataSource; -import org.apache.commons.lang3.StringUtils; import org.apache.ibatis.builder.xml.XMLConfigBuilder; import org.apache.ibatis.builder.xml.XMLMapperBuilder; import org.apache.ibatis.datasource.pooled.PooledDataSource; diff --git a/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/util/DbUtil.java b/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/util/DbUtil.java index 7d6ba521af7..5f19d613014 100644 --- a/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/util/DbUtil.java +++ b/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/util/DbUtil.java @@ -17,7 +17,6 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; -import java.util.Map; import java.util.Properties; import javax.sql.DataSource; diff --git a/modules/flowable-engine/src/test/java/org/flowable/engine/test/bpmn/servicetask/LeavingFutureJavaDelegateServiceTask.java b/modules/flowable-engine/src/test/java/org/flowable/engine/test/bpmn/servicetask/LeavingFutureJavaDelegateServiceTask.java index bb2167c5a7a..c250c3f20d9 100644 --- a/modules/flowable-engine/src/test/java/org/flowable/engine/test/bpmn/servicetask/LeavingFutureJavaDelegateServiceTask.java +++ b/modules/flowable-engine/src/test/java/org/flowable/engine/test/bpmn/servicetask/LeavingFutureJavaDelegateServiceTask.java @@ -15,11 +15,7 @@ import java.io.Serializable; import java.util.Map; -import java.util.concurrent.CompletableFuture; -import org.flowable.common.engine.api.async.AsyncTaskInvoker; -import org.flowable.engine.delegate.DelegateExecution; -import org.flowable.engine.delegate.FutureJavaDelegate; import org.flowable.engine.delegate.MapBasedFlowableFutureJavaDelegate; import org.flowable.engine.delegate.ReadOnlyDelegateExecution; import org.flowable.engine.impl.delegate.TriggerableJavaDelegate; From 2ee613e8e575df128c7e9170c75233329be5a355 Mon Sep 17 00:00:00 2001 From: Filip Hrisafov Date: Tue, 10 Sep 2024 15:54:05 +0200 Subject: [PATCH 6/8] Add method in EngineSqlScriptBasedDbSchemaManager for parsing change log version --- .../EngineSqlScriptBasedDbSchemaManager.java | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/db/EngineSqlScriptBasedDbSchemaManager.java b/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/db/EngineSqlScriptBasedDbSchemaManager.java index b60d7e93491..e85c4ea2469 100644 --- a/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/db/EngineSqlScriptBasedDbSchemaManager.java +++ b/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/db/EngineSqlScriptBasedDbSchemaManager.java @@ -190,6 +190,10 @@ protected String getDbVersion() { return getProperty(getSchemaVersionPropertyName(), false); } + protected int getChangeLogVersionOrder(String changeLogVersion) { + return Integer.parseInt(changeLogVersion); + } + protected ChangeLogVersion getChangeLogVersion() { String changeLogTableName = getChangeLogTableName(); if (changeLogTableName != null && isTablePresent(changeLogTableName)) { @@ -199,22 +203,22 @@ protected ChangeLogVersion getChangeLogVersion() { } try (PreparedStatement statement = databaseConfiguration.getConnection() .prepareStatement("select ID from " + changeLogTableName + " order by DATEEXECUTED")) { - int changeLogVersion = 0; + int latestChangeLogVersionOrder = 0; + String changeLogVersion = null; try (ResultSet resultSet = statement.executeQuery()) { while (resultSet.next()) { String changeLogVersionId = resultSet.getString(1); - int parsedChangeLogVersion = Integer.parseInt(changeLogVersionId); - if (parsedChangeLogVersion > changeLogVersion) { + int changeLogVersionOrder = getChangeLogVersionOrder(changeLogVersionId); + if (changeLogVersionOrder > latestChangeLogVersionOrder) { // Even though we are ordering by DATEEXECUTED, and the last ID should be the last executed one. // It is still possible that there are multiple entries with the same DATEEXECUTED value and the order might not be correct. // e.g. MySQL 8.0 sometimes does not return the correct order. - changeLogVersion = parsedChangeLogVersion; + changeLogVersion = changeLogVersionId; } } } - if (changeLogVersion > 0) { - String changeLogVersionString = String.valueOf(changeLogVersion); - return new ChangeLogVersion(changeLogVersionString, getDbVersionForChangelogVersion(changeLogVersionString)); + if (changeLogVersion != null) { + return new ChangeLogVersion(changeLogVersion, getDbVersionForChangelogVersion(changeLogVersion)); } } catch (SQLException e) { throw new RuntimeException("Failed to get change log version from " + changeLogTableName, e); From 675608b1316ce4cee51105594dc808a61f6185f9 Mon Sep 17 00:00:00 2001 From: Filip Hrisafov Date: Wed, 11 Sep 2024 08:29:27 +0200 Subject: [PATCH 7/8] Use cmmn script task expression value if no string value is missing and do not throw exception if field is missing Not throwing the exception aligns this with the way it works for BPMN, where no exception is thrown by the model. There will eventually be an exception thrown when the script is executed --- ...ScriptServiceTaskCmmnXmlConverterTest.java | 30 +++++++++++++++ .../script-task-expression-field.cmmn | 38 +++++++++++++++++++ .../cmmn/model/ScriptServiceTask.java | 19 ++++++---- 3 files changed, 79 insertions(+), 8 deletions(-) create mode 100644 modules/flowable-cmmn-converter/src/test/resources/org/flowable/test/cmmn/converter/script-task-expression-field.cmmn diff --git a/modules/flowable-cmmn-converter/src/test/java/org/flowable/test/cmmn/converter/ScriptServiceTaskCmmnXmlConverterTest.java b/modules/flowable-cmmn-converter/src/test/java/org/flowable/test/cmmn/converter/ScriptServiceTaskCmmnXmlConverterTest.java index a9da14dca6a..e999c9fef73 100644 --- a/modules/flowable-cmmn-converter/src/test/java/org/flowable/test/cmmn/converter/ScriptServiceTaskCmmnXmlConverterTest.java +++ b/modules/flowable-cmmn-converter/src/test/java/org/flowable/test/cmmn/converter/ScriptServiceTaskCmmnXmlConverterTest.java @@ -84,4 +84,34 @@ public void validateModel(CmmnModel cmmnModel) { }); } + + @CmmnXmlConverterTest("org/flowable/test/cmmn/converter/script-task-expression-field.cmmn") + public void scriptTaskWithExpressionField(CmmnModel cmmnModel) { + assertThat(cmmnModel).isNotNull(); + + PlanItem planItemTaskA = cmmnModel.findPlanItem("planItemTaskA"); + assertThat(planItemTaskA.getEntryCriteria()).isEmpty(); + + PlanItemDefinition planItemDefinition = planItemTaskA.getPlanItemDefinition(); + assertThat(planItemDefinition) + .isInstanceOfSatisfying(ScriptServiceTask.class, scriptTask -> { + assertThat(scriptTask.getType()).isEqualTo(ScriptServiceTask.SCRIPT_TASK); + assertThat(scriptTask.getScriptFormat()).isEqualTo("javascript"); + assertThat(scriptTask.getScript()).isEqualTo("var a = '${testA}';"); + assertThat(scriptTask.getResultVariableName()).isEqualTo("scriptResult"); + assertThat(scriptTask.isAutoStoreVariables()).isFalse(); + assertThat(scriptTask.isBlocking()).isTrue(); + assertThat(scriptTask.isAsync()).isFalse(); + }); + + PlanItem planItemTaskB = cmmnModel.findPlanItem("planItemTaskB"); + planItemDefinition = planItemTaskB.getPlanItemDefinition(); + assertThat(planItemDefinition) + .isInstanceOfSatisfying(ScriptServiceTask.class, scriptServiceTask -> { + assertThat(scriptServiceTask.getScriptFormat()).isEqualTo("groovy"); + assertThat(scriptServiceTask.getScript()).isEqualTo("var b = '${testB}';"); + assertThat(scriptServiceTask.isAutoStoreVariables()).isTrue(); + }); + } + } diff --git a/modules/flowable-cmmn-converter/src/test/resources/org/flowable/test/cmmn/converter/script-task-expression-field.cmmn b/modules/flowable-cmmn-converter/src/test/resources/org/flowable/test/cmmn/converter/script-task-expression-field.cmmn new file mode 100644 index 00000000000..f5609cc3580 --- /dev/null +++ b/modules/flowable-cmmn-converter/src/test/resources/org/flowable/test/cmmn/converter/script-task-expression-field.cmmn @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/flowable-cmmn-model/src/main/java/org/flowable/cmmn/model/ScriptServiceTask.java b/modules/flowable-cmmn-model/src/main/java/org/flowable/cmmn/model/ScriptServiceTask.java index 36da3d73ae3..7d06dbcdcdf 100644 --- a/modules/flowable-cmmn-model/src/main/java/org/flowable/cmmn/model/ScriptServiceTask.java +++ b/modules/flowable-cmmn-model/src/main/java/org/flowable/cmmn/model/ScriptServiceTask.java @@ -12,9 +12,7 @@ */ package org.flowable.cmmn.model; -import java.util.Optional; - -import org.flowable.common.engine.api.FlowableException; +import org.apache.commons.lang3.StringUtils; /** * @author Dennis @@ -38,11 +36,16 @@ public void setScriptFormat(String scriptFormat) { } public String getScript() { - Optional script = fieldExtensions.stream() - .filter(e -> "script".equalsIgnoreCase(e.getFieldName())) - .findFirst() - .map(FieldExtension::getStringValue); - return script.orElseThrow(() -> new FlowableException("Missing script")); + for (FieldExtension fieldExtension : fieldExtensions) { + if ("script".equalsIgnoreCase(fieldExtension.getFieldName())) { + String script = fieldExtension.getStringValue(); + if (StringUtils.isNotEmpty(script)) { + return script; + } + return fieldExtension.getExpression(); + } + } + return null; } public boolean isAutoStoreVariables() { From 1432f1738d6ccff2773b890be6e153e510be6505 Mon Sep 17 00:00:00 2001 From: Joram Barrez Date: Wed, 11 Sep 2024 10:19:30 +0200 Subject: [PATCH 8/8] Pass engine configuration to typeHandler and typeAlias to retrieve configuration settings if needed --- .../common/engine/impl/AbstractEngineConfiguration.java | 4 ++-- .../common/engine/impl/AbstractEngineConfigurator.java | 4 ++-- .../common/engine/impl/db/MybatisTypeAliasConfigurator.java | 3 ++- .../common/engine/impl/db/MybatisTypeHandlerConfigurator.java | 3 ++- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/AbstractEngineConfiguration.java b/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/AbstractEngineConfiguration.java index 358ee894d33..5b156847873 100755 --- a/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/AbstractEngineConfiguration.java +++ b/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/AbstractEngineConfiguration.java @@ -954,12 +954,12 @@ protected void applyCustomMybatisCustomizations(Configuration configuration) { if (dependentEngineMybatisTypeAliasConfigs != null) { for (MybatisTypeAliasConfigurator typeAliasConfig : dependentEngineMybatisTypeAliasConfigs) { - typeAliasConfig.configure(configuration.getTypeAliasRegistry()); + typeAliasConfig.configure(this, configuration.getTypeAliasRegistry()); } } if (dependentEngineMybatisTypeHandlerConfigs != null) { for (MybatisTypeHandlerConfigurator typeHandlerConfig : dependentEngineMybatisTypeHandlerConfigs) { - typeHandlerConfig.configure(configuration.getTypeHandlerRegistry()); + typeHandlerConfig.configure(this, configuration.getTypeHandlerRegistry()); } } diff --git a/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/AbstractEngineConfigurator.java b/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/AbstractEngineConfigurator.java index a0ec4df84a8..66d36d239a9 100644 --- a/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/AbstractEngineConfigurator.java +++ b/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/AbstractEngineConfigurator.java @@ -113,7 +113,7 @@ protected void registerCustomMybatisMappings(AbstractEngineConfiguration engineC Node node = typeAliasList.item(i); MybatisTypeAliasConfigurator typeAlias = new MybatisTypeAliasConfigurator() { @Override - public void configure(TypeAliasRegistry typeAliasRegistry) { + public void configure(AbstractEngineConfiguration abstractEngineConfiguration, TypeAliasRegistry typeAliasRegistry) { try { typeAliasRegistry.registerAlias(node.getAttributes().getNamedItem("alias").getTextContent(), Class.forName(node.getAttributes().getNamedItem("type").getTextContent())); @@ -131,7 +131,7 @@ public void configure(TypeAliasRegistry typeAliasRegistry) { Node node = typeHandlerList.item(i); MybatisTypeHandlerConfigurator typeHandler = new MybatisTypeHandlerConfigurator() { @Override - public void configure(TypeHandlerRegistry typeHandlerRegistry) { + public void configure(AbstractEngineConfiguration abstractEngineConfiguration, TypeHandlerRegistry typeHandlerRegistry) { try { typeHandlerRegistry.register(node.getAttributes().getNamedItem("javaType").getTextContent(), node.getAttributes().getNamedItem("handler").getTextContent()); diff --git a/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/db/MybatisTypeAliasConfigurator.java b/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/db/MybatisTypeAliasConfigurator.java index 9e618b85663..96867688629 100644 --- a/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/db/MybatisTypeAliasConfigurator.java +++ b/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/db/MybatisTypeAliasConfigurator.java @@ -13,6 +13,7 @@ package org.flowable.common.engine.impl.db; import org.apache.ibatis.type.TypeAliasRegistry; +import org.flowable.common.engine.impl.AbstractEngineConfiguration; /** * This class configures typeAliases in {@link TypeAliasRegistry} @@ -21,5 +22,5 @@ */ public interface MybatisTypeAliasConfigurator { - void configure(TypeAliasRegistry typeAliasRegistry); + void configure(AbstractEngineConfiguration engineConfiguration, TypeAliasRegistry typeAliasRegistry); } diff --git a/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/db/MybatisTypeHandlerConfigurator.java b/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/db/MybatisTypeHandlerConfigurator.java index b4342b7b3e8..2b0f91b4f84 100644 --- a/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/db/MybatisTypeHandlerConfigurator.java +++ b/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/db/MybatisTypeHandlerConfigurator.java @@ -14,6 +14,7 @@ import org.apache.ibatis.type.TypeHandler; import org.apache.ibatis.type.TypeHandlerRegistry; +import org.flowable.common.engine.impl.AbstractEngineConfiguration; /** * This class configures {@link TypeHandler} in {@link TypeHandlerRegistry} @@ -22,5 +23,5 @@ */ public interface MybatisTypeHandlerConfigurator { - void configure(TypeHandlerRegistry typeHandlerRegistry); + void configure(AbstractEngineConfiguration engineConfiguration, TypeHandlerRegistry typeHandlerRegistry); }