diff --git a/modules/flowable-cmmn-api/src/main/java/org/flowable/cmmn/api/reactivation/CaseReactivationBuilder.java b/modules/flowable-cmmn-api/src/main/java/org/flowable/cmmn/api/reactivation/CaseReactivationBuilder.java index 1f913812499..f3fe821842c 100644 --- a/modules/flowable-cmmn-api/src/main/java/org/flowable/cmmn/api/reactivation/CaseReactivationBuilder.java +++ b/modules/flowable-cmmn-api/src/main/java/org/flowable/cmmn/api/reactivation/CaseReactivationBuilder.java @@ -24,6 +24,14 @@ * @author Micha Kiener */ public interface CaseReactivationBuilder { + + /** + * Adds a plan item instance for a new plan item definition. This is mainly needed when no reactivation listener was present in the old case definition id. + * + * @param planItemDefinitionId the plan item definition id for which a new plan item instance will be created in available state + * @return the builder for method chaining + */ + CaseReactivationBuilder addTerminatedPlanItemInstanceForPlanItemDefinition(String planItemDefinitionId); /** * Adds a variable to be added to the case before triggering the reactivation event. diff --git a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/cmd/ReactivateHistoricCaseInstanceCmd.java b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/cmd/ReactivateHistoricCaseInstanceCmd.java index 144a29a0d36..0d639b57e4d 100644 --- a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/cmd/ReactivateHistoricCaseInstanceCmd.java +++ b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/cmd/ReactivateHistoricCaseInstanceCmd.java @@ -13,17 +13,24 @@ package org.flowable.cmmn.engine.impl.cmd; import java.io.Serializable; +import java.util.Date; import org.apache.commons.lang3.StringUtils; import org.flowable.cmmn.api.history.HistoricCaseInstance; import org.flowable.cmmn.api.runtime.CaseInstance; import org.flowable.cmmn.api.runtime.CaseInstanceState; +import org.flowable.cmmn.api.runtime.PlanItemInstanceState; import org.flowable.cmmn.engine.CmmnEngineConfiguration; import org.flowable.cmmn.engine.impl.persistence.entity.CaseInstanceEntity; +import org.flowable.cmmn.engine.impl.persistence.entity.PlanItemInstanceEntity; +import org.flowable.cmmn.engine.impl.persistence.entity.PlanItemInstanceEntityManager; import org.flowable.cmmn.engine.impl.reactivation.CaseReactivationBuilderImpl; import org.flowable.cmmn.engine.impl.repository.CaseDefinitionUtil; import org.flowable.cmmn.engine.impl.util.CommandContextUtil; +import org.flowable.cmmn.model.Case; +import org.flowable.cmmn.model.PlanItem; import org.flowable.cmmn.model.ReactivateEventListener; +import org.flowable.common.engine.api.FlowableException; import org.flowable.common.engine.api.FlowableIllegalArgumentException; import org.flowable.common.engine.api.FlowableIllegalStateException; import org.flowable.common.engine.api.FlowableObjectNotFoundException; @@ -85,7 +92,8 @@ public CaseInstance execute(CommandContext commandContext) { } // if there is an available condition on the reactivation event listener, evaluate it to make sure the listener is available for reactivation - ReactivateEventListener reactivateEventListener = CaseDefinitionUtil.getCase(instance.getCaseDefinitionId()).getReactivateEventListener(); + Case caze = CaseDefinitionUtil.getCase(instance.getCaseDefinitionId()); + ReactivateEventListener reactivateEventListener = caze.getReactivateEventListener(); String availableConditionExpression = reactivateEventListener.getReactivationAvailableConditionExpression(); if (StringUtils.isNotEmpty(availableConditionExpression)) { Object listenerAvailable = CommandContextUtil.getCmmnEngineConfiguration() @@ -103,6 +111,32 @@ public CaseInstance execute(CommandContext commandContext) { if (cmmnEngineConfiguration.getIdentityLinkInterceptor() != null) { cmmnEngineConfiguration.getIdentityLinkInterceptor().handleReactivateCaseInstance(caseInstanceEntity); } + + if (!reactivationBuilder.getTerminatedPlanItemDefinitionIds().isEmpty()) { + for (String planItemDefinitionId : reactivationBuilder.getTerminatedPlanItemDefinitionIds()) { + PlanItem planItem = caze.getPlanModel().findPlanItemForPlanItemDefinitionInPlanFragmentOrDownwards(planItemDefinitionId); + if (planItem == null) { + throw new FlowableException("incorrect plan item definition id passed " + planItemDefinitionId); + } + + PlanItemInstanceEntityManager planItemInstanceEntityManager = cmmnEngineConfiguration.getPlanItemInstanceEntityManager(); + PlanItemInstanceEntity reactivatedPlanItemInstance = planItemInstanceEntityManager + .createPlanItemInstanceEntityBuilder() + .planItem(planItem) + .caseDefinitionId(caseInstanceEntity.getCaseDefinitionId()) + .caseInstanceId(caseInstanceEntity.getId()) + .tenantId(caseInstanceEntity.getTenantId()) + .create(); + + Date currentTime = cmmnEngineConfiguration.getClock().getCurrentTime(); + reactivatedPlanItemInstance.setState(PlanItemInstanceState.TERMINATED); + reactivatedPlanItemInstance.setEndedTime(currentTime); + reactivatedPlanItemInstance.setTerminatedTime(currentTime); + caseInstanceEntity.getChildPlanItemInstances().add(reactivatedPlanItemInstance); + + cmmnEngineConfiguration.getCmmnHistoryManager().recordPlanItemInstanceReactivated(reactivatedPlanItemInstance); + } + } // record the reactivation of the case in the history manager to also synchronize the new state and last reactivation data to the history cmmnEngineConfiguration.getCmmnHistoryManager().recordHistoricCaseInstanceReactivated(caseInstanceEntity); diff --git a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/reactivation/CaseReactivationBuilderImpl.java b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/reactivation/CaseReactivationBuilderImpl.java index 41d4a5082b7..e01df00e583 100644 --- a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/reactivation/CaseReactivationBuilderImpl.java +++ b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/reactivation/CaseReactivationBuilderImpl.java @@ -12,7 +12,9 @@ */ package org.flowable.cmmn.engine.impl.reactivation; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.flowable.cmmn.api.reactivation.CaseReactivationBuilder; @@ -30,6 +32,7 @@ public class CaseReactivationBuilderImpl implements CaseReactivationBuilder { protected final CommandExecutor commandExecutor; protected final String caseInstanceId; + protected List terminatedPlanItemDefinitionIds = new ArrayList<>(); protected Map variables; protected Map transientVariables; @@ -42,6 +45,10 @@ public String getCaseInstanceId() { return caseInstanceId; } + public List getTerminatedPlanItemDefinitionIds() { + return terminatedPlanItemDefinitionIds; + } + public boolean hasVariables() { return variables != null && variables.size() > 0; } @@ -58,6 +65,12 @@ public Map getTransientVariables() { return transientVariables; } + @Override + public CaseReactivationBuilder addTerminatedPlanItemInstanceForPlanItemDefinition(String planItemDefinitionId) { + this.terminatedPlanItemDefinitionIds.add(planItemDefinitionId); + return this; + } + @Override public CaseReactivationBuilder variable(String name, Object value) { if (variables == null) { diff --git a/modules/flowable-cmmn-engine/src/test/java/org/flowable/cmmn/test/reactivation/SimpleHistoricCaseReactivationTest.java b/modules/flowable-cmmn-engine/src/test/java/org/flowable/cmmn/test/reactivation/SimpleHistoricCaseReactivationTest.java index d87a741713f..910bdfcc96b 100644 --- a/modules/flowable-cmmn-engine/src/test/java/org/flowable/cmmn/test/reactivation/SimpleHistoricCaseReactivationTest.java +++ b/modules/flowable-cmmn-engine/src/test/java/org/flowable/cmmn/test/reactivation/SimpleHistoricCaseReactivationTest.java @@ -111,6 +111,85 @@ public void simpleMigrateCaseReactivationTest() { Authentication.setAuthenticatedUserId(previousUserId); } } + + @Test + @CmmnDeployment(resources = "org/flowable/cmmn/test/reactivation/Simple_Reactivation_Test_Case_No_Reactivation.cmmn.xml") + public void simpleMigrateCaseWithNoReactivationTest() { + String previousUserId = Authentication.getAuthenticatedUserId(); + org.flowable.cmmn.api.repository.CmmnDeployment deployment = null; + try { + Authentication.setAuthenticatedUserId("simpleCaseReactivationTest_user"); + final HistoricCaseInstance historicCase = createAndFinishSimpleCase("simpleReactivationTestCase"); + + deployment = cmmnRepositoryService.createDeployment() + .addClasspathResource("org/flowable/cmmn/test/reactivation/Simple_Reactivation_Test_Case_Added_Reactivation.cmmn.xml") + .deploy(); + + CaseDefinition newCaseDefinition = cmmnRepositoryService.createCaseDefinitionQuery() + .deploymentId(deployment.getId()) + .singleResult(); + + cmmnMigrationService.createHistoricCaseInstanceMigrationBuilder() + .migrateToCaseDefinition(newCaseDefinition.getId()) + .migrate(historicCase.getId()); + + HistoricCaseInstance historicCaseInstanceAfterMigration = cmmnHistoryService.createHistoricCaseInstanceQuery() + .caseInstanceId(historicCase.getId()) + .singleResult(); + assertThat(historicCaseInstanceAfterMigration.getCaseDefinitionId()).isEqualTo(newCaseDefinition.getId()); + assertThat(historicCaseInstanceAfterMigration.getCaseDefinitionKey()).isEqualTo("simpleReactivationTestCase"); + assertThat(historicCaseInstanceAfterMigration.getCaseDefinitionName()).isEqualTo("Simple Reactivation Test Case Added Reactivaton"); + assertThat(historicCaseInstanceAfterMigration.getCaseDefinitionVersion()).isEqualTo(2); + assertThat(historicCaseInstanceAfterMigration.getCaseDefinitionDeploymentId()).isEqualTo(newCaseDefinition.getDeploymentId()); + + CaseInstance reactivatedCase = cmmnHistoryService.createCaseReactivationBuilder(historicCase.getId()) + .addTerminatedPlanItemInstanceForPlanItemDefinition("reactivateEventListener1") + .addTerminatedPlanItemInstanceForPlanItemDefinition("humanTask3") + .reactivate(); + assertThat(reactivatedCase).isNotNull(); + + List planItemInstances = getAllPlanItemInstances(reactivatedCase.getId()); + assertThat(planItemInstances).isNotNull().hasSize(8); + + // we need to have two reactivation listeners by now, one in terminated state (from the first case completion) and the second one needs to be + // in completion state as we just triggered it for case reactivation + assertPlanItemInstanceState(planItemInstances, "Reactivate case", TERMINATED, COMPLETED); + + assertPlanItemInstanceState(planItemInstances, "Task C", TERMINATED, ACTIVE); + + assertCaseInstanceNotEnded(reactivatedCase); + + // the plan items must be equal for both the runtime as well as the history as of now + assertSamePlanItemState(reactivatedCase); + + // make sure we have exactly the same variables as the historic case + assertSameVariables(historicCase, reactivatedCase); + + List historicPlanItemInstances = cmmnHistoryService.createHistoricPlanItemInstanceQuery().list(); + assertThat(historicPlanItemInstances).isNotNull().hasSize(8); + + Map historicPlanItemInstanceMap = new HashMap<>(); + for (HistoricPlanItemInstance historicPlanItemInstance : historicPlanItemInstances) { + historicPlanItemInstanceMap.put(historicPlanItemInstance.getId(), historicPlanItemInstance); + } + + for (PlanItemInstance planItemInstance : planItemInstances) { + assertThat(historicPlanItemInstanceMap.containsKey(planItemInstance.getId())).isTrue(); + HistoricPlanItemInstance historicPlanItemInstance = historicPlanItemInstanceMap.get(planItemInstance.getId()); + assertThat(historicPlanItemInstance.getState()).isEqualTo(planItemInstance.getState()); + assertThat(historicPlanItemInstance.getElementId()).isEqualTo(planItemInstance.getElementId()); + } + + cmmnTaskService.complete(cmmnTaskService.createTaskQuery().caseInstanceId(reactivatedCase.getId()).singleResult().getId()); + assertCaseInstanceEnded(reactivatedCase); + + } finally { + if (deployment != null) { + CmmnTestHelper.deleteDeployment(cmmnEngineConfiguration, deployment.getId()); + } + Authentication.setAuthenticatedUserId(previousUserId); + } + } protected HistoricCaseInstance createAndFinishSimpleCase(String caseDefinitionKey) { CaseInstance caseInstance = cmmnRuntimeService.createCaseInstanceBuilder() diff --git a/modules/flowable-cmmn-engine/src/test/resources/org/flowable/cmmn/test/reactivation/Simple_Reactivation_Test_Case_Added_Reactivation.cmmn.xml b/modules/flowable-cmmn-engine/src/test/resources/org/flowable/cmmn/test/reactivation/Simple_Reactivation_Test_Case_Added_Reactivation.cmmn.xml new file mode 100644 index 00000000000..2e00f5a022e --- /dev/null +++ b/modules/flowable-cmmn-engine/src/test/resources/org/flowable/cmmn/test/reactivation/Simple_Reactivation_Test_Case_Added_Reactivation.cmmn.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + complete + + + + + occur + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/flowable-cmmn-engine/src/test/resources/org/flowable/cmmn/test/reactivation/Simple_Reactivation_Test_Case_No_Reactivation.cmmn.xml b/modules/flowable-cmmn-engine/src/test/resources/org/flowable/cmmn/test/reactivation/Simple_Reactivation_Test_Case_No_Reactivation.cmmn.xml new file mode 100644 index 00000000000..b02a0254f00 --- /dev/null +++ b/modules/flowable-cmmn-engine/src/test/resources/org/flowable/cmmn/test/reactivation/Simple_Reactivation_Test_Case_No_Reactivation.cmmn.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + complete + + + + + + + + + + + + + \ No newline at end of file