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-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-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() { 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-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() { 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 2d437aa0eb1..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 @@ -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/db/EngineSqlScriptBasedDbSchemaManager.java b/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/db/EngineSqlScriptBasedDbSchemaManager.java index f15ccc3c334..e987a1b7b35 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 @@ -189,6 +189,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)) { @@ -198,22 +202,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); 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/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/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; 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 + + + + + + + + + +