From 919ebfa7e3047915cc1494d74b203a4a4990e9e5 Mon Sep 17 00:00:00 2001 From: Roman Saratz Date: Mon, 29 Jun 2026 15:59:30 +0200 Subject: [PATCH] Pass the source definition to migration callbacks CaseInstanceMigrationCallback and ProcessInstanceMigrationCallback previously only received the definition an instance was migrated *to*. They now also receive the definition it was migrated *from*, so listeners can compare source and target (e.g. version-aware auditing or reacting to specific upgrade paths). This is done backwards compatibly via a new default overload on each callback method that takes the source definition as an extra parameter and delegates to the existing method. Existing implementations keep working unchanged; the engines now invoke the new overloads: - CaseInstanceMigrationCallback#caseInstanceMigrated and #historicCaseInstanceMigrated gain a sourceCaseDefinition parameter. - ProcessInstanceMigrationCallback#processInstanceMigrated gains a sourceProcessDefinition parameter. The source definition is resolved from the instance's pre-migration definition id, captured before the reference is overwritten, and only resolved when at least one callback is registered (no overhead otherwise). Adds CaseInstanceMigrationCallbackTest and ProcessInstanceMigrationCallbackTest covering the source definition on runtime and historic migrations, plus that callbacks implementing only the original methods are still invoked. --- .../CaseInstanceMigrationCallback.java | 14 +- .../CaseInstanceMigrationManagerImpl.java | 7 +- .../CaseInstanceMigrationCallbackTest.java | 186 ++++++++++++++++++ .../ProcessInstanceMigrationManagerImpl.java | 4 +- .../ProcessInstanceMigrationCallback.java | 7 +- .../ProcessInstanceMigrationCallbackTest.java | 135 +++++++++++++ 6 files changed, 347 insertions(+), 6 deletions(-) create mode 100644 modules/flowable-cmmn-engine/src/test/java/org/flowable/cmmn/test/migration/CaseInstanceMigrationCallbackTest.java create mode 100644 modules/flowable-engine/src/test/java/org/flowable/engine/test/api/runtime/migration/ProcessInstanceMigrationCallbackTest.java diff --git a/modules/flowable-cmmn-api/src/main/java/org/flowable/cmmn/api/migration/CaseInstanceMigrationCallback.java b/modules/flowable-cmmn-api/src/main/java/org/flowable/cmmn/api/migration/CaseInstanceMigrationCallback.java index bd6c82c88a2..13f4880a74d 100644 --- a/modules/flowable-cmmn-api/src/main/java/org/flowable/cmmn/api/migration/CaseInstanceMigrationCallback.java +++ b/modules/flowable-cmmn-api/src/main/java/org/flowable/cmmn/api/migration/CaseInstanceMigrationCallback.java @@ -19,7 +19,17 @@ public interface CaseInstanceMigrationCallback { void caseInstanceMigrated(CaseInstance caseInstance, CaseDefinition caseDefToMigrateTo, CaseInstanceMigrationDocument document); - + + default void caseInstanceMigrated(CaseInstance caseInstance, CaseDefinition sourceCaseDefinition, CaseDefinition caseDefToMigrateTo, + CaseInstanceMigrationDocument document) { + caseInstanceMigrated(caseInstance, caseDefToMigrateTo, document); + } + void historicCaseInstanceMigrated(HistoricCaseInstance caseInstance, CaseDefinition caseDefToMigrateTo, HistoricCaseInstanceMigrationDocument document); - + + default void historicCaseInstanceMigrated(HistoricCaseInstance caseInstance, CaseDefinition sourceCaseDefinition, CaseDefinition caseDefToMigrateTo, + HistoricCaseInstanceMigrationDocument document) { + historicCaseInstanceMigrated(caseInstance, caseDefToMigrateTo, document); + } + } diff --git a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/migration/CaseInstanceMigrationManagerImpl.java b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/migration/CaseInstanceMigrationManagerImpl.java index a32367a523c..9881f8345f4 100644 --- a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/migration/CaseInstanceMigrationManagerImpl.java +++ b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/migration/CaseInstanceMigrationManagerImpl.java @@ -330,8 +330,9 @@ protected void doMigrateCaseInstance(CaseInstanceEntity caseInstance, CaseDefini List migrationCallbacks = CommandContextUtil.getCmmnEngineConfiguration(commandContext).getCaseInstanceMigrationCallbacks(); if (migrationCallbacks != null && !migrationCallbacks.isEmpty()) { + CaseDefinition sourceCaseDefinition = CaseDefinitionUtil.getCaseDefinition(originalCaseDefinitionId); for (CaseInstanceMigrationCallback caseInstanceMigrationCallback : migrationCallbacks) { - caseInstanceMigrationCallback.caseInstanceMigrated(caseInstance, caseDefinitionToMigrateTo, document); + caseInstanceMigrationCallback.caseInstanceMigrated(caseInstance, sourceCaseDefinition, caseDefinitionToMigrateTo, document); } } @@ -359,6 +360,7 @@ protected void doMigrateHistoricCaseInstance(HistoricCaseInstanceEntity historic } LOGGER.debug("Updating case definition reference of case root execution with id:'{}' to '{}'", historicCaseInstance.getId(), caseDefinitionToMigrateTo.getId()); + String originalCaseDefinitionId = historicCaseInstance.getCaseDefinitionId(); historicCaseInstance.setCaseDefinitionId(caseDefinitionToMigrateTo.getId()); historicCaseInstance.setCaseDefinitionKey(caseDefinitionToMigrateTo.getKey()); historicCaseInstance.setCaseDefinitionName(caseDefinitionToMigrateTo.getName()); @@ -371,8 +373,9 @@ protected void doMigrateHistoricCaseInstance(HistoricCaseInstanceEntity historic List migrationCallbacks = CommandContextUtil.getCmmnEngineConfiguration(commandContext).getCaseInstanceMigrationCallbacks(); if (migrationCallbacks != null && !migrationCallbacks.isEmpty()) { + CaseDefinition sourceCaseDefinition = CaseDefinitionUtil.getCaseDefinition(originalCaseDefinitionId); for (CaseInstanceMigrationCallback caseInstanceMigrationCallback : migrationCallbacks) { - caseInstanceMigrationCallback.historicCaseInstanceMigrated(historicCaseInstance, caseDefinitionToMigrateTo, document); + caseInstanceMigrationCallback.historicCaseInstanceMigrated(historicCaseInstance, sourceCaseDefinition, caseDefinitionToMigrateTo, document); } } } diff --git a/modules/flowable-cmmn-engine/src/test/java/org/flowable/cmmn/test/migration/CaseInstanceMigrationCallbackTest.java b/modules/flowable-cmmn-engine/src/test/java/org/flowable/cmmn/test/migration/CaseInstanceMigrationCallbackTest.java new file mode 100644 index 00000000000..aa1b99cb2bb --- /dev/null +++ b/modules/flowable-cmmn-engine/src/test/java/org/flowable/cmmn/test/migration/CaseInstanceMigrationCallbackTest.java @@ -0,0 +1,186 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.flowable.cmmn.test.migration; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.flowable.cmmn.api.history.HistoricCaseInstance; +import org.flowable.cmmn.api.migration.CaseInstanceMigrationCallback; +import org.flowable.cmmn.api.migration.CaseInstanceMigrationDocument; +import org.flowable.cmmn.api.migration.HistoricCaseInstanceMigrationDocument; +import org.flowable.cmmn.api.repository.CaseDefinition; +import org.flowable.cmmn.api.runtime.CaseInstance; +import org.flowable.task.api.Task; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +/** + * Tests that {@link CaseInstanceMigrationCallback} receives the source case definition the instance was migrated from, + * and that callbacks implementing only the deprecated methods are still invoked (backwards compatibility). + */ +public class CaseInstanceMigrationCallbackTest extends AbstractCaseMigrationTest { + + protected static final String ONE_TASK_CASE = "org/flowable/cmmn/test/migration/one-task.cmmn.xml"; + + @AfterEach + void resetMigrationCallbacks() { + cmmnEngineConfiguration.setCaseInstanceMigrationCallbacks(null); + } + + @Test + void runtimeMigrationProvidesSourceCaseDefinition() { + RecordingCallback callback = new RecordingCallback(); + cmmnEngineConfiguration.setCaseInstanceMigrationCallbacks(Collections.singletonList(callback)); + + CaseDefinition sourceDefinition = deployCaseDefinition("test1", ONE_TASK_CASE); + CaseInstance caseInstance = cmmnRuntimeService.createCaseInstanceBuilder().caseDefinitionKey("testCase").start(); + CaseDefinition destinationDefinition = deployCaseDefinition("test1", ONE_TASK_CASE); + + cmmnMigrationService.createCaseInstanceMigrationBuilder() + .migrateToCaseDefinition(destinationDefinition.getId()) + .migrate(caseInstance.getId()); + + assertThat(callback.getRuntimeMigrations()).hasSize(1); + RecordedMigration migration = callback.getRuntimeMigrations().get(0); + assertThat(migration.getSource().getId()).isEqualTo(sourceDefinition.getId()); + assertThat(migration.getSource().getVersion()).isEqualTo(1); + assertThat(migration.getTarget().getId()).isEqualTo(destinationDefinition.getId()); + assertThat(migration.getTarget().getVersion()).isEqualTo(2); + } + + @Test + void historicMigrationProvidesSourceCaseDefinition() { + RecordingCallback callback = new RecordingCallback(); + cmmnEngineConfiguration.setCaseInstanceMigrationCallbacks(Collections.singletonList(callback)); + + CaseDefinition sourceDefinition = deployCaseDefinition("test1", ONE_TASK_CASE); + CaseInstance caseInstance = cmmnRuntimeService.createCaseInstanceBuilder().caseDefinitionKey("testCase").start(); + CaseDefinition destinationDefinition = deployCaseDefinition("test1", ONE_TASK_CASE); + + Task task = cmmnTaskService.createTaskQuery().caseInstanceId(caseInstance.getId()).singleResult(); + cmmnTaskService.complete(task.getId()); + + cmmnMigrationService.createHistoricCaseInstanceMigrationBuilder() + .migrateToCaseDefinition(destinationDefinition.getId()) + .migrate(caseInstance.getId()); + + assertThat(callback.getHistoricMigrations()).hasSize(1); + RecordedMigration migration = callback.getHistoricMigrations().get(0); + assertThat(migration.getSource().getId()).isEqualTo(sourceDefinition.getId()); + assertThat(migration.getSource().getVersion()).isEqualTo(1); + assertThat(migration.getTarget().getId()).isEqualTo(destinationDefinition.getId()); + assertThat(migration.getTarget().getVersion()).isEqualTo(2); + } + + @Test + void legacyCallbackStillInvokedForRuntimeMigration() { + LegacyRecordingCallback callback = new LegacyRecordingCallback(); + cmmnEngineConfiguration.setCaseInstanceMigrationCallbacks(Collections.singletonList(callback)); + + deployCaseDefinition("test1", ONE_TASK_CASE); + CaseInstance caseInstance = cmmnRuntimeService.createCaseInstanceBuilder().caseDefinitionKey("testCase").start(); + CaseDefinition destinationDefinition = deployCaseDefinition("test1", ONE_TASK_CASE); + + cmmnMigrationService.createCaseInstanceMigrationBuilder() + .migrateToCaseDefinition(destinationDefinition.getId()) + .migrate(caseInstance.getId()); + + assertThat(callback.getRuntimeTargetDefinitionIds()).containsExactly(destinationDefinition.getId()); + } + + static class RecordedMigration { + + private final CaseDefinition source; + private final CaseDefinition target; + + RecordedMigration(CaseDefinition source, CaseDefinition target) { + this.source = source; + this.target = target; + } + + public CaseDefinition getSource() { + return source; + } + + public CaseDefinition getTarget() { + return target; + } + } + + static class RecordingCallback implements CaseInstanceMigrationCallback { + + private final List runtimeMigrations = new ArrayList<>(); + private final List historicMigrations = new ArrayList<>(); + + @Override + public void caseInstanceMigrated(CaseInstance caseInstance, CaseDefinition caseDefToMigrateTo, CaseInstanceMigrationDocument document) { + } + + @Override + public void historicCaseInstanceMigrated(HistoricCaseInstance caseInstance, CaseDefinition caseDefToMigrateTo, + HistoricCaseInstanceMigrationDocument document) { + } + + @Override + public void caseInstanceMigrated(CaseInstance caseInstance, CaseDefinition sourceCaseDefinition, + CaseDefinition caseDefToMigrateTo, CaseInstanceMigrationDocument document) { + runtimeMigrations.add(new RecordedMigration(sourceCaseDefinition, caseDefToMigrateTo)); + } + + @Override + public void historicCaseInstanceMigrated(HistoricCaseInstance caseInstance, CaseDefinition sourceCaseDefinition, + CaseDefinition caseDefToMigrateTo, HistoricCaseInstanceMigrationDocument document) { + historicMigrations.add(new RecordedMigration(sourceCaseDefinition, caseDefToMigrateTo)); + } + + public List getRuntimeMigrations() { + return runtimeMigrations; + } + + public List getHistoricMigrations() { + return historicMigrations; + } + } + + /** Implements only the old methods to verify the new overloads delegate to them. */ + static class LegacyRecordingCallback implements CaseInstanceMigrationCallback { + + private final List runtimeTargetDefinitionIds = new ArrayList<>(); + private final List historicTargetDefinitionIds = new ArrayList<>(); + + @Override + public void caseInstanceMigrated(CaseInstance caseInstance, CaseDefinition caseDefToMigrateTo, CaseInstanceMigrationDocument document) { + runtimeTargetDefinitionIds.add(caseDefToMigrateTo.getId()); + } + + @Override + public void historicCaseInstanceMigrated(HistoricCaseInstance caseInstance, CaseDefinition caseDefToMigrateTo, + HistoricCaseInstanceMigrationDocument document) { + historicTargetDefinitionIds.add(caseDefToMigrateTo.getId()); + } + + public List getRuntimeTargetDefinitionIds() { + return runtimeTargetDefinitionIds; + } + + public List getHistoricTargetDefinitionIds() { + return historicTargetDefinitionIds; + } + } + +} diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/migration/ProcessInstanceMigrationManagerImpl.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/migration/ProcessInstanceMigrationManagerImpl.java index 7f6355ed7dc..ff55892678e 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/migration/ProcessInstanceMigrationManagerImpl.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/migration/ProcessInstanceMigrationManagerImpl.java @@ -449,6 +449,7 @@ protected void doMigrateProcessInstance(ProcessInstance processInstance, Process procDefToMigrateTo, document, commandContext); LOGGER.debug("Updating Process definition reference of process root execution with id:'{}' to '{}'", processInstance.getId(), procDefToMigrateTo.getId()); + String originalProcessDefinitionId = processInstanceEntity.getProcessDefinitionId(); processInstanceEntity.setProcessDefinitionId(procDefToMigrateTo.getId()); LOGGER.debug("Resolve activity executions to migrate"); @@ -500,8 +501,9 @@ protected void doMigrateProcessInstance(ProcessInstance processInstance, Process List migrationCallbacks = CommandContextUtil.getProcessEngineConfiguration(commandContext).getProcessInstanceMigrationCallbacks(); if (migrationCallbacks != null && !migrationCallbacks.isEmpty()) { + ProcessDefinition sourceProcessDefinition = ProcessDefinitionUtil.getProcessDefinition(originalProcessDefinitionId); for (ProcessInstanceMigrationCallback processInstanceMigrationCallback : migrationCallbacks) { - processInstanceMigrationCallback.processInstanceMigrated(processInstance, procDefToMigrateTo, document, commandContext); + processInstanceMigrationCallback.processInstanceMigrated(processInstance, sourceProcessDefinition, procDefToMigrateTo, document, commandContext); } } diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/migration/ProcessInstanceMigrationCallback.java b/modules/flowable-engine/src/main/java/org/flowable/engine/migration/ProcessInstanceMigrationCallback.java index aaebcf1a9b8..b203c41bd3d 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/migration/ProcessInstanceMigrationCallback.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/migration/ProcessInstanceMigrationCallback.java @@ -20,5 +20,10 @@ public interface ProcessInstanceMigrationCallback { void processInstanceMigrated(ProcessInstance processInstance, ProcessDefinition procDefToMigrateTo, ProcessInstanceMigrationDocument document, CommandContext commandContext); - + + default void processInstanceMigrated(ProcessInstance processInstance, ProcessDefinition sourceProcessDefinition, + ProcessDefinition procDefToMigrateTo, ProcessInstanceMigrationDocument document, CommandContext commandContext) { + processInstanceMigrated(processInstance, procDefToMigrateTo, document, commandContext); + } + } diff --git a/modules/flowable-engine/src/test/java/org/flowable/engine/test/api/runtime/migration/ProcessInstanceMigrationCallbackTest.java b/modules/flowable-engine/src/test/java/org/flowable/engine/test/api/runtime/migration/ProcessInstanceMigrationCallbackTest.java new file mode 100644 index 00000000000..908e05198bb --- /dev/null +++ b/modules/flowable-engine/src/test/java/org/flowable/engine/test/api/runtime/migration/ProcessInstanceMigrationCallbackTest.java @@ -0,0 +1,135 @@ +/* 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.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.flowable.common.engine.impl.interceptor.CommandContext; +import org.flowable.engine.migration.ProcessInstanceMigrationCallback; +import org.flowable.engine.migration.ProcessInstanceMigrationDocument; +import org.flowable.engine.repository.ProcessDefinition; +import org.flowable.engine.runtime.ProcessInstance; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +/** + * Tests that {@link ProcessInstanceMigrationCallback} receives the source process definition the instance was migrated + * from, and that a callback implementing only the deprecated method is still invoked (backwards compatibility). + */ +public class ProcessInstanceMigrationCallbackTest extends AbstractProcessInstanceMigrationTest { + + protected static final String ONE_TASK_PROCESS = "org/flowable/engine/test/api/runtime/migration/one-task-simple-process.bpmn20.xml"; + + @AfterEach + void resetMigrationCallbacks() { + processEngineConfiguration.setProcessInstanceMigrationCallbacks(null); + deleteDeployments(); + } + + @Test + public void migrationProvidesSourceProcessDefinition() { + RecordingCallback callback = new RecordingCallback(); + processEngineConfiguration.setProcessInstanceMigrationCallbacks(Collections.singletonList(callback)); + + ProcessDefinition sourceDefinition = deployProcessDefinition("my deploy", ONE_TASK_PROCESS); + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("MP"); + ProcessDefinition destinationDefinition = deployProcessDefinition("my deploy", ONE_TASK_PROCESS); + + processMigrationService.createProcessInstanceMigrationBuilder() + .migrateToProcessDefinition(destinationDefinition.getId()) + .migrate(processInstance.getId()); + + assertThat(callback.getMigrations()).hasSize(1); + RecordedMigration migration = callback.getMigrations().get(0); + assertThat(migration.getSource().getId()).isEqualTo(sourceDefinition.getId()); + assertThat(migration.getSource().getVersion()).isEqualTo(1); + assertThat(migration.getTarget().getId()).isEqualTo(destinationDefinition.getId()); + assertThat(migration.getTarget().getVersion()).isEqualTo(2); + } + + @Test + public void legacyCallbackStillInvoked() { + LegacyRecordingCallback callback = new LegacyRecordingCallback(); + processEngineConfiguration.setProcessInstanceMigrationCallbacks(Collections.singletonList(callback)); + + deployProcessDefinition("my deploy", ONE_TASK_PROCESS); + ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("MP"); + ProcessDefinition destinationDefinition = deployProcessDefinition("my deploy", ONE_TASK_PROCESS); + + processMigrationService.createProcessInstanceMigrationBuilder() + .migrateToProcessDefinition(destinationDefinition.getId()) + .migrate(processInstance.getId()); + + assertThat(callback.getTargetDefinitionIds()).containsExactly(destinationDefinition.getId()); + } + + static class RecordedMigration { + + private final ProcessDefinition source; + private final ProcessDefinition target; + + RecordedMigration(ProcessDefinition source, ProcessDefinition target) { + this.source = source; + this.target = target; + } + + public ProcessDefinition getSource() { + return source; + } + + public ProcessDefinition getTarget() { + return target; + } + } + + static class RecordingCallback implements ProcessInstanceMigrationCallback { + + private final List migrations = new ArrayList<>(); + + @Override + public void processInstanceMigrated(ProcessInstance processInstance, ProcessDefinition procDefToMigrateTo, + ProcessInstanceMigrationDocument document, CommandContext commandContext) { + } + + @Override + public void processInstanceMigrated(ProcessInstance processInstance, ProcessDefinition sourceProcessDefinition, + ProcessDefinition procDefToMigrateTo, ProcessInstanceMigrationDocument document, CommandContext commandContext) { + migrations.add(new RecordedMigration(sourceProcessDefinition, procDefToMigrateTo)); + } + + public List getMigrations() { + return migrations; + } + } + + /** Implements only the deprecated method to verify the new overload delegates to it. */ + static class LegacyRecordingCallback implements ProcessInstanceMigrationCallback { + + private final List targetDefinitionIds = new ArrayList<>(); + + @Override + public void processInstanceMigrated(ProcessInstance processInstance, ProcessDefinition procDefToMigrateTo, + ProcessInstanceMigrationDocument document, CommandContext commandContext) { + targetDefinitionIds.add(procDefToMigrateTo.getId()); + } + + public List getTargetDefinitionIds() { + return targetDefinitionIds; + } + } + +}