From da7a133fb16ca2b90a27ecb3a62639834f9cef34 Mon Sep 17 00:00:00 2001 From: davidfrigolet Date: Mon, 26 Jan 2026 07:58:40 +0000 Subject: [PATCH 1/2] feat: add steps support to templates --- .../core/preview/TemplatePreviewChange.java | 12 ++++++++ .../builder/TemplatePreviewTaskBuilder.java | 7 +++++ .../template/ChangeTemplateFileContent.java | 9 ++++++ .../executable/TemplateExecutableTask.java | 29 +++++++++++++++++++ .../task/loaded/TemplateLoadedChange.java | 7 +++++ .../loaded/TemplateLoadedTaskBuilder.java | 8 +++++ .../TemplateChangeTestDefinition.java | 1 + 7 files changed, 73 insertions(+) diff --git a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/TemplatePreviewChange.java b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/TemplatePreviewChange.java index b961a06a8..97756aa88 100644 --- a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/TemplatePreviewChange.java +++ b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/TemplatePreviewChange.java @@ -27,6 +27,7 @@ public class TemplatePreviewChange extends AbstractPreviewTask { private Object configuration; private Object apply; private Object rollback; + private Object steps; public TemplatePreviewChange() {} @@ -44,6 +45,7 @@ public TemplatePreviewChange(String fileName, Object configuration, Object apply, Object rollback, + Object steps, TargetSystemDescriptor targetSystem, RecoveryDescriptor recovery) { super(id, order, author, templateName, runAlways, transactional, system, targetSystem, recovery, false); @@ -52,6 +54,7 @@ public TemplatePreviewChange(String fileName, this.configuration = configuration; this.apply = apply; this.rollback = rollback; + this.steps = steps; } public String getFileName() { @@ -98,12 +101,21 @@ public void setRollback(Object rollback) { this.rollback = rollback; } + public Object getSteps() { + return steps; + } + + public void setSteps(Object steps) { + this.steps = steps; + } + @Override public String toString() { return "TemplatePreviewChange{" + "profiles=" + profiles + ", configuration=" + configuration + ", apply=" + apply + ", rollback=" + rollback + + ", steps=" + steps + ", id='" + id + '\'' + ", order='" + order + '\'' + ", author='" + author + '\'' + diff --git a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/builder/TemplatePreviewTaskBuilder.java b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/builder/TemplatePreviewTaskBuilder.java index 6334f05c9..c4542b78c 100644 --- a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/builder/TemplatePreviewTaskBuilder.java +++ b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/builder/TemplatePreviewTaskBuilder.java @@ -40,6 +40,7 @@ class TemplatePreviewTaskBuilder implements PreviewTaskBuilder changeTemplateInstance = (ChangeTemplate) instance; changeTemplateInstance.setTransactional(descriptor.isTransactional()); + changeTemplateInstance.setChangeId(descriptor.getId()); setExecutionData(executionRuntime, changeTemplateInstance, "Configuration"); setExecutionData(executionRuntime, changeTemplateInstance, "ApplyPayload"); setExecutionData(executionRuntime, changeTemplateInstance, "RollbackPayload"); + setStepsPayloadIfPresent(executionRuntime, changeTemplateInstance); executionRuntime.executeMethodWithInjectedDependencies(instance, method); } catch (Throwable ex) { throw new ChangeExecutionException(ex.getMessage(), this.getId(), ex); @@ -100,6 +102,33 @@ private Method getSetterMethod(Class changeTemplateClass, String methodName) } + /** + * Sets the steps payload on the template if the template supports it and steps data is present. + * This method uses reflection to call setStepsPayload if the template has such a method. + * Templates that don't support steps will simply not have this method called. + */ + private void setStepsPayloadIfPresent(ExecutionRuntime executionRuntime, + ChangeTemplate instance) { + Object stepsData = descriptor.getSteps(); + if (stepsData == null) { + return; + } + + Method setStepsMethod = Arrays.stream(instance.getClass().getMethods()) + .filter(m -> "setStepsPayload".equals(m.getName())) + .filter(m -> m.getParameterCount() == 1) + .findFirst() + .orElse(null); + + if (setStepsMethod != null) { + logger.debug("Setting steps payload for change[{}]", descriptor.getId()); + executionRuntime.executeMethodWithParameters(instance, setStepsMethod, stepsData); + } else { + logger.warn("Template[{}] has steps defined but doesn't support setStepsPayload method", + instance.getClass().getSimpleName()); + } + } + diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/TemplateLoadedChange.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/TemplateLoadedChange.java index 4bb077be4..c30e84a07 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/TemplateLoadedChange.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/TemplateLoadedChange.java @@ -35,6 +35,7 @@ public class TemplateLoadedChange extends AbstractLoadedChange { private final Object configuration; private final Object apply; private final Object rollback; + private final Object steps; TemplateLoadedChange(String changeFileName, String id, @@ -49,6 +50,7 @@ public class TemplateLoadedChange extends AbstractLoadedChange { Object configuration, Object apply, Object rollback, + Object steps, TargetSystemDescriptor targetSystem, RecoveryDescriptor recovery) { super(changeFileName, id, order, author, templateClass, constructor, runAlways, transactional, systemTask, targetSystem, recovery, false); @@ -57,6 +59,7 @@ public class TemplateLoadedChange extends AbstractLoadedChange { this.configuration = configuration; this.apply = apply; this.rollback = rollback; + this.steps = steps; } public Object getConfiguration() { @@ -71,6 +74,10 @@ public Object getRollback() { return rollback; } + public Object getSteps() { + return steps; + } + public List getProfiles() { return profiles; } diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/TemplateLoadedTaskBuilder.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/TemplateLoadedTaskBuilder.java index 6e850de35..dd0776896 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/TemplateLoadedTaskBuilder.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/TemplateLoadedTaskBuilder.java @@ -45,6 +45,7 @@ public class TemplateLoadedTaskBuilder implements LoadedTaskBuilder Date: Tue, 27 Jan 2026 15:56:07 +0000 Subject: [PATCH 2/2] refactor: template steps --- .../api/template/AbstractChangeTemplate.java | 29 ++++ .../api/template/ChangeTemplate.java | 18 +++ .../flamingock/api/template/TemplateStep.java | 125 ++++++++++++++++++ .../executable/TemplateExecutableTask.java | 81 +++++++++--- .../loaded/TemplateLoadedTaskBuilderTest.java | 10 ++ .../graalvm/RegistrationFeature.java | 2 + 6 files changed, 247 insertions(+), 18 deletions(-) create mode 100644 core/flamingock-core-api/src/main/java/io/flamingock/api/template/TemplateStep.java diff --git a/core/flamingock-core-api/src/main/java/io/flamingock/api/template/AbstractChangeTemplate.java b/core/flamingock-core-api/src/main/java/io/flamingock/api/template/AbstractChangeTemplate.java index 494ab02bd..4526c8508 100644 --- a/core/flamingock-core-api/src/main/java/io/flamingock/api/template/AbstractChangeTemplate.java +++ b/core/flamingock-core-api/src/main/java/io/flamingock/api/template/AbstractChangeTemplate.java @@ -20,6 +20,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.HashSet; +import java.util.List; import java.util.Set; @@ -31,8 +32,17 @@ public abstract class AbstractChangeTemplate> stepsPayload; private final Set> reflectiveClasses; @@ -56,6 +66,7 @@ public AbstractChangeTemplate(Class... additionalReflectiveClass) { reflectiveClasses.add(configurationClass); reflectiveClasses.add(applyPayloadClass); reflectiveClasses.add(rollbackPayloadClass); + reflectiveClasses.add(TemplateStep.class); } catch (ClassCastException e) { throw new IllegalStateException("Generic type arguments for a Template must be concrete types (classes, interfaces, or primitive wrappers like String, Integer, etc.): " + e.getMessage(), e); } catch (Exception e) { @@ -83,16 +94,34 @@ public void setConfiguration(SHARED_CONFIGURATION_FIELD configuration) { this.configuration = configuration; } + /** + * @deprecated Use {@link #setStepsPayload(List)} instead. Will be removed in a future release. + */ + @Deprecated @Override public void setApplyPayload(APPLY_FIELD applyPayload) { this.applyPayload = applyPayload; } + /** + * @deprecated Use {@link #setStepsPayload(List)} instead. Will be removed in a future release. + */ + @Deprecated @Override public void setRollbackPayload(ROLLBACK_FIELD rollbackPayload) { this.rollbackPayload = rollbackPayload; } + @Override + public void setStepsPayload(List> stepsPayload) { + this.stepsPayload = stepsPayload; + } + + @Override + public List> getStepsPayload() { + return stepsPayload; + } + @Override public Class getConfigurationClass() { return configurationClass; diff --git a/core/flamingock-core-api/src/main/java/io/flamingock/api/template/ChangeTemplate.java b/core/flamingock-core-api/src/main/java/io/flamingock/api/template/ChangeTemplate.java index 1bbeee6bc..fe9968f5c 100644 --- a/core/flamingock-core-api/src/main/java/io/flamingock/api/template/ChangeTemplate.java +++ b/core/flamingock-core-api/src/main/java/io/flamingock/api/template/ChangeTemplate.java @@ -15,6 +15,8 @@ */ package io.flamingock.api.template; +import java.util.List; + /** * Interface representing a reusable change template with configuration of type {@code CONFIG}. * @@ -29,10 +31,26 @@ public interface ChangeTemplate> stepsPayload); + + List> getStepsPayload(); + + default boolean hasStepsPayload() { + return getStepsPayload() != null && !getStepsPayload().isEmpty(); + } + Class getConfigurationClass(); Class getApplyPayloadClass(); diff --git a/core/flamingock-core-api/src/main/java/io/flamingock/api/template/TemplateStep.java b/core/flamingock-core-api/src/main/java/io/flamingock/api/template/TemplateStep.java new file mode 100644 index 000000000..7ee3192f2 --- /dev/null +++ b/core/flamingock-core-api/src/main/java/io/flamingock/api/template/TemplateStep.java @@ -0,0 +1,125 @@ +/* + * Copyright 2025 Flamingock (https://www.flamingock.io) + * + * 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 io.flamingock.api.template; + +/** + * Represents a single step in a step-based change template. + * + *

Each step contains an {@code apply} operation that executes during the forward + * migration, and an optional {@code rollback} operation that executes if the step + * or a subsequent step fails.

+ * + *

YAML Structure

+ *
{@code
+ * steps:
+ *   - apply:
+ *       type: createCollection
+ *       collection: users
+ *     rollback:
+ *       type: dropCollection
+ *       collection: users
+ *   - apply:
+ *       type: insert
+ *       collection: users
+ *       parameters:
+ *         documents:
+ *           - name: "John"
+ *     rollback:
+ *       type: delete
+ *       collection: users
+ *       parameters:
+ *         filter: {}
+ * }
+ * + *

Rollback Behavior

+ *
    + *
  • Rollback is optional - steps without rollback are skipped during rollback
  • + *
  • When a step fails, all previously successful steps are rolled back in reverse order
  • + *
  • Rollback errors are logged but don't stop the rollback process
  • + *
+ * + * @param the type of the apply payload + * @param the type of the rollback payload + */ +public class TemplateStep { + + private APPLY apply; + private ROLLBACK rollback; + + public TemplateStep() { + } + + public TemplateStep(APPLY apply, ROLLBACK rollback) { + this.apply = apply; + this.rollback = rollback; + } + + /** + * Returns the apply payload for this step. + * + * @return the apply payload (required) + */ + public APPLY getApply() { + return apply; + } + + /** + * Sets the apply payload for this step. + * + * @param apply the apply payload + */ + public void setApply(APPLY apply) { + this.apply = apply; + } + + /** + * Returns the rollback payload for this step. + * + * @return the rollback payload, or null if no rollback is defined + */ + public ROLLBACK getRollback() { + return rollback; + } + + /** + * Sets the rollback payload for this step. + * + * @param rollback the rollback payload (optional) + */ + public void setRollback(ROLLBACK rollback) { + this.rollback = rollback; + } + + /** + * Checks if this step has a rollback payload defined. + * + * @return true if a rollback payload is defined + */ + public boolean hasRollback() { + return rollback != null; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("TemplateStep{"); + sb.append("apply=").append(apply); + if (rollback != null) { + sb.append(", rollback=").append(rollback); + } + sb.append('}'); + return sb.toString(); + } +} diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/TemplateExecutableTask.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/TemplateExecutableTask.java index fe10d7ac7..b556fa170 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/TemplateExecutableTask.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/TemplateExecutableTask.java @@ -16,6 +16,7 @@ package io.flamingock.internal.core.task.executable; import io.flamingock.api.template.ChangeTemplate; +import io.flamingock.api.template.TemplateStep; import io.flamingock.internal.common.core.error.ChangeExecutionException; import io.flamingock.internal.core.runtime.ExecutionRuntime; import io.flamingock.internal.core.task.loaded.TemplateLoadedChange; @@ -25,7 +26,10 @@ import org.slf4j.Logger; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; +import java.util.Map; public class TemplateExecutableTask extends ReflectionExecutableTask { private final Logger logger = FlamingockLoggerFactory.getLogger("TemplateTask"); @@ -50,7 +54,7 @@ protected void executeInternal(ExecutionRuntime executionRuntime, Method method setExecutionData(executionRuntime, changeTemplateInstance, "Configuration"); setExecutionData(executionRuntime, changeTemplateInstance, "ApplyPayload"); setExecutionData(executionRuntime, changeTemplateInstance, "RollbackPayload"); - setStepsPayloadIfPresent(executionRuntime, changeTemplateInstance); + setStepsIfPresent(executionRuntime, changeTemplateInstance); executionRuntime.executeMethodWithInjectedDependencies(instance, method); } catch (Throwable ex) { throw new ChangeExecutionException(ex.getMessage(), this.getId(), ex); @@ -103,30 +107,71 @@ private Method getSetterMethod(Class changeTemplateClass, String methodName) } /** - * Sets the steps payload on the template if the template supports it and steps data is present. - * This method uses reflection to call setStepsPayload if the template has such a method. - * Templates that don't support steps will simply not have this method called. + * Sets the steps on the template if steps data is present. + * Converts raw step data (List of Maps) to List of TemplateStep using the template's payload classes. */ - private void setStepsPayloadIfPresent(ExecutionRuntime executionRuntime, - ChangeTemplate instance) { + @SuppressWarnings({"unchecked", "rawtypes"}) + private void setStepsIfPresent(ExecutionRuntime executionRuntime, + ChangeTemplate instance) { Object stepsData = descriptor.getSteps(); if (stepsData == null) { return; } - Method setStepsMethod = Arrays.stream(instance.getClass().getMethods()) - .filter(m -> "setStepsPayload".equals(m.getName())) - .filter(m -> m.getParameterCount() == 1) - .findFirst() - .orElse(null); - - if (setStepsMethod != null) { - logger.debug("Setting steps payload for change[{}]", descriptor.getId()); - executionRuntime.executeMethodWithParameters(instance, setStepsMethod, stepsData); - } else { - logger.warn("Template[{}] has steps defined but doesn't support setStepsPayload method", - instance.getClass().getSimpleName()); + logger.debug("Setting steps for change[{}]", descriptor.getId()); + + List convertedSteps = convertToTemplateSteps( + stepsData, + instance.getApplyPayloadClass(), + instance.getRollbackPayloadClass() + ); + + Method setStepsMethod = getSetterMethod(instance.getClass(), "setStepsPayload"); + executionRuntime.executeMethodWithParameters(instance, setStepsMethod, convertedSteps); + } + + /** + * Converts raw step data (List of Maps from YAML) to a list of TemplateStep objects. + * + * @param stepsData the raw steps data (expected to be a List of Maps) + * @param applyClass the class type for apply payloads + * @param rollbackClass the class type for rollback payloads + * @return list of converted TemplateStep objects + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + private List convertToTemplateSteps(Object stepsData, + Class applyClass, + Class rollbackClass) { + List result = new ArrayList<>(); + + if (!(stepsData instanceof List)) { + logger.warn("Steps data is not a List, ignoring"); + return result; } + + List stepsList = (List) stepsData; + for (Object stepItem : stepsList) { + if (stepItem instanceof Map) { + Map stepMap = (Map) stepItem; + TemplateStep step = new TemplateStep(); + + Object applyData = stepMap.get("apply"); + if (applyData != null && Void.class != applyClass) { + step.setApply(FileUtil.getFromMap(applyClass, applyData)); + } + + Object rollbackData = stepMap.get("rollback"); + if (rollbackData != null && Void.class != rollbackClass) { + step.setRollback(FileUtil.getFromMap(rollbackClass, rollbackData)); + } + + result.add(step); + } else if (stepItem instanceof TemplateStep) { + result.add((TemplateStep) stepItem); + } + } + + return result; } diff --git a/core/flamingock-core/src/test/java/io/flamingock/internal/core/task/loaded/TemplateLoadedTaskBuilderTest.java b/core/flamingock-core/src/test/java/io/flamingock/internal/core/task/loaded/TemplateLoadedTaskBuilderTest.java index 7940ebdf5..9fdb5b8b4 100644 --- a/core/flamingock-core/src/test/java/io/flamingock/internal/core/task/loaded/TemplateLoadedTaskBuilderTest.java +++ b/core/flamingock-core/src/test/java/io/flamingock/internal/core/task/loaded/TemplateLoadedTaskBuilderTest.java @@ -18,6 +18,7 @@ import io.flamingock.internal.common.core.error.FlamingockException; import io.flamingock.internal.common.core.template.ChangeTemplateManager; import io.flamingock.api.template.ChangeTemplate; +import io.flamingock.api.template.TemplateStep; import io.flamingock.api.annotations.Apply; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -27,6 +28,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.List; import java.util.Optional; import static org.junit.jupiter.api.Assertions.*; @@ -39,6 +41,8 @@ class TemplateLoadedTaskBuilderTest { // Simple test template implementation public static class TestChangeTemplate implements ChangeTemplate { + private List> stepsPayload; + @Override public void setChangeId(String changeId) {} @@ -54,6 +58,12 @@ public void setApplyPayload(Object applyPayload) {} @Override public void setRollbackPayload(Object rollbackPayload) {} + @Override + public void setStepsPayload(List> stepsPayload) { this.stepsPayload = stepsPayload; } + + @Override + public List> getStepsPayload() { return stepsPayload; } + @Override public Class getConfigurationClass() { return Object.class; } diff --git a/core/flamingock-graalvm/src/main/java/io/flamingock/graalvm/RegistrationFeature.java b/core/flamingock-graalvm/src/main/java/io/flamingock/graalvm/RegistrationFeature.java index a5ce4a3c5..cab5ae879 100644 --- a/core/flamingock-graalvm/src/main/java/io/flamingock/graalvm/RegistrationFeature.java +++ b/core/flamingock-graalvm/src/main/java/io/flamingock/graalvm/RegistrationFeature.java @@ -17,6 +17,7 @@ import io.flamingock.api.template.AbstractChangeTemplate; import io.flamingock.api.template.ChangeTemplate; +import io.flamingock.api.template.TemplateStep; import io.flamingock.internal.common.core.metadata.FlamingockMetadata; import io.flamingock.internal.common.core.preview.*; import io.flamingock.internal.common.core.task.AbstractTaskDescriptor; @@ -142,6 +143,7 @@ private void registerTemplates() { registerClassForReflection(ChangeTemplateManager.class); registerClassForReflection(ChangeTemplate.class); registerClassForReflection(AbstractChangeTemplate.class); + registerClassForReflection(TemplateStep.class); ChangeTemplateManager.getTemplates().forEach(template -> { registerClassForReflection(template.getClass()); template.getReflectiveClasses().forEach(RegistrationFeature::registerClassForReflection);