From 8f8e085fffde1c01a50ae019543d145fd3d39b48 Mon Sep 17 00:00:00 2001 From: Tijs Rademakers Date: Sun, 10 Sep 2023 11:23:56 +0200 Subject: [PATCH] Fix issue with stage plan item instance selection in case migration + terminate plan item instances that are not present in cmmn model automatically --- .../PlanItemInstanceEntityManagerImpl.java | 17 +- .../impl/runtime/CaseInstanceChangeState.java | 9 +- .../migration/CaseInstanceMigrationTest.java | 223 ++++++++++++++++++ .../migration/case-with-active-stage.cmmn.xml | 22 ++ .../test/migration/case-with-stage.cmmn.xml | 25 ++ 5 files changed, 292 insertions(+), 4 deletions(-) create mode 100644 modules/flowable-cmmn-engine/src/test/resources/org/flowable/cmmn/test/migration/case-with-active-stage.cmmn.xml create mode 100644 modules/flowable-cmmn-engine/src/test/resources/org/flowable/cmmn/test/migration/case-with-stage.cmmn.xml diff --git a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/persistence/entity/PlanItemInstanceEntityManagerImpl.java b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/persistence/entity/PlanItemInstanceEntityManagerImpl.java index a89e424eb46..d1a6b3435bc 100644 --- a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/persistence/entity/PlanItemInstanceEntityManagerImpl.java +++ b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/persistence/entity/PlanItemInstanceEntityManagerImpl.java @@ -14,6 +14,7 @@ package org.flowable.cmmn.engine.impl.persistence.entity; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.List; @@ -29,10 +30,12 @@ import org.flowable.cmmn.converter.util.PlanItemDependencyUtil; import org.flowable.cmmn.engine.CmmnEngineConfiguration; import org.flowable.cmmn.engine.impl.persistence.entity.data.PlanItemInstanceDataManager; +import org.flowable.cmmn.engine.impl.repository.CaseDefinitionUtil; import org.flowable.cmmn.engine.impl.runtime.PlanItemInstanceQueryImpl; import org.flowable.cmmn.engine.impl.util.CaseInstanceUtil; import org.flowable.cmmn.engine.impl.util.CommandContextUtil; import org.flowable.cmmn.engine.impl.util.ExpressionUtil; +import org.flowable.cmmn.model.CmmnModel; import org.flowable.cmmn.model.EventListener; import org.flowable.cmmn.model.PlanFragment; import org.flowable.cmmn.model.PlanItem; @@ -480,8 +483,20 @@ public void updatePlanItemInstancesCaseDefinitionId(String caseInstanceId, Strin .caseInstanceId(caseInstanceId) .includeEnded(); List planItemInstances = findByCriteria(planItemQuery); - if (planItemInstances != null) { + if (planItemInstances != null && !planItemInstances.isEmpty()) { + List endStates = Arrays.asList(PlanItemInstanceState.UNAVAILABLE, PlanItemInstanceState.DISABLED, PlanItemInstanceState.COMPLETED, PlanItemInstanceState.TERMINATED, PlanItemInstanceState.FAILED); + CmmnModel cmmnModel = CaseDefinitionUtil.getCmmnModel(caseDefinitionId); for (PlanItemInstance planItemInstance : planItemInstances) { + if (!endStates.contains(planItemInstance.getState())) { + if (cmmnModel.findPlanItemByPlanItemDefinitionId(planItemInstance.getPlanItemDefinitionId()) == null) { + PlanItemInstanceEntity planItemInstanceEntity = (PlanItemInstanceEntity) planItemInstance; + planItemInstanceEntity.setState(PlanItemInstanceState.TERMINATED); + planItemInstanceEntity.setEndedTime(engineConfiguration.getClock().getCurrentTime()); + planItemInstanceEntity.setTerminatedTime(planItemInstanceEntity.getEndedTime()); + CommandContextUtil.getCmmnHistoryManager(commandContext).recordPlanItemInstanceTerminated(planItemInstanceEntity); + } + } + PlanItemInstanceEntity planItemInstanceEntity = (PlanItemInstanceEntity) planItemInstance; planItemInstanceEntity.setCaseDefinitionId(caseDefinitionId); update(planItemInstanceEntity); diff --git a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/runtime/CaseInstanceChangeState.java b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/runtime/CaseInstanceChangeState.java index da0d0887039..d617ac6ba62 100644 --- a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/runtime/CaseInstanceChangeState.java +++ b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/runtime/CaseInstanceChangeState.java @@ -85,16 +85,19 @@ public void setCurrentPlanItemInstances(Map } public PlanItemInstanceEntity getRuntimePlanItemInstance(String planItemDefinitionId) { + PlanItemInstanceEntity foundPlanItemInstance = null; if (currentPlanItemInstances != null && currentPlanItemInstances.containsKey(planItemDefinitionId)) { List currentPlanItemInstanceList = currentPlanItemInstances.get(planItemDefinitionId); for (PlanItemInstanceEntity planItemInstance : currentPlanItemInstanceList) { - if (!PlanItemInstanceState.TERMINAL_STATES.contains(planItemInstance.getState())) { - return planItemInstance; + if (!PlanItemInstanceState.TERMINAL_STATES.contains(planItemInstance.getState()) && + (foundPlanItemInstance == null || !PlanItemInstanceState.WAITING_FOR_REPETITION.equals(planItemInstance.getState()))) { + + foundPlanItemInstance = planItemInstance; } } } - return null; + return foundPlanItemInstance; } public Map> getActivePlanItemInstances() { 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 f512c803360..cc03055bf8d 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 @@ -616,6 +616,73 @@ void withChangingTheAssignee() { } } + @Test + void withNoNewPlanItemForPlanItemInstance() { + // 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()); + List planItemInstances = cmmnRuntimeService.createPlanItemInstanceQuery() + .caseInstanceId(caseInstance.getId()) + .includeEnded() + .list(); + assertThat(planItemInstances).hasSize(2); + assertThat(planItemInstances) + .extracting(PlanItemInstance::getCaseDefinitionId) + .containsOnly(destinationDefinition.getId()); + assertThat(planItemInstances) + .extracting(PlanItemInstance::getName) + .containsExactlyInAnyOrder("Task 1", "Task 2"); + assertThat(planItemInstances) + .filteredOn("name", "Task 1") + .extracting(PlanItemInstance::getState) + .containsOnly(PlanItemInstanceState.ACTIVE); + + assertThat(planItemInstances) + .filteredOn("name", "Task 2") + .extracting(PlanItemInstance::getState) + .containsOnly(PlanItemInstanceState.TERMINATED); + + Task task = cmmnTaskService.createTaskQuery().caseInstanceId(caseInstance.getId()).taskDefinitionKey("humanTask1").singleResult(); + cmmnTaskService.complete(task.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()); + } + + assertThat(cmmnHistoryService.createHistoricPlanItemInstanceQuery().planItemInstanceCaseInstanceId(caseInstance.getId()).planItemInstanceDefinitionId("humanTask1").singleResult().getState()).isEqualTo(PlanItemInstanceState.COMPLETED); + assertThat(cmmnHistoryService.createHistoricPlanItemInstanceQuery().planItemInstanceCaseInstanceId(caseInstance.getId()).planItemInstanceDefinitionId("humanTask2").singleResult().getState()).isEqualTo(PlanItemInstanceState.TERMINATED); + + List historicTasks = cmmnHistoryService.createHistoricTaskInstanceQuery().caseInstanceId(caseInstance.getId()).list(); + assertThat(historicTasks).hasSize(2); + for (HistoricTaskInstance historicTask : historicTasks) { + assertThat(historicTask.getScopeDefinitionId()).isEqualTo(destinationDefinition.getId()); + assertThat(historicTask.getEndTime()).isNotNull(); + } + } + } + @Test void withChangingPlanItemId() { // Arrange @@ -1345,6 +1412,162 @@ void stageWithListener() { "humanTask4", "userEventListener1", "userEventListener2"); } + @Test + void withActivateStage() { + // Arrange + deployCaseDefinition("test1", "org/flowable/cmmn/test/migration/case-with-stage.cmmn.xml"); + CaseInstance caseInstance = cmmnRuntimeService.createCaseInstanceBuilder().caseDefinitionKey("migrationCase").start(); + CaseDefinition destinationDefinition = deployCaseDefinition("test1", "org/flowable/cmmn/test/migration/case-with-stage.cmmn.xml"); + + // Act + cmmnMigrationService.createCaseInstanceMigrationBuilder() + .migrateToCaseDefinition(destinationDefinition.getId()) + .addActivatePlanItemDefinitionMapping(PlanItemDefinitionMappingBuilder.createActivatePlanItemDefinitionMappingFor("cmmnStage1")) + .migrate(caseInstance.getId()); + + // Assert + CaseInstance caseInstanceAfterMigration = cmmnRuntimeService.createCaseInstanceQuery() + .caseInstanceId(caseInstance.getId()) + .singleResult(); + assertThat(caseInstanceAfterMigration.getCaseDefinitionId()).isEqualTo(destinationDefinition.getId()); + List planItemInstances = cmmnRuntimeService.createPlanItemInstanceQuery() + .caseInstanceId(caseInstance.getId()) + .includeEnded() + .list(); + assertThat(planItemInstances).hasSize(3); + assertThat(planItemInstances) + .extracting(PlanItemInstance::getCaseDefinitionId) + .containsOnly(destinationDefinition.getId()); + assertThat(planItemInstances) + .extracting(PlanItemInstance::getName) + .containsExactlyInAnyOrder("Stage 1", "Stage 1", "Human task 1"); + assertThat(planItemInstances) + .filteredOn("name", "Human task 1") + .extracting(PlanItemInstance::getState) + .containsOnly(PlanItemInstanceState.ACTIVE); + + assertThat(planItemInstances) + .filteredOn("name", "Stage 1") + .extracting(PlanItemInstance::getState) + .containsOnly(PlanItemInstanceState.WAITING_FOR_REPETITION, PlanItemInstanceState.ACTIVE); + + PlanItemInstance stagePlanItem = cmmnRuntimeService.createPlanItemInstanceQuery().caseInstanceId(caseInstance.getId()).planItemDefinitionId("cmmnStage1").planItemInstanceState(PlanItemInstanceState.ACTIVE).singleResult(); + + PlanItemInstance taskPlanItem = cmmnRuntimeService.createPlanItemInstanceQuery().caseInstanceId(caseInstance.getId()).planItemDefinitionId("stageTask1").singleResult(); + + assertThat(taskPlanItem.getStageInstanceId()).isEqualTo(stagePlanItem.getId()); + + Task task = cmmnTaskService.createTaskQuery().caseInstanceId(caseInstance.getId()).singleResult(); + cmmnTaskService.complete(task.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(1); + for (HistoricTaskInstance historicTask : historicTasks) { + assertThat(historicTask.getScopeDefinitionId()).isEqualTo(destinationDefinition.getId()); + } + } + } + + @Test + void withActivateAnotherRepetitionStage() { + // Arrange + deployCaseDefinition("test1", "org/flowable/cmmn/test/migration/case-with-active-stage.cmmn.xml"); + CaseInstance caseInstance = cmmnRuntimeService.createCaseInstanceBuilder().caseDefinitionKey("migrationCase").start(); + CaseDefinition destinationDefinition = deployCaseDefinition("test1", "org/flowable/cmmn/test/migration/case-with-active-stage.cmmn.xml"); + + List planItemInstances = cmmnRuntimeService.createPlanItemInstanceQuery() + .caseInstanceId(caseInstance.getId()) + .includeEnded() + .list(); + assertThat(planItemInstances).hasSize(3); + + assertThat(planItemInstances) + .extracting(PlanItemInstance::getName) + .containsExactlyInAnyOrder("Stage 1", "Stage 1", "Human task 1"); + assertThat(planItemInstances) + .filteredOn("name", "Human task 1") + .extracting(PlanItemInstance::getState) + .containsOnly(PlanItemInstanceState.ACTIVE); + + assertThat(planItemInstances) + .filteredOn("name", "Stage 1") + .extracting(PlanItemInstance::getState) + .containsOnly(PlanItemInstanceState.WAITING_FOR_REPETITION, PlanItemInstanceState.ACTIVE); + + // Act + cmmnMigrationService.createCaseInstanceMigrationBuilder() + .migrateToCaseDefinition(destinationDefinition.getId()) + .addActivatePlanItemDefinitionMapping(PlanItemDefinitionMappingBuilder.createActivatePlanItemDefinitionMappingFor("stageTask1")) + .migrate(caseInstance.getId()); + + // Assert + CaseInstance caseInstanceAfterMigration = cmmnRuntimeService.createCaseInstanceQuery() + .caseInstanceId(caseInstance.getId()) + .singleResult(); + assertThat(caseInstanceAfterMigration.getCaseDefinitionId()).isEqualTo(destinationDefinition.getId()); + planItemInstances = cmmnRuntimeService.createPlanItemInstanceQuery() + .caseInstanceId(caseInstance.getId()) + .includeEnded() + .list(); + assertThat(planItemInstances).hasSize(4); + assertThat(planItemInstances) + .extracting(PlanItemInstance::getCaseDefinitionId) + .containsOnly(destinationDefinition.getId()); + assertThat(planItemInstances) + .extracting(PlanItemInstance::getName) + .containsExactlyInAnyOrder("Stage 1", "Stage 1", "Human task 1", "Human task 1"); + assertThat(planItemInstances) + .filteredOn("name", "Human task 1") + .extracting(PlanItemInstance::getState) + .containsOnly(PlanItemInstanceState.ACTIVE, PlanItemInstanceState.ACTIVE); + + assertThat(planItemInstances) + .filteredOn("name", "Stage 1") + .extracting(PlanItemInstance::getState) + .containsOnly(PlanItemInstanceState.WAITING_FOR_REPETITION, PlanItemInstanceState.ACTIVE); + + PlanItemInstance stagePlanItem = cmmnRuntimeService.createPlanItemInstanceQuery().caseInstanceId(caseInstance.getId()).planItemDefinitionId("cmmnStage1").planItemInstanceState(PlanItemInstanceState.ACTIVE).singleResult(); + + List taskPlanItems = cmmnRuntimeService.createPlanItemInstanceQuery().caseInstanceId(caseInstance.getId()).planItemDefinitionId("stageTask1").list(); + + for (PlanItemInstance taskPlanItemInstance : taskPlanItems) { + assertThat(taskPlanItemInstance.getStageInstanceId()).isEqualTo(stagePlanItem.getId()); + } + + 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(4); + 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 terminateListener() { // Arrange diff --git a/modules/flowable-cmmn-engine/src/test/resources/org/flowable/cmmn/test/migration/case-with-active-stage.cmmn.xml b/modules/flowable-cmmn-engine/src/test/resources/org/flowable/cmmn/test/migration/case-with-active-stage.cmmn.xml new file mode 100644 index 00000000000..6158c3dc24a --- /dev/null +++ b/modules/flowable-cmmn-engine/src/test/resources/org/flowable/cmmn/test/migration/case-with-active-stage.cmmn.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/flowable-cmmn-engine/src/test/resources/org/flowable/cmmn/test/migration/case-with-stage.cmmn.xml b/modules/flowable-cmmn-engine/src/test/resources/org/flowable/cmmn/test/migration/case-with-stage.cmmn.xml new file mode 100644 index 00000000000..0585ad78bca --- /dev/null +++ b/modules/flowable-cmmn-engine/src/test/resources/org/flowable/cmmn/test/migration/case-with-stage.cmmn.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + complete + + + + + + + + + + + + \ No newline at end of file