diff --git a/CLAUDE.md b/CLAUDE.md index 593f290f3..17d89a54e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -338,11 +338,15 @@ YAML File ↓ (parsing) ChangeTemplateFileContent ↓ (preview building) -TemplatePreviewChange +TemplatePreviewChange (unified) ↓ (loaded task building - template lookup from registry) -TemplateLoadedChange +AbstractTemplateLoadedChange + ├── SimpleTemplateLoadedChange (for AbstractSimpleTemplate) + └── SteppableTemplateLoadedChange (for AbstractSteppableTemplate) ↓ (execution preparation) -TemplateExecutableTask +TemplateExecutableTask + ├── SimpleTemplateExecutableTask (calls setStep()) + └── SteppableTemplateExecutableTask (calls setSteps()) ↓ (runtime execution) Template instance with injected dependencies ``` @@ -350,8 +354,9 @@ Template instance with injected dependencies **Key Classes in Flow**: - `ChangeTemplateFileContent` - YAML parsed data (`core/flamingock-core-commons`) - `TemplatePreviewTaskBuilder` - Builds preview from file content (`core/flamingock-core-commons`) -- `TemplateLoadedTaskBuilder` - Resolves template class, builds loaded change (`core/flamingock-core`) -- `TemplateExecutableTask` - Executes template with dependency injection (`core/flamingock-core`) +- `TemplateLoadedTaskBuilder` - Resolves template class, builds type-specific loaded change (`core/flamingock-core`) +- `TemplateExecutableTaskBuilder` - Builds type-specific executable task (`core/flamingock-core`) +- `TemplateExecutableTask` - Abstract base for template execution (`core/flamingock-core`) ### Discovery Mechanism (SPI) @@ -430,6 +435,50 @@ Templates are validated at compile-time to ensure YAML structure matches the tem - `io.flamingock.api.annotations.EnableFlamingock` - strictTemplateValidation flag - `io.flamingock.api.template.AbstractChangeTemplate` - template base classes +<<<<<<< Updated upstream +======= +### Template Class Hierarchy (Loaded & Executable) + +At the **Loaded** and **Executable** phases, templates are split into type-specific classes: + +**Loaded Phase:** +``` +AbstractTemplateLoadedChange (abstract base) +├── SimpleTemplateLoadedChange (apply, rollback) +└── SteppableTemplateLoadedChange (steps) +``` + +**Executable Phase:** +``` +TemplateExecutableTask (abstract base) +├── SimpleTemplateExecutableTask (calls setStep()) +└── SteppableTemplateExecutableTask (calls setSteps()) +``` + +**Type Detection:** Happens in `TemplateLoadedTaskBuilder.build()` using: +- `AbstractSteppableTemplate.class.isAssignableFrom(templateClass)` → SteppableTemplateLoadedChange +- Otherwise → SimpleTemplateLoadedChange (default for AbstractSimpleTemplate and unknown types) + +**Note:** Preview phase (`TemplatePreviewChange`) remains unified since YAML is parsed before template type is known. + +### SteppableTemplateExecutableTask Apply/Rollback Lifecycle + +The `SteppableTemplateExecutableTask` manages multi-step execution with per-step rollback: + +**Apply Phase:** +- Iterates through steps in order (0 → N-1) +- Sets `applyPayload` on template instance before executing `@Apply` method +- `stepIndex` tracks current progress (-1 before start, N-1 after all steps complete) +- On failure at step N, `stepIndex = N` (points to failed step) + +**Rollback Phase (on apply failure):** +- Rolls back from current `stepIndex` down to 0 (reverse order) +- Sets `rollbackPayload` on template instance before executing `@Rollback` method +- **Skips steps without rollback payload** (`hasRollback()` returns false) +- **Skips if template has no `@Rollback` method** (logs warning) + +**Key Design Decision:** Same `SteppableTemplateExecutableTask` instance is used for both apply and rollback (no retry). The `stepIndex` state persists to enable rollback from the exact failure point. + ### Dependency Injection in Templates Template methods (`@Apply`, `@Rollback`) receive dependencies as **method parameters**, not constructor injection: diff --git a/core/flamingock-core-api/src/main/java/io/flamingock/api/annotations/ChangeTemplate.java b/core/flamingock-core-api/src/main/java/io/flamingock/api/annotations/ChangeTemplate.java new file mode 100644 index 000000000..fc2239fae --- /dev/null +++ b/core/flamingock-core-api/src/main/java/io/flamingock/api/annotations/ChangeTemplate.java @@ -0,0 +1,69 @@ +/* + * 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.annotations; + +import io.flamingock.api.template.AbstractChangeTemplate; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a class as a Flamingock change template and configures its execution mode. + * + *

All template classes must extend {@link AbstractChangeTemplate} and be annotated with + * this annotation to specify whether they process single or multiple steps. + * + *

Simple templates (default, {@code steppable = false}): + *

+ * id: create-users-table
+ * template: SqlTemplate
+ * apply: "CREATE TABLE users (id INT PRIMARY KEY)"
+ * rollback: "DROP TABLE users"
+ * 
+ * + *

Steppable templates ({@code steppable = true}) process multiple operations: + *

+ * id: setup-orders
+ * template: MongoTemplate
+ * steps:
+ *   - apply: { type: createCollection, collection: orders }
+ *     rollback: { type: dropCollection, collection: orders }
+ *   - apply: { type: insert, collection: orders, ... }
+ *     rollback: { type: delete, collection: orders, ... }
+ * 
+ * + *

Steppable rollback behavior: + *

    + *
  • On failure, previously successful steps are rolled back in reverse order
  • + *
  • Steps without rollback are skipped during rollback
  • + *
+ * + * @see AbstractChangeTemplate + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface ChangeTemplate { + + /** + * When {@code true}, the template expects a {@code steps} array in YAML. + * When {@code false} (default), it expects {@code apply} and optional {@code rollback} at root. + * + * @return {@code true} for steppable templates, {@code false} for simple templates + */ + boolean multiStep() default false; +} 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 a148d3a4e..1a278531d 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 @@ -24,16 +24,20 @@ /** - * Abstract base class for change templates providing common functionality. + * Base class for creating Flamingock change templates. * - *

This class handles generic type resolution and provides the common fields - * needed by all templates: changeId, isTransactional, and configuration. + *

Extend this class and annotate with {@link io.flamingock.api.annotations.ChangeTemplate} + * to create custom templates. The annotation's {@code steppable} attribute determines the YAML structure. * - *

For new templates, extend one of the specialized abstract classes: - *

    - *
  • {@link AbstractSimpleTemplate} - for templates with a single apply/rollback step
  • - *
  • {@link AbstractSteppableTemplate} - for templates with multiple steps
  • - *
+ *

Use {@code @ChangeTemplate} (default {@code steppable = false}) for simple templates with + * single apply/rollback fields. + * + *

Use {@code @ChangeTemplate(steppable = true)} for steppable templates with multiple steps. + * + * @param shared configuration type (use {@code Void} if none) + * @param apply payload type + * @param rollback payload type + * @see io.flamingock.api.annotations.ChangeTemplate */ public abstract class AbstractChangeTemplate implements ChangeTemplate { @@ -42,7 +46,10 @@ public abstract class AbstractChangeTemplate rollbackPayloadClass; protected String changeId; protected boolean isTransactional; + protected SHARED_CONFIGURATION_FIELD configuration; + protected APPLY_FIELD applyPayload; + protected ROLLBACK_FIELD rollbackPayload; private final Set> additionalReflectiveClasses; @@ -111,6 +118,16 @@ public void setConfiguration(SHARED_CONFIGURATION_FIELD configuration) { this.configuration = configuration; } + @Override + public void setApplyPayload(APPLY_FIELD applyPayload) { + this.applyPayload = applyPayload; + } + + @Override + public void setRollbackPayload(ROLLBACK_FIELD rollbackPayload) { + this.rollbackPayload = rollbackPayload; + } + @Override public Class getConfigurationClass() { return configurationClass; diff --git a/core/flamingock-core-api/src/main/java/io/flamingock/api/template/AbstractSimpleTemplate.java b/core/flamingock-core-api/src/main/java/io/flamingock/api/template/AbstractSimpleTemplate.java deleted file mode 100644 index 3642a9b35..000000000 --- a/core/flamingock-core-api/src/main/java/io/flamingock/api/template/AbstractSimpleTemplate.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * 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; - -/** - * Abstract base class for templates with a single apply/rollback step. - * - *

Use this class when your template processes a single operation that may have - * an optional rollback. The YAML structure for this template type is: - * - *

{@code
- * id: create-users-table
- * template: SqlTemplate
- * apply: "CREATE TABLE users ..."
- * rollback: "DROP TABLE users"
- * }
- * - *

The framework will automatically create a {@link TemplateStep} from the - * apply/rollback fields in the YAML and inject it via {@link #setStep}. - * - * @param the type of shared configuration - * @param the type of the apply payload - * @param the type of the rollback payload - */ -public abstract class AbstractSimpleTemplate - extends AbstractChangeTemplate { - - protected TemplateStep step; - - public AbstractSimpleTemplate(Class... additionalReflectiveClass) { - super(additionalReflectiveClass); - } - - /** - * Sets the step containing the apply and optional rollback payloads. - * - * @param step the template step - */ - public void setStep(TemplateStep step) { - this.step = step; - } - - /** - * Returns the step containing the apply and optional rollback payloads. - * - * @return the template step, or null if not set - */ - public TemplateStep getStep() { - return step; - } - - /** - * Checks if this template has a step set. - * - * @return true if a step is set - */ - public boolean hasStep() { - return step != null; - } - - /** - * Convenience method to get the apply payload from the step. - * - * @return the apply payload, or null if no step is set - */ - public APPLY getApply() { - return step != null ? step.getApply() : null; - } - - /** - * Convenience method to get the rollback payload from the step. - * - * @return the rollback payload, or null if no step is set or no rollback defined - */ - public ROLLBACK getRollback() { - return step != null ? step.getRollback() : null; - } - - /** - * Checks if this template has a rollback payload defined. - * - * @return true if a step is set and it has a rollback payload - */ - public boolean hasRollback() { - return step != null && step.hasRollback(); - } -} diff --git a/core/flamingock-core-api/src/main/java/io/flamingock/api/template/AbstractSteppableTemplate.java b/core/flamingock-core-api/src/main/java/io/flamingock/api/template/AbstractSteppableTemplate.java deleted file mode 100644 index 310188031..000000000 --- a/core/flamingock-core-api/src/main/java/io/flamingock/api/template/AbstractSteppableTemplate.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * 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; - -import java.util.List; - -/** - * Abstract base class for templates with multiple steps. - * - *

Use this class when your template processes multiple operations, each with - * its own apply and optional rollback. The YAML structure for this template type is: - * - *

{@code
- * id: create-orders-collection
- * template: MongoChangeTemplate
- * steps:
- *   - apply:
- *       type: createCollection
- *       collection: orders
- *     rollback:
- *       type: dropCollection
- *       collection: orders
- *   - apply:
- *       type: insert
- *       collection: orders
- *       parameters:
- *         documents:
- *           - orderId: "ORD-001"
- *     rollback:
- *       type: delete
- *       collection: orders
- *       parameters:
- *         filter: {}
- * }
- * - *

The framework will automatically parse the steps from the YAML and inject - * them via {@link #setSteps}. - * - *

Rollback Behavior: - *

    - *
  • When a step fails, all previously successful steps are rolled back in reverse order
  • - *
  • Steps without rollback operations are skipped during rollback
  • - *
  • Rollback errors are logged but don't stop the rollback process
  • - *
- * - * @param the type of shared configuration - * @param the type of the apply payload for each step - * @param the type of the rollback payload for each step - */ -public abstract class AbstractSteppableTemplate - extends AbstractChangeTemplate { - - protected List> steps; - - public AbstractSteppableTemplate(Class... additionalReflectiveClass) { - super(additionalReflectiveClass); - } - - /** - * Sets the list of steps to execute. - * - * @param steps the list of template steps - */ - public void setSteps(List> steps) { - this.steps = steps; - } - - /** - * Returns the list of steps. - * - * @return the list of template steps, or null if not set - */ - public List> getSteps() { - return steps; - } - -} 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 c99e20d1f..cdc0cbde0 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 @@ -16,16 +16,17 @@ package io.flamingock.api.template; /** - * Interface representing a reusable change template with configuration of type {@code CONFIG}. + * Core interface for Flamingock change templates. * - *

This interface is commonly implemented by classes that act as templates for Changes - * where a specific configuration needs to be injected and managed independently. + *

Templates enable declarative, YAML-based changes. Implement this interface + * by extending {@link AbstractChangeTemplate} and annotating with + * {@link io.flamingock.api.annotations.ChangeTemplate}. * - *

Templates should extend one of the abstract base classes: - *

    - *
  • {@link AbstractSimpleTemplate} - for templates with a single apply/rollback step
  • - *
  • {@link AbstractSteppableTemplate} - for templates with multiple steps
  • - *
+ * @param shared configuration type (use {@code Void} if none) + * @param apply payload type parsed from YAML + * @param rollback payload type parsed from YAML + * @see AbstractChangeTemplate + * @see io.flamingock.api.annotations.ChangeTemplate */ public interface ChangeTemplate extends ReflectionMetadataProvider { @@ -35,6 +36,10 @@ public interface ChangeTemplate 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 index 7ee3192f2..b4a999a6d 100644 --- 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 @@ -56,15 +56,15 @@ */ public class TemplateStep { - private APPLY apply; - private ROLLBACK rollback; + private APPLY applyPayload; + private ROLLBACK rollbackPayload; public TemplateStep() { } - public TemplateStep(APPLY apply, ROLLBACK rollback) { - this.apply = apply; - this.rollback = rollback; + public TemplateStep(APPLY applyPayload, ROLLBACK rollbackPayload) { + this.applyPayload = applyPayload; + this.rollbackPayload = rollbackPayload; } /** @@ -72,17 +72,17 @@ public TemplateStep(APPLY apply, ROLLBACK rollback) { * * @return the apply payload (required) */ - public APPLY getApply() { - return apply; + public APPLY getApplyPayload() { + return applyPayload; } /** * Sets the apply payload for this step. * - * @param apply the apply payload + * @param applyPayload the apply payload */ - public void setApply(APPLY apply) { - this.apply = apply; + public void setApplyPayload(APPLY applyPayload) { + this.applyPayload = applyPayload; } /** @@ -90,17 +90,17 @@ public void setApply(APPLY apply) { * * @return the rollback payload, or null if no rollback is defined */ - public ROLLBACK getRollback() { - return rollback; + public ROLLBACK getRollbackPayload() { + return rollbackPayload; } /** * Sets the rollback payload for this step. * - * @param rollback the rollback payload (optional) + * @param rollbackPayload the rollback payload (optional) */ - public void setRollback(ROLLBACK rollback) { - this.rollback = rollback; + public void setRollbackPayload(ROLLBACK rollbackPayload) { + this.rollbackPayload = rollbackPayload; } /** @@ -109,15 +109,15 @@ public void setRollback(ROLLBACK rollback) { * @return true if a rollback payload is defined */ public boolean hasRollback() { - return rollback != null; + return rollbackPayload != 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("apply=").append(applyPayload); + if (rollbackPayload != null) { + sb.append(", rollback=").append(rollbackPayload); } sb.append('}'); return sb.toString(); diff --git a/core/flamingock-core-api/src/test/java/io/flamingock/api/template/AbstractChangeTemplateReflectiveClassesTest.java b/core/flamingock-core-api/src/test/java/io/flamingock/api/template/AbstractChangeTemplateReflectiveClassesTest.java index 5b38e9564..6227c5a24 100644 --- a/core/flamingock-core-api/src/test/java/io/flamingock/api/template/AbstractChangeTemplateReflectiveClassesTest.java +++ b/core/flamingock-core-api/src/test/java/io/flamingock/api/template/AbstractChangeTemplateReflectiveClassesTest.java @@ -16,6 +16,7 @@ package io.flamingock.api.template; import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.ChangeTemplate; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -51,8 +52,9 @@ public static class AnotherAdditionalClass { } // Test template with custom generic types + @ChangeTemplate public static class TestTemplateWithCustomTypes - extends AbstractSimpleTemplate { + extends AbstractChangeTemplate { public TestTemplateWithCustomTypes() { super(); @@ -65,8 +67,9 @@ public void apply() { } // Test template with additional reflective classes + @ChangeTemplate public static class TestTemplateWithAdditionalClasses - extends AbstractSimpleTemplate { + extends AbstractChangeTemplate { public TestTemplateWithAdditionalClasses() { super(AdditionalClass.class, AnotherAdditionalClass.class); @@ -79,8 +82,9 @@ public void apply() { } // Test template with Void configuration + @ChangeTemplate public static class TestTemplateWithVoidConfig - extends AbstractSimpleTemplate { + extends AbstractChangeTemplate { public TestTemplateWithVoidConfig() { super(); diff --git a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/error/ChangeExecutionException.java b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/error/ChangeExecutionException.java index ddcbbd37d..02191eb2d 100644 --- a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/error/ChangeExecutionException.java +++ b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/error/ChangeExecutionException.java @@ -19,7 +19,7 @@ /** * Exception thrown when a Change execution fails. - * + * *

This exception provides rich context about the failed change execution including: *

    *
  • Change ID and stage information
  • @@ -28,14 +28,14 @@ *
  • Target system information
  • *
  • Original cause with preserved stack trace
  • *
- * - *

This exception should be used instead of generic {@link FlamingockException} + * + *

This exception should be used instead of generic {@link FlamingockException} * when change execution fails, as it provides much better debugging context. - * + * * @since 6.0.0 */ public class ChangeExecutionException extends FlamingockException { - + private final String changeId; private final String stageName; private final String executionMode; @@ -45,22 +45,22 @@ public class ChangeExecutionException extends FlamingockException { /** * Creates a new ChangeExecutionException with full execution context. * - * @param message descriptive error message - * @param changeId the ID of the failed change - * @param stageName the name of the stage containing the change - * @param executionMode the execution mode (e.g., "transactional", "non-transactional") + * @param message descriptive error message + * @param changeId the ID of the failed change + * @param stageName the name of the stage containing the change + * @param executionMode the execution mode (e.g., "transactional", "non-transactional") * @param executionDuration how long the change took before failing - * @param targetSystemId the target system where the change was being applied - * @param cause the underlying exception that caused the failure + * @param targetSystemId the target system where the change was being applied + * @param cause the underlying exception that caused the failure */ - public ChangeExecutionException(String message, - String changeId, - String stageName, - String executionMode, - Duration executionDuration, - String targetSystemId, - Throwable cause) { - super(buildContextualMessage(message, changeId, stageName, executionMode, executionDuration, targetSystemId), cause); + public ChangeExecutionException(String stageName, + String changeId, + String message, + String executionMode, + Duration executionDuration, + String targetSystemId, + Throwable cause) { + super(buildContextualMessage(stageName, changeId, message, executionMode, executionDuration, targetSystemId), cause); this.changeId = changeId; this.stageName = stageName; this.executionMode = executionMode; @@ -71,22 +71,22 @@ public ChangeExecutionException(String message, /** * Creates a new ChangeExecutionException with minimal context (for backward compatibility). * - * @param message descriptive error message * @param changeId the ID of the failed change - * @param cause the underlying exception that caused the failure + * @param message descriptive error message + * @param cause the underlying exception that caused the failure */ - public ChangeExecutionException(String message, String changeId, Throwable cause) { - this(message, changeId, null, null, null, null, cause); + public ChangeExecutionException(String changeId, String message, Throwable cause) { + this(null, changeId, message, null, null, null, cause); } - private static String buildContextualMessage(String message, - String changeId, - String stageName, - String executionMode, - Duration executionDuration, - String targetSystemId) { + private static String buildContextualMessage(String stageName, + String changeId, + String message, + String executionMode, + Duration executionDuration, + String targetSystemId) { StringBuilder contextMessage = new StringBuilder(message); - + if (changeId != null) { contextMessage.append("\n Change ID: ").append(changeId); } @@ -102,7 +102,7 @@ private static String buildContextualMessage(String message, if (targetSystemId != null) { contextMessage.append("\n Target System: ").append(targetSystemId); } - + return contextMessage.toString(); } diff --git a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/template/TemplateValidator.java b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/template/TemplateValidator.java index 667f23064..055314921 100644 --- a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/template/TemplateValidator.java +++ b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/template/TemplateValidator.java @@ -15,9 +15,7 @@ */ package io.flamingock.internal.common.core.template; -import io.flamingock.api.template.AbstractSimpleTemplate; -import io.flamingock.api.template.AbstractSteppableTemplate; -import io.flamingock.api.template.ChangeTemplate; +import io.flamingock.api.annotations.ChangeTemplate; import io.flamingock.internal.common.core.error.validation.ValidationError; import io.flamingock.internal.common.core.error.validation.ValidationResult; @@ -49,15 +47,17 @@ public class TemplateValidator { */ public enum TemplateType { /** - * Template extends AbstractSimpleTemplate - uses apply/rollback fields. + * Template annotated with {@code @ChangeTemplate(steppable = false)} or without annotation. + * Uses apply/rollback fields. */ SIMPLE, /** - * Template extends AbstractSteppableTemplate - uses steps field. + * Template annotated with {@code @ChangeTemplate(steppable = true)}. + * Uses steps field. */ STEPPABLE, /** - * Template type could not be determined. + * Template type could not be determined (kept for backward compatibility). */ UNKNOWN } @@ -88,7 +88,7 @@ public ValidationResult validate(ChangeTemplateFileContent content) { return result; } - Optional>> templateClassOpt = ChangeTemplateManager.getTemplate(templateName); + Optional>> templateClassOpt = ChangeTemplateManager.getTemplate(templateName); if (!templateClassOpt.isPresent()) { result.add(new ValidationError( @@ -99,7 +99,7 @@ public ValidationResult validate(ChangeTemplateFileContent content) { return result; } - Class> templateClass = templateClassOpt.get(); + Class> templateClass = templateClassOpt.get(); TemplateType type = getTemplateType(templateClass); switch (type) { @@ -118,18 +118,18 @@ public ValidationResult validate(ChangeTemplateFileContent content) { } /** - * Determines the template type based on the class hierarchy. + * Determines the template type based on the {@link ChangeTemplate} annotation. * * @param templateClass the template class to check - * @return the TemplateType (SIMPLE, STEPPABLE, or UNKNOWN) + * @return the TemplateType (SIMPLE or STEPPABLE). Returns SIMPLE by default if annotation is missing. */ - public TemplateType getTemplateType(Class> templateClass) { - if (AbstractSimpleTemplate.class.isAssignableFrom(templateClass)) { - return TemplateType.SIMPLE; - } else if (AbstractSteppableTemplate.class.isAssignableFrom(templateClass)) { + public TemplateType getTemplateType(Class> templateClass) { + ChangeTemplate annotation = templateClass.getAnnotation(ChangeTemplate.class); + if (annotation != null && annotation.multiStep()) { return TemplateType.STEPPABLE; } - return TemplateType.UNKNOWN; + // Default to SIMPLE (including when annotation is missing or steppable=false) + return TemplateType.SIMPLE; } /** diff --git a/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/template/TemplateValidatorTest.java b/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/template/TemplateValidatorTest.java index 82975e039..ba399f8e6 100644 --- a/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/template/TemplateValidatorTest.java +++ b/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/template/TemplateValidatorTest.java @@ -16,8 +16,8 @@ package io.flamingock.internal.common.core.template; import io.flamingock.api.annotations.Apply; -import io.flamingock.api.template.AbstractSimpleTemplate; -import io.flamingock.api.template.AbstractSteppableTemplate; +import io.flamingock.api.annotations.ChangeTemplate; +import io.flamingock.api.template.AbstractChangeTemplate; import io.flamingock.internal.common.core.error.validation.ValidationResult; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -36,8 +36,9 @@ class TemplateValidatorTest { private TemplateValidator validator; - // Test template extending AbstractSimpleTemplate - public static class TestSimpleTemplate extends AbstractSimpleTemplate { + // Test template with @ChangeTemplate (simple template) + @ChangeTemplate + public static class TestSimpleTemplate extends AbstractChangeTemplate { public TestSimpleTemplate() { super(); } @@ -48,8 +49,9 @@ public void apply() { } } - // Test template extending AbstractSteppableTemplate - public static class TestSteppableTemplate extends AbstractSteppableTemplate { + // Test template with @ChangeTemplate(steppable = true) + @ChangeTemplate(multiStep = true) + public static class TestSteppableTemplate extends AbstractChangeTemplate { public TestSteppableTemplate() { super(); } diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/external/store/audit/domain/RuntimeContext.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/external/store/audit/domain/RuntimeContext.java index a3308b891..95f83f2c2 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/external/store/audit/domain/RuntimeContext.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/external/store/audit/domain/RuntimeContext.java @@ -106,7 +106,7 @@ private Builder() { public Builder setStartStep(StartStep taskStep) { duration = 0L; - methodExecutor = taskStep.getTask().getExecutionMethodName(); + methodExecutor = taskStep.getTask().getApplyMethodName(); stageName = taskStep.getTask().getStageName(); setResult(taskStep); return this; @@ -114,7 +114,7 @@ public Builder setStartStep(StartStep taskStep) { public Builder setExecutionStep(ExecutionStep taskStep) { duration = taskStep.getDuration(); - methodExecutor = taskStep.getTask().getExecutionMethodName(); + methodExecutor = taskStep.getTask().getApplyMethodName(); stageName = taskStep.getTask().getStageName(); setResult(taskStep); return this; @@ -122,7 +122,7 @@ public Builder setExecutionStep(ExecutionStep taskStep) { public Builder setManualRollbackStep(ManualRolledBackStep rolledBackStep) { duration = rolledBackStep.getDuration(); - methodExecutor = rolledBackStep.getRollback().getRollbackMethodName(); + methodExecutor = rolledBackStep.getTask().getRollbackMethodName(); stageName = rolledBackStep.getTask().getStageName(); setResult(rolledBackStep); return this; diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/AbstractTemplateExecutableTask.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/AbstractTemplateExecutableTask.java new file mode 100644 index 000000000..b20f80c15 --- /dev/null +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/AbstractTemplateExecutableTask.java @@ -0,0 +1,71 @@ +/* + * Copyright 2023 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.internal.core.task.executable; + +import io.flamingock.api.template.ChangeTemplate; +import io.flamingock.internal.common.core.recovery.action.ChangeAction; +import io.flamingock.internal.core.runtime.ExecutionRuntime; +import io.flamingock.internal.core.task.loaded.AbstractTemplateLoadedChange; +import io.flamingock.internal.util.FileUtil; +import io.flamingock.internal.util.log.FlamingockLoggerFactory; +import org.slf4j.Logger; + +import java.lang.reflect.Method; +import java.util.Arrays; + +/** + * Abstract base class for template executable tasks. + * Contains common logic for executing templates, with type-specific data setting + * delegated to subclasses. + * + * @param the configuration type for the template + * @param the apply payload type + * @param the rollback payload type + * @param the type of template loaded change + */ +public abstract class AbstractTemplateExecutableTask> extends ReflectionExecutableTask { + protected final Logger logger = FlamingockLoggerFactory.getLogger("TemplateTask"); + + public AbstractTemplateExecutableTask(String stageName, + T descriptor, + ChangeAction action, + Method executionMethod, + Method rollbackMethod) { + super(stageName, descriptor, action, executionMethod, rollbackMethod); + } + + protected void setConfigurationData(ChangeTemplate instance) { + Class parameterClass = instance.getConfigurationClass(); + CONFIG data = descriptor.getConfiguration(); + + if (data != null && Void.class != parameterClass) { + instance.setConfiguration(FileUtil.getFromMap(parameterClass, data)); + } else if (Void.class != parameterClass) { + logger.warn("No 'Configuration' section provided for template-based change[{}] of type[{}]", + descriptor.getId(), descriptor.getTemplateClass().getName()); + } + } + + protected Method getSetterMethod(Class changeTemplateClass, String methodName) { + return Arrays.stream(changeTemplateClass.getMethods()) + .filter(m -> methodName.equals(m.getName())) + .findFirst() + .orElseThrow(() -> new RuntimeException("Not found config setter for template: " + changeTemplateClass.getSimpleName())); + } + + +} diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/CodeExecutableTask.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/CodeExecutableTask.java new file mode 100644 index 000000000..23ea02912 --- /dev/null +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/CodeExecutableTask.java @@ -0,0 +1,73 @@ +/* + * Copyright 2023 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.internal.core.task.executable; + +import io.flamingock.internal.common.core.error.ChangeExecutionException; +import io.flamingock.internal.common.core.recovery.action.ChangeAction; +import io.flamingock.internal.core.runtime.ExecutionRuntime; +import io.flamingock.internal.core.task.loaded.AbstractReflectionLoadedTask; + +import java.lang.reflect.Method; +import java.util.List; + +/** + * This class is a reflection version of the ExecutableTask. + *

+ * It creates a new instance on demand in every execution(apply and rollback), because it's intended to be applied + * just once. The only case it will be potentially applied twice is if it fails, and in that case will only happen + * once(in case of sequential execution) or very few times(in case or parallel execution and happen to fail multiple + * concurrent tasks at the same time),because after that the process will abort. + *

+ * For this reason it's more optimal to do it on demand, that articulate some synchronisation mechanism. + *

+ * However, the methods are extracted in advance, so we can spot wrong configuration before starting the process and + * fail fast. + */ +public class CodeExecutableTask + extends ReflectionExecutableTask { + + + public CodeExecutableTask(String stageName, + REFLECTION_TASK_DESCRIPTOR descriptor, + ChangeAction action, + Method executionMethod, + Method rollbackMethod) { + super(stageName, descriptor, action, executionMethod, rollbackMethod); + } + + + + @Override + public void apply(ExecutionRuntime executionRuntime) { + executeInternal(executionRuntime, executionMethod); + } + + @Override + public void rollback(ExecutionRuntime executionRuntime) { + executeInternal(executionRuntime, rollbackMethod); + } + + protected void executeInternal(ExecutionRuntime executionRuntime, Method method ) { + Object instance = executionRuntime.getInstance(descriptor.getConstructor()); + try { + executionRuntime.executeMethodWithInjectedDependencies(instance, method); + } catch (Throwable ex) { + throw new ChangeExecutionException(this.getId(), ex.getMessage(), ex); + } + } + + +} diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/ExecutableTask.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/ExecutableTask.java index a8475f6c0..62f415633 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/ExecutableTask.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/ExecutableTask.java @@ -27,16 +27,16 @@ public interface ExecutableTask extends TaskDescriptor { String getStageName(); - void execute(ExecutionRuntime runtimeHelper); + void apply(ExecutionRuntime executionRuntime); - String getExecutionMethodName(); + String getApplyMethodName(); - boolean isAlreadyApplied(); + void rollback(ExecutionRuntime executionRuntime); - ChangeAction getAction(); + String getRollbackMethodName(); - void addRollback(Rollback rollback); + boolean isAlreadyApplied(); - List getRollbackChain(); + ChangeAction getAction(); } \ No newline at end of file diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/ReflectionExecutableTask.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/ReflectionExecutableTask.java index 6aaf6bcd6..1de857237 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/ReflectionExecutableTask.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/ReflectionExecutableTask.java @@ -37,11 +37,11 @@ * However, the methods are extracted in advance, so we can spot wrong configuration before starting the process and * fail fast. */ -public class ReflectionExecutableTask extends AbstractExecutableTask implements ExecutableTask { +public abstract class ReflectionExecutableTask + extends AbstractExecutableTask implements ExecutableTask { protected final Method executionMethod; - - protected final List rollbackChain; + protected final Method rollbackMethod; public ReflectionExecutableTask(String stageName, @@ -51,60 +51,18 @@ public ReflectionExecutableTask(String stageName, Method rollbackMethod) { super(stageName, descriptor, action); this.executionMethod = executionMethod; - rollbackChain = new LinkedList<>(); - if(rollbackMethod != null) { - rollbackChain.add(buildRollBack(rollbackMethod)); - } - } - - @Override - public void addRollback(Rollback rollback) { - rollbackChain.add(rollback); - - } - - @Override - public List getRollbackChain() { - return rollbackChain; + this.rollbackMethod = rollbackMethod; } @Override - public void execute(ExecutionRuntime executionRuntime) { - executeInternal(executionRuntime, executionMethod); - - } - - protected void executeInternal(ExecutionRuntime executionRuntime, Method method ) { - Object instance = executionRuntime.getInstance(descriptor.getConstructor()); - try { - executionRuntime.executeMethodWithInjectedDependencies(instance, method); - } catch (Throwable ex) { - throw new ChangeExecutionException(ex.getMessage(), this.getId(), ex); - } + public String getApplyMethodName() { + return executionMethod.getName(); } @Override - public String getExecutionMethodName() { - return executionMethod.getName(); + public String getRollbackMethodName() { + return rollbackMethod.getName(); } - private Rollback buildRollBack(Method rollbackMethod) { - return new Rollback() { - @Override - public ExecutableTask getTask() { - return ReflectionExecutableTask.this; - } - - @Override - public void rollback(ExecutionRuntime executionRuntime) { - executeInternal(executionRuntime, rollbackMethod); - } - - @Override - public String getRollbackMethodName() { - return rollbackMethod.getName(); - } - }; - } } diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/SimpleTemplateExecutableTask.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/SimpleTemplateExecutableTask.java new file mode 100644 index 000000000..5add1dad2 --- /dev/null +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/SimpleTemplateExecutableTask.java @@ -0,0 +1,79 @@ +/* + * Copyright 2023 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.internal.core.task.executable; + +import io.flamingock.api.template.AbstractChangeTemplate; +import io.flamingock.internal.common.core.error.ChangeExecutionException; +import io.flamingock.internal.common.core.recovery.action.ChangeAction; +import io.flamingock.internal.core.runtime.ExecutionRuntime; +import io.flamingock.internal.core.task.loaded.SimpleTemplateLoadedChange; + +import java.lang.reflect.Method; + +/** + * Executable task for simple templates (single apply/rollback step). + * Handles templates annotated with {@code @ChangeTemplate(steppable = false)} or without annotation. + * + * @param the configuration type for the template + * @param the apply payload type + * @param the rollback payload type + */ +public class SimpleTemplateExecutableTask + extends AbstractTemplateExecutableTask> { + + public SimpleTemplateExecutableTask(String stageName, + SimpleTemplateLoadedChange descriptor, + ChangeAction action, + Method executionMethod, + Method rollbackMethod) { + super(stageName, descriptor, action, executionMethod, rollbackMethod); + } + + @Override + public void apply(ExecutionRuntime executionRuntime) { + logger.debug("Applying change[{}] with template: {}", descriptor.getId(), descriptor.getTemplateClass()); + logger.debug("change[{}] transactional: {}", descriptor.getId(), descriptor.isTransactional()); + executeInternal(executionRuntime, executionMethod); + } + + @Override + public void rollback(ExecutionRuntime executionRuntime) { + logger.debug("Rolling back change[{}] with template: {}", descriptor.getId(), descriptor.getTemplateClass()); + executeInternal(executionRuntime, rollbackMethod); + } + + @SuppressWarnings("unchecked") + protected void executeInternal(ExecutionRuntime executionRuntime, Method method) { + try { + AbstractChangeTemplate instance = (AbstractChangeTemplate) + executionRuntime.getInstance(descriptor.getConstructor()); + + instance.setTransactional(descriptor.isTransactional()); + instance.setChangeId(descriptor.getId()); + logger.trace("Setting payloads for simple template change[{}]", descriptor.getId()); + setConfigurationData(instance); + instance.setApplyPayload(descriptor.getApplyPayload()); + instance.setRollbackPayload(descriptor.getRollbackPayload()); + + executionRuntime.executeMethodWithInjectedDependencies(instance, method); + } catch (Throwable ex) { + throw new ChangeExecutionException(this.getId(), ex.getMessage(), ex); + } + } + + +} diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/SteppableTemplateExecutableTask.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/SteppableTemplateExecutableTask.java new file mode 100644 index 000000000..0b735e054 --- /dev/null +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/SteppableTemplateExecutableTask.java @@ -0,0 +1,112 @@ +/* + * Copyright 2023 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.internal.core.task.executable; + +import io.flamingock.api.template.AbstractChangeTemplate; +import io.flamingock.api.template.TemplateStep; +import io.flamingock.internal.common.core.error.ChangeExecutionException; +import io.flamingock.internal.common.core.recovery.action.ChangeAction; +import io.flamingock.internal.core.runtime.ExecutionRuntime; +import io.flamingock.internal.core.task.loaded.SteppableTemplateLoadedChange; +import org.jetbrains.annotations.NotNull; + +import java.lang.reflect.Method; +import java.util.List; + +/** + * Executable task for steppable templates (multiple steps). + * Handles templates annotated with {@code @ChangeTemplate(steppable = true)}. + * + * @param the configuration type for the template + * @param the apply payload type + * @param the rollback payload type + */ +public class SteppableTemplateExecutableTask + extends AbstractTemplateExecutableTask> { + + private int stepIndex = -1; + + public SteppableTemplateExecutableTask(String stageName, + SteppableTemplateLoadedChange descriptor, + ChangeAction action, + Method executionMethod, + Method rollbackMethod) { + super(stageName, descriptor, action, executionMethod, rollbackMethod); + } + + @Override + public void apply(ExecutionRuntime executionRuntime) { + AbstractChangeTemplate instance = buildInstance(executionRuntime); + + try { + List> steps = descriptor.getSteps(); + while (stepIndex >= -1 && stepIndex + 1 < steps.size()) { + TemplateStep currentSep = steps.get(stepIndex + 1); + instance.setApplyPayload(currentSep.getApplyPayload()); + stepIndex++; + executionRuntime.executeMethodWithInjectedDependencies(instance, executionMethod); + } + } catch (Throwable ex) { + throw new ChangeExecutionException(this.getId(), ex.getMessage(), ex); + } + } + + @Override + public void rollback(ExecutionRuntime executionRuntime) { + AbstractChangeTemplate instance = buildInstance(executionRuntime); + + try { + List> steps = descriptor.getSteps(); + while (stepIndex >= 0 && stepIndex < steps.size()) { + TemplateStep currentSep = steps.get(stepIndex); + if(currentSep.hasRollback() && rollbackMethod != null) { + instance.setRollbackPayload(currentSep.getRollbackPayload()); + executionRuntime.executeMethodWithInjectedDependencies(instance, rollbackMethod); + } else { + logger.warn("Skipping rollback for change[{}], step[{}] -> payload provided[{}], rollback support in template[{}]", + getId(), stepIndex, currentSep.hasRollback(), rollbackMethod != null); + } + stepIndex--; + } + } catch (Throwable ex) { + throw new ChangeExecutionException(this.getId(), ex.getMessage(), ex); + } + } + + @NotNull + @SuppressWarnings("unchecked") + private AbstractChangeTemplate buildInstance(ExecutionRuntime executionRuntime) { + AbstractChangeTemplate instance; + try { + logger.debug("Starting execution of change[{}] with template: {}", descriptor.getId(), descriptor.getTemplateClass()); + logger.debug("change[{}] transactional: {}", descriptor.getId(), descriptor.isTransactional()); + + instance = (AbstractChangeTemplate) + executionRuntime.getInstance(descriptor.getConstructor()); + + instance.setTransactional(descriptor.isTransactional()); + instance.setChangeId(descriptor.getId()); + setConfigurationData(instance); + + } catch (Throwable ex) { + throw new ChangeExecutionException(this.getId(), ex.getMessage(), ex); + } + return instance; + } + + +} 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 deleted file mode 100644 index 176b0e5f0..000000000 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/TemplateExecutableTask.java +++ /dev/null @@ -1,220 +0,0 @@ -/* - * Copyright 2023 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.internal.core.task.executable; - -import io.flamingock.api.template.AbstractSimpleTemplate; -import io.flamingock.api.template.AbstractSteppableTemplate; -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; -import io.flamingock.internal.common.core.recovery.action.ChangeAction; -import io.flamingock.internal.util.FileUtil; -import io.flamingock.internal.util.log.FlamingockLoggerFactory; -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"); - - public TemplateExecutableTask(String stageName, - TemplateLoadedChange descriptor, - ChangeAction action, - Method executionMethod, - Method rollbackMethod) { - super(stageName, descriptor, action, executionMethod, rollbackMethod); - } - - @Override - protected void executeInternal(ExecutionRuntime executionRuntime, Method method ) { - try { - logger.debug("Starting execution of change[{}] with template: {}", descriptor.getId(), descriptor.getTemplateClass()); - logger.debug("change[{}] transactional: {}", descriptor.getId(), descriptor.isTransactional()); - Object instance = executionRuntime.getInstance(descriptor.getConstructor()); - ChangeTemplate changeTemplateInstance = (ChangeTemplate) instance; - changeTemplateInstance.setTransactional(descriptor.isTransactional()); - changeTemplateInstance.setChangeId(descriptor.getId()); - setConfigurationData(executionRuntime, changeTemplateInstance); - - if (instance instanceof AbstractSteppableTemplate) { - setStepsForSteppableTemplate(executionRuntime, (AbstractSteppableTemplate) instance); - } else if (instance instanceof AbstractSimpleTemplate) { - setStepForSimpleTemplate(executionRuntime, (AbstractSimpleTemplate) instance); - } - - executionRuntime.executeMethodWithInjectedDependencies(instance, method); - } catch (Throwable ex) { - throw new ChangeExecutionException(ex.getMessage(), this.getId(), ex); - } - } - - /** - * Sets the steps for an AbstractSteppableTemplate. - */ - @SuppressWarnings({"unchecked", "rawtypes"}) - private void setStepsForSteppableTemplate(ExecutionRuntime executionRuntime, - AbstractSteppableTemplate instance) { - Object stepsData = descriptor.getSteps(); - - if (stepsData != null) { - logger.debug("Setting steps for steppable template change[{}]", descriptor.getId()); - - List convertedSteps = convertToTemplateSteps( - stepsData, - instance.getApplyPayloadClass(), - instance.getRollbackPayloadClass() - ); - - Method setStepsMethod = getSetterMethod(instance.getClass(), "setSteps"); - executionRuntime.executeMethodWithParameters(instance, setStepsMethod, convertedSteps); - } else { - logger.warn("No 'steps' section provided for steppable template-based change[{}]", descriptor.getId()); - } - } - - /** - * Sets the step for an AbstractSimpleTemplate. - */ - @SuppressWarnings({"unchecked", "rawtypes"}) - private void setStepForSimpleTemplate(ExecutionRuntime executionRuntime, - AbstractSimpleTemplate instance) { - Object applyData = descriptor.getApply(); - - if (applyData != null) { - logger.debug("Setting step for simple template change[{}]", descriptor.getId()); - - TemplateStep step = convertToTemplateStep( - applyData, - descriptor.getRollback(), - instance.getApplyPayloadClass(), - instance.getRollbackPayloadClass() - ); - - Method setStepMethod = getSetterMethod(instance.getClass(), "setStep"); - executionRuntime.executeMethodWithParameters(instance, setStepMethod, step); - } else { - logger.warn("No 'apply' section provided for simple template-based change[{}]", descriptor.getId()); - } - } - - private void setConfigurationData(ExecutionRuntime executionRuntime, - ChangeTemplate instance) { - Class parameterClass = instance.getConfigurationClass(); - Object data = descriptor.getConfiguration(); - - if (data != null && Void.class != parameterClass) { - Method setConfigurationMethod = getSetterMethod(instance.getClass(), "setConfiguration"); - executionRuntime.executeMethodWithParameters( - instance, - setConfigurationMethod, - FileUtil.getFromMap(parameterClass, data)); - } else if (Void.class != parameterClass) { - logger.warn("No 'Configuration' section provided for template-based change[{}] of type[{}]", - descriptor.getId(), descriptor.getTemplateClass().getName()); - } - } - - - private Method getSetterMethod(Class changeTemplateClass, String methodName) { - - return Arrays.stream(changeTemplateClass.getMethods()) - .filter(m-> methodName.equals(m.getName())) - .findFirst() - .orElseThrow(()-> new RuntimeException("Not found config setter for template: " + changeTemplateClass.getSimpleName())); - - } - - /** - * Converts raw apply/rollback data from YAML to a TemplateStep object. - * - * @param applyData the raw apply data - * @param rollbackData the raw rollback data (maybe null) - * @param applyClass the class type for apply payload - * @param rollbackClass the class type for rollback payload - * @return the converted TemplateStep object - */ - @SuppressWarnings({"unchecked", "rawtypes"}) - private TemplateStep convertToTemplateStep(Object applyData, - Object rollbackData, - Class applyClass, - Class rollbackClass) { - TemplateStep step = new TemplateStep(); - - if (applyData != null && Void.class != applyClass) { - step.setApply(FileUtil.getFromMap(applyClass, applyData)); - } - - if (rollbackData != null && Void.class != rollbackClass) { - step.setRollback(FileUtil.getFromMap(rollbackClass, rollbackData)); - } - - return step; - } - - /** - * 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/main/java/io/flamingock/internal/core/task/executable/builder/CodeExecutableTaskBuilder.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/builder/CodeExecutableTaskBuilder.java index e3915c669..72bb32440 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/builder/CodeExecutableTaskBuilder.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/builder/CodeExecutableTaskBuilder.java @@ -16,11 +16,12 @@ package io.flamingock.internal.core.task.executable.builder; import io.flamingock.internal.common.core.recovery.action.ChangeAction; +import io.flamingock.internal.core.task.executable.CodeExecutableTask; import io.flamingock.internal.core.task.executable.ExecutableTask; import io.flamingock.internal.core.task.executable.ReflectionExecutableTask; import io.flamingock.internal.core.task.loaded.AbstractLoadedTask; -import io.flamingock.internal.core.task.loaded.CodeLoadedChange; import io.flamingock.internal.core.task.loaded.AbstractReflectionLoadedTask; +import io.flamingock.internal.core.task.loaded.CodeLoadedChange; import java.lang.reflect.Method; import java.util.Optional; @@ -47,7 +48,7 @@ public static boolean supports(AbstractLoadedTask loadedTask) { @Override public CodeLoadedChange cast(AbstractLoadedTask loadedTask) { - return (CodeLoadedChange)loadedTask; + return (CodeLoadedChange) loadedTask; } @Override @@ -77,18 +78,18 @@ public ExecutableTask build() { * New ChangeAction-based method for building tasks. */ private ReflectionExecutableTask getTasksFromReflection(String stageName, - CodeLoadedChange loadedTask, - ChangeAction action) { + CodeLoadedChange loadedTask, + ChangeAction action) { return buildTasksInternal(stageName, loadedTask, action); } private ReflectionExecutableTask buildTasksInternal(String stageName, - CodeLoadedChange loadedTask, - ChangeAction action) { + CodeLoadedChange loadedTask, + ChangeAction action) { Method executionMethod = loadedTask.getApplyMethod(); Optional rollbackMethodOpt = loadedTask.getRollbackMethod(); - return new ReflectionExecutableTask<>( + return new CodeExecutableTask<>( stageName, loadedTask, action, diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/builder/ExecutableTaskBuilder.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/builder/ExecutableTaskBuilder.java index 5b566beeb..bc156115f 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/builder/ExecutableTaskBuilder.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/builder/ExecutableTaskBuilder.java @@ -18,8 +18,8 @@ import io.flamingock.internal.common.core.recovery.action.ChangeAction; import io.flamingock.internal.core.task.executable.ExecutableTask; import io.flamingock.internal.core.task.loaded.AbstractLoadedTask; +import io.flamingock.internal.core.task.loaded.AbstractTemplateLoadedChange; import io.flamingock.internal.core.task.loaded.CodeLoadedChange; -import io.flamingock.internal.core.task.loaded.TemplateLoadedChange; public interface ExecutableTaskBuilder { @@ -43,7 +43,7 @@ static ExecutableTaskBuilder getInstance(AbstractLoadedTask loadedTask) { if(TemplateExecutableTaskBuilder.supports(loadedTask)) { TemplateExecutableTaskBuilder templateBuilder = TemplateExecutableTaskBuilder.getInstance(); - TemplateLoadedChange castedTask = templateBuilder.cast(loadedTask); + AbstractTemplateLoadedChange castedTask = templateBuilder.cast(loadedTask); return templateBuilder.setLoadedTask(castedTask); } else if(CodeExecutableTaskBuilder.supports(loadedTask)) { diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/builder/TemplateExecutableTaskBuilder.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/builder/TemplateExecutableTaskBuilder.java index ad14e6467..0facf711b 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/builder/TemplateExecutableTaskBuilder.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/builder/TemplateExecutableTaskBuilder.java @@ -15,12 +15,14 @@ */ package io.flamingock.internal.core.task.executable.builder; -import io.flamingock.api.template.AbstractSteppableTemplate; import io.flamingock.internal.common.core.recovery.action.ChangeAction; import io.flamingock.internal.core.task.executable.ExecutableTask; -import io.flamingock.internal.core.task.executable.TemplateExecutableTask; +import io.flamingock.internal.core.task.executable.SimpleTemplateExecutableTask; +import io.flamingock.internal.core.task.executable.SteppableTemplateExecutableTask; import io.flamingock.internal.core.task.loaded.AbstractLoadedTask; -import io.flamingock.internal.core.task.loaded.TemplateLoadedChange; +import io.flamingock.internal.core.task.loaded.AbstractTemplateLoadedChange; +import io.flamingock.internal.core.task.loaded.SimpleTemplateLoadedChange; +import io.flamingock.internal.core.task.loaded.SteppableTemplateLoadedChange; import io.flamingock.internal.util.log.FlamingockLoggerFactory; import org.slf4j.Logger; @@ -30,29 +32,29 @@ /** * Factory for Change classes */ -public class TemplateExecutableTaskBuilder implements ExecutableTaskBuilder { +public class TemplateExecutableTaskBuilder implements ExecutableTaskBuilder> { private final static Logger logger = FlamingockLoggerFactory.getLogger("TemplateBuilder"); private static final TemplateExecutableTaskBuilder instance = new TemplateExecutableTaskBuilder(); private String stageName; private ChangeAction changeAction; - private TemplateLoadedChange loadedTask; + private AbstractTemplateLoadedChange loadedTask; static TemplateExecutableTaskBuilder getInstance() { return instance; } public static boolean supports(AbstractLoadedTask loadedTask) { - return TemplateLoadedChange.class.isAssignableFrom(loadedTask.getClass()); + return AbstractTemplateLoadedChange.class.isAssignableFrom(loadedTask.getClass()); } @Override - public TemplateLoadedChange cast(AbstractLoadedTask loadedTask) { - return (TemplateLoadedChange) loadedTask; + public AbstractTemplateLoadedChange cast(AbstractLoadedTask loadedTask) { + return (AbstractTemplateLoadedChange) loadedTask; } @Override - public TemplateExecutableTaskBuilder setLoadedTask(TemplateLoadedChange loadedTask) { + public TemplateExecutableTaskBuilder setLoadedTask(AbstractTemplateLoadedChange loadedTask) { this.loadedTask = loadedTask; return this; } @@ -72,57 +74,50 @@ public TemplateExecutableTaskBuilder setChangeAction(ChangeAction action) { @Override public ExecutableTask build() { - return getTasksFromReflection(stageName, loadedTask, changeAction); - } - - - /** - * New ChangeAction-based method for building tasks. - */ - private TemplateExecutableTask getTasksFromReflection(String stageName, - TemplateLoadedChange loadedTask, - ChangeAction action) { - return buildTask(stageName, loadedTask, action); - } - - private TemplateExecutableTask buildTask(String stageName, - TemplateLoadedChange loadedTask, - ChangeAction action) { - Method rollbackMethod = null; - - boolean isSteppableTemplate = AbstractSteppableTemplate.class.isAssignableFrom(loadedTask.getTemplateClass()); - - if (isSteppableTemplate) { - rollbackMethod = loadedTask.getRollbackMethod().orElse(null); - if (rollbackMethod != null) { - logger.trace("Change[{}] is a steppable template with rollback method", loadedTask.getId()); - } - } else if (loadedTask.getRollback() != null) { - rollbackMethod = loadedTask.getRollbackMethod().orElse(null); - if (rollbackMethod != null) { - logger.trace("Change[{}] provides rollback in configuration", loadedTask.getId()); + Method rollbackMethod = loadedTask.getRollbackMethod().orElse(null); + + if (loadedTask instanceof SimpleTemplateLoadedChange) { + SimpleTemplateLoadedChange simple = (SimpleTemplateLoadedChange) loadedTask; + // Only include rollback method if rollback data is present + if (simple.hasRollback()) { + if (rollbackMethod != null) { + logger.trace("Change[{}] provides rollback in configuration", loadedTask.getId()); + } else { + logger.warn("Change[{}] provides rollback in configuration, but template[{}] doesn't support manual rollback", + loadedTask.getId(), + loadedTask.getSource() + ); + } } else { - logger.warn("Change[{}] provides rollback in configuration, but based on a template[{}] not supporting manual rollback", - loadedTask.getId(), - loadedTask.getSource() - ); + if (rollbackMethod != null) { + logger.warn("Change[{}] does not provide rollback, but template[{}] supports manual rollback", + loadedTask.getId(), + loadedTask.getSource() + ); + } + rollbackMethod = null; } - } else { - if (loadedTask.getRollbackMethod().isPresent()) { - logger.warn("Change[{}] does not provide rollback, but based on a template[{}] support manual rollback", - loadedTask.getId(), - loadedTask.getSource() - ); + return new SimpleTemplateExecutableTask<>( + stageName, + simple, + changeAction, + loadedTask.getApplyMethod(), + rollbackMethod + ); + } else if (loadedTask instanceof SteppableTemplateLoadedChange) { + SteppableTemplateLoadedChange steppable = (SteppableTemplateLoadedChange) loadedTask; + if (rollbackMethod != null) { + logger.trace("Change[{}] is a steppable template with rollback method", loadedTask.getId()); } + return new SteppableTemplateExecutableTask<>( + stageName, + steppable, + changeAction, + loadedTask.getApplyMethod(), + rollbackMethod + ); } - return new TemplateExecutableTask( - stageName, - loadedTask, - action, - loadedTask.getApplyMethod(), - rollbackMethod - ); - + throw new IllegalArgumentException("Unknown template type: " + loadedTask.getClass().getName()); } } 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/AbstractTemplateLoadedChange.java similarity index 57% rename from core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/TemplateLoadedChange.java rename to core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/AbstractTemplateLoadedChange.java index c30e84a07..59b692ee9 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/AbstractTemplateLoadedChange.java @@ -18,74 +18,60 @@ import io.flamingock.api.annotations.Apply; import io.flamingock.api.annotations.Rollback; import io.flamingock.api.template.ChangeTemplate; -import io.flamingock.internal.common.core.preview.PreviewConstructor; -import io.flamingock.internal.util.ReflectionUtil; import io.flamingock.internal.common.core.task.RecoveryDescriptor; import io.flamingock.internal.common.core.task.TargetSystemDescriptor; +import io.flamingock.internal.util.ReflectionUtil; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.util.List; import java.util.Optional; - -public class TemplateLoadedChange extends AbstractLoadedChange { +/** + * Abstract base class for template-based loaded changes. + * Contains common fields and methods shared by both SimpleTemplateLoadedChange + * and SteppableTemplateLoadedChange. + * + * @param the configuration type for the template + * @param the apply payload type + * @param the rollback payload type + */ +public abstract class AbstractTemplateLoadedChange extends AbstractLoadedChange { private final List profiles; - private final Object configuration; - private final Object apply; - private final Object rollback; - private final Object steps; - - TemplateLoadedChange(String changeFileName, - String id, - String order, - String author, - Class> templateClass, - Constructor constructor, - List profiles, - boolean transactional, - boolean runAlways, - boolean systemTask, - Object configuration, - Object apply, - Object rollback, - Object steps, - TargetSystemDescriptor targetSystem, - RecoveryDescriptor recovery) { + private final CONFIG configuration; + + protected AbstractTemplateLoadedChange(String changeFileName, + String id, + String order, + String author, + Class> templateClass, + Constructor constructor, + List profiles, + boolean transactional, + boolean runAlways, + boolean systemTask, + CONFIG configuration, + TargetSystemDescriptor targetSystem, + RecoveryDescriptor recovery) { super(changeFileName, id, order, author, templateClass, constructor, runAlways, transactional, systemTask, targetSystem, recovery, false); this.profiles = profiles; this.transactional = transactional; this.configuration = configuration; - this.apply = apply; - this.rollback = rollback; - this.steps = steps; } - public Object getConfiguration() { - return configuration; - } - public Object getApply() { - return apply; - } - - public Object getRollback() { - return rollback; - } - - public Object getSteps() { - return steps; + public CONFIG getConfiguration() { + return configuration; } public List getProfiles() { return profiles; } - @SuppressWarnings("unchecked") - public Class> getTemplateClass() { - return (Class>) this.getImplementationClass(); + public Class> getTemplateClass() { + return (Class>) this.getImplementationClass(); } @Override @@ -101,5 +87,4 @@ public Method getApplyMethod() { public Optional getRollbackMethod() { return ReflectionUtil.findFirstAnnotatedMethod(getImplementationClass(), Rollback.class); } - } diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/SimpleTemplateLoadedChange.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/SimpleTemplateLoadedChange.java new file mode 100644 index 000000000..6abea2546 --- /dev/null +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/SimpleTemplateLoadedChange.java @@ -0,0 +1,74 @@ +/* + * Copyright 2023 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.internal.core.task.loaded; + +import io.flamingock.api.template.AbstractChangeTemplate; +import io.flamingock.internal.common.core.task.RecoveryDescriptor; +import io.flamingock.internal.common.core.task.TargetSystemDescriptor; + +import java.lang.reflect.Constructor; +import java.util.List; + +/** + * Loaded change for simple templates (single apply/rollback step). + * Used for templates annotated with {@code @ChangeTemplate(steppable = false)} or without annotation. + *

+ * The payloads are converted from raw YAML data (Object/Map) to typed values + * at load time, enabling early validation and cleaner executable tasks. + * + * @param the configuration type for the template + * @param the apply payload type + * @param the rollback payload type + */ +public class SimpleTemplateLoadedChange + extends AbstractTemplateLoadedChange { + + // Already converted to typed payload (no longer raw Object from YAML) + private final APPLY applyPayload; + private final ROLLBACK rollbackPayload; + + SimpleTemplateLoadedChange(String changeFileName, + String id, + String order, + String author, + Class> templateClass, + Constructor constructor, + List profiles, + boolean transactional, + boolean runAlways, + boolean systemTask, + CONFIG configuration, + APPLY applyPayload, + ROLLBACK rollbackPayload, + TargetSystemDescriptor targetSystem, + RecoveryDescriptor recovery) { + super(changeFileName, id, order, author, templateClass, constructor, profiles, transactional, runAlways, systemTask, configuration, targetSystem, recovery); + this.applyPayload = applyPayload; + this.rollbackPayload = rollbackPayload; + } + + public APPLY getApplyPayload() { + return applyPayload; + } + + public ROLLBACK getRollbackPayload() { + return rollbackPayload; + } + + public boolean hasRollback() { + return rollbackPayload != null; + } +} diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/SteppableTemplateLoadedChange.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/SteppableTemplateLoadedChange.java new file mode 100644 index 000000000..3bd42660c --- /dev/null +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/SteppableTemplateLoadedChange.java @@ -0,0 +1,63 @@ +/* + * Copyright 2023 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.internal.core.task.loaded; + +import io.flamingock.api.template.AbstractChangeTemplate; +import io.flamingock.api.template.TemplateStep; +import io.flamingock.internal.common.core.task.RecoveryDescriptor; +import io.flamingock.internal.common.core.task.TargetSystemDescriptor; + +import java.lang.reflect.Constructor; +import java.util.List; + +/** + * Loaded change for steppable templates (multiple steps). + * Used for templates annotated with {@code @ChangeTemplate(steppable = true)}. + *

+ * The steps are converted from raw YAML data (List of Maps) to typed TemplateStep objects + * at load time, enabling early validation and cleaner executable tasks. + * + * @param the configuration type for the template + * @param the apply payload type + * @param the rollback payload type + */ +public class SteppableTemplateLoadedChange + extends AbstractTemplateLoadedChange { + + private final List> steps; + + SteppableTemplateLoadedChange(String changeFileName, + String id, + String order, + String author, + Class> templateClass, + Constructor constructor, + List profiles, + boolean transactional, + boolean runAlways, + boolean systemTask, + CONFIG configuration, + List> steps, + TargetSystemDescriptor targetSystem, + RecoveryDescriptor recovery) { + super(changeFileName, id, order, author, templateClass, constructor, profiles, transactional, runAlways, systemTask, configuration, targetSystem, recovery); + this.steps = steps; + } + + public List> getSteps() { + return steps; + } +} 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 dd0776896..5ad6831e5 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 @@ -15,23 +15,28 @@ */ package io.flamingock.internal.core.task.loaded; +import io.flamingock.api.annotations.ChangeTemplate; +import io.flamingock.api.template.AbstractChangeTemplate; +import io.flamingock.api.template.TemplateStep; import io.flamingock.internal.common.core.error.FlamingockException; -import io.flamingock.api.template.ChangeTemplate; -import io.flamingock.internal.common.core.preview.PreviewConstructor; -import io.flamingock.internal.common.core.template.ChangeTemplateManager; import io.flamingock.internal.common.core.preview.AbstractPreviewTask; import io.flamingock.internal.common.core.preview.TemplatePreviewChange; import io.flamingock.internal.common.core.task.RecoveryDescriptor; import io.flamingock.internal.common.core.task.TargetSystemDescriptor; +import io.flamingock.internal.common.core.template.ChangeTemplateManager; +import io.flamingock.internal.util.FileUtil; +import io.flamingock.internal.util.Pair; import io.flamingock.internal.util.ReflectionUtil; import java.lang.reflect.Constructor; +import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; //TODO how to set transactional and runAlways -public class TemplateLoadedTaskBuilder implements LoadedTaskBuilder { +public class TemplateLoadedTaskBuilder implements LoadedTaskBuilder> { private String fileName; private String id; @@ -141,31 +146,158 @@ public TemplateLoadedTaskBuilder setSteps(Object steps) { } @Override - public TemplateLoadedChange build() { + @SuppressWarnings("unchecked") + public AbstractTemplateLoadedChange build() { // boolean isTaskTransactional = true;//TODO implement this. isTaskTransactionalAccordingTemplate(templateSpec); - Class> templateClass = ChangeTemplateManager.getTemplate(templateName) + Class> templateClass = ChangeTemplateManager.getTemplate(templateName) .orElseThrow(()-> new FlamingockException(String.format("Template[%s] not found. This is probably because template's name is wrong or template's library not imported", templateName))); Constructor constructor = ReflectionUtil.getDefaultConstructor(templateClass); - return new TemplateLoadedChange( - fileName, - id, - order, - author, - templateClass, - constructor, - profiles, - transactional, - runAlways, - system, - configuration, - apply, - rollback, - steps, - targetSystem, - recovery); + // Determine template type based on @ChangeTemplate annotation + // Note: Due to type erasure, we use Object bounds at construction time. + // The actual type safety comes from the conversion methods that use reflection + // to determine the real types at runtime. + ChangeTemplate annotation = templateClass.getAnnotation(ChangeTemplate.class); + boolean isSteppable = annotation != null && annotation.multiStep(); + + if (isSteppable) { + Class> steppableTemplateClass = + (Class>) + templateClass.asSubclass(AbstractChangeTemplate.class); + + // Convert steps at load time + List> convertedSteps = convertSteps(constructor, steps); + + return new SteppableTemplateLoadedChange<>( + fileName, + id, + order, + author, + steppableTemplateClass, + constructor, + profiles, + transactional, + runAlways, + system, + configuration, + convertedSteps, + targetSystem, + recovery); + } else { + // Default to SimpleTemplateLoadedChange for simple templates (steppable=false or missing annotation) + Class> simpleTemplateClass = + (Class>) + templateClass.asSubclass(AbstractChangeTemplate.class); + + // Convert apply/rollback to typed payloads at load time + Pair convertedPayloads = convertPayloads(constructor, apply, rollback); + + return new SimpleTemplateLoadedChange<>( + fileName, + id, + order, + author, + simpleTemplateClass, + constructor, + profiles, + transactional, + runAlways, + system, + configuration, + convertedPayloads.getFirst(), + convertedPayloads.getSecond(), + targetSystem, + recovery); + } + + } + + /** + * Converts raw apply/rollback data to typed payloads for simple templates. + * Returns Pair. + */ + private Pair convertPayloads(Constructor constructor, Object applyData, Object rollbackData) { + if (applyData == null) { + return new Pair<>(null, null); + } + + // Instantiate template temporarily to get payload types + AbstractChangeTemplate templateInstance; + try { + templateInstance = (AbstractChangeTemplate) constructor.newInstance(); + } catch (Exception e) { + throw new FlamingockException("Failed to instantiate template for type resolution: " + e.getMessage(), e); + } + + Class applyClass = templateInstance.getApplyPayloadClass(); + Class rollbackClass = templateInstance.getRollbackPayloadClass(); + + Object applyPayload = FileUtil.getFromMap(applyClass, applyData); + Object rollbackPayload = null; + + if (rollbackData != null && Void.class != rollbackClass) { + rollbackPayload = FileUtil.getFromMap(rollbackClass, rollbackData); + } + + return new Pair<>(applyPayload, rollbackPayload); + } + + /** + * Converts raw steps data from YAML to typed TemplateStep objects. + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + private List> convertSteps(Constructor constructor, Object stepsData) { + if (stepsData == null) { + return null; + } + + if (!(stepsData instanceof List)) { + throw new FlamingockException(String.format( + "Steps must be a List for steppable template change[%s], but got: %s", + id, stepsData.getClass().getSimpleName())); + } + + List stepsList = (List) stepsData; + if (stepsList.isEmpty()) { + return Collections.emptyList(); + } + + // Instantiate template temporarily to get payload types + AbstractChangeTemplate templateInstance; + try { + templateInstance = (AbstractChangeTemplate) constructor.newInstance(); + } catch (Exception e) { + throw new FlamingockException("Failed to instantiate template for type resolution: " + e.getMessage(), e); + } + + Class applyClass = templateInstance.getApplyPayloadClass(); + Class rollbackClass = templateInstance.getRollbackPayloadClass(); + + List> result = new ArrayList<>(); + + for (Object stepItem : stepsList) { + if (stepItem instanceof Map) { + Map stepMap = (Map) stepItem; + TemplateStep step = new TemplateStep<>(); + + Object applyItemData = stepMap.get("apply"); + if (applyItemData != null && Void.class != applyClass) { + step.setApplyPayload(FileUtil.getFromMap(applyClass, applyItemData)); + } + + Object rollbackItemData = stepMap.get("rollback"); + if (rollbackItemData != null && Void.class != rollbackClass) { + step.setRollbackPayload(FileUtil.getFromMap(rollbackClass, rollbackItemData)); + } + + result.add(step); + } else if (stepItem instanceof TemplateStep) { + result.add((TemplateStep) stepItem); + } + } + return result; } private TemplateLoadedTaskBuilder setPreview(TemplatePreviewChange preview) { diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/navigation/navigator/strategy/NonTxChangeProcessStrategy.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/navigation/navigator/strategy/NonTxChangeProcessStrategy.java index a22fa4456..de903961e 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/navigation/navigator/strategy/NonTxChangeProcessStrategy.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/navigation/navigator/strategy/NonTxChangeProcessStrategy.java @@ -29,6 +29,7 @@ import io.flamingock.internal.core.task.navigation.step.StartStep; import io.flamingock.internal.core.task.navigation.step.afteraudit.AfterExecutionAuditStep; import io.flamingock.internal.core.task.navigation.step.afteraudit.FailedAfterExecutionAuditStep; +import io.flamingock.internal.core.task.navigation.step.afteraudit.RollableStep; import io.flamingock.internal.core.task.navigation.step.execution.ExecutionStep; import io.flamingock.internal.core.task.navigation.step.rolledback.ManualRolledBackStep; import io.flamingock.internal.util.TimeService; @@ -118,10 +119,9 @@ protected ChangeProcessResult doApplyChange() { } private void rollbackActualChangeAndChain(FailedAfterExecutionAuditStep rollableFailedStep, ExecutionContext executionContext) { - rollableFailedStep.getRollbackSteps().forEach(rollableStep -> { - ManualRolledBackStep rolledBack = targetSystemOps.rollbackChange(rollableStep::rollback, buildExecutionRuntime()); - stepLogger.logManualRollbackResult(rolledBack); - auditAndLogManualRollback(rolledBack, executionContext); - }); + RollableStep rollableStep = rollableFailedStep.getRollbackStep(); + ManualRolledBackStep rolledBack = targetSystemOps.rollbackChange(rollableStep::rollback, buildExecutionRuntime()); + stepLogger.logManualRollbackResult(rolledBack); + auditAndLogManualRollback(rolledBack, executionContext); } } diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/navigation/navigator/strategy/SharedTxChangeProcessStrategy.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/navigation/navigator/strategy/SharedTxChangeProcessStrategy.java index 0465203e5..602b3a795 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/navigation/navigator/strategy/SharedTxChangeProcessStrategy.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/navigation/navigator/strategy/SharedTxChangeProcessStrategy.java @@ -30,6 +30,7 @@ import io.flamingock.internal.core.task.navigation.step.StartStep; import io.flamingock.internal.core.task.navigation.step.afteraudit.AfterExecutionAuditStep; import io.flamingock.internal.core.task.navigation.step.afteraudit.FailedAfterExecutionAuditStep; +import io.flamingock.internal.core.task.navigation.step.afteraudit.RollableStep; import io.flamingock.internal.core.task.navigation.step.complete.failed.CompleteAutoRolledBackStep; import io.flamingock.internal.core.task.navigation.step.execution.ExecutionStep; import io.flamingock.internal.core.task.navigation.step.rolledback.ManualRolledBackStep; @@ -148,13 +149,10 @@ private void auditIfExecutionFailure(Wrapper executionStep) { private void rollbackChain(RollableFailedStep rollableFailedStep, ExecutionContext executionContext) { // Skip first rollback (main change) as transaction already rolled it back - rollableFailedStep.getRollbackSteps() - .stream().skip(1) - .forEach(rollableStep -> { - ManualRolledBackStep rolledBack = targetSystemOps.rollbackChange(rollableStep::rollback, buildExecutionRuntime()); - stepLogger.logManualRollbackResult(rolledBack); - auditAndLogManualRollback(rolledBack, executionContext); - }); + RollableStep rollableStep = rollableFailedStep.getRollbackStep(); + ManualRolledBackStep rolledBack = targetSystemOps.rollbackChange(rollableStep::rollback, buildExecutionRuntime()); + stepLogger.logManualRollbackResult(rolledBack); + auditAndLogManualRollback(rolledBack, executionContext); } diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/navigation/navigator/strategy/SimpleTxChangeProcessStrategy.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/navigation/navigator/strategy/SimpleTxChangeProcessStrategy.java index f82b07b69..ad7e3becc 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/navigation/navigator/strategy/SimpleTxChangeProcessStrategy.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/navigation/navigator/strategy/SimpleTxChangeProcessStrategy.java @@ -138,7 +138,6 @@ protected ChangeProcessResult doApplyChange() { Throwable mainError = ((FailedAfterExecutionAuditStep)afterAudit) .getMainError(); auditAndLogAutoRollback(); - rollbackChain((RollableFailedStep) afterAudit, executionContext); ChangeResult result = resultBuilder .rolledBack() .error(mainError) @@ -147,16 +146,6 @@ protected ChangeProcessResult doApplyChange() { } } - private void rollbackChain(RollableFailedStep rollableFailedStep, ExecutionContext executionContext) { - // Skip first rollback (main change) as transaction already rolled it back - rollableFailedStep.getRollbackSteps() - .stream().skip(1) - .forEach(rollableStep -> { - ManualRolledBackStep rolledBack = targetSystemOps.rollbackChange(rollableStep::rollback, buildExecutionRuntime()); - stepLogger.logManualRollbackResult(rolledBack); - auditAndLogManualRollback(rolledBack, executionContext); - }); - } protected void auditAndLogAutoRollback() { diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/navigation/step/ExecutableStep.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/navigation/step/ExecutableStep.java index 12eb0f283..794303dd8 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/navigation/step/ExecutableStep.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/navigation/step/ExecutableStep.java @@ -30,7 +30,7 @@ public ExecutableStep(StartStep step) { public ExecutionStep execute(ExecutionRuntime executionRuntime) { StopWatch stopWatch = StopWatch.startAndGet(); try { - task.execute(executionRuntime); + task.apply(executionRuntime); return SuccessApplyStep.instance(this, stopWatch.getElapsed()); } catch (Throwable throwable) { return FailedExecutionStep.instance(this, stopWatch.getElapsed(), throwable); diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/navigation/step/RollableFailedStep.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/navigation/step/RollableFailedStep.java index 914676da2..6d1a8c5c0 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/navigation/step/RollableFailedStep.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/navigation/step/RollableFailedStep.java @@ -17,8 +17,9 @@ import io.flamingock.internal.core.task.navigation.step.afteraudit.RollableStep; -import java.util.List; - public interface RollableFailedStep extends FailedStep { - List getRollbackSteps(); + + RollableStep getRollbackStep(); + + } diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/navigation/step/afteraudit/FailedAfterExecutionAuditStep.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/navigation/step/afteraudit/FailedAfterExecutionAuditStep.java index 7e986c853..a21e8031e 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/navigation/step/afteraudit/FailedAfterExecutionAuditStep.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/navigation/step/afteraudit/FailedAfterExecutionAuditStep.java @@ -46,11 +46,8 @@ protected FailedAfterExecutionAuditStep(ExecutableTask task, boolean successAudi } @Override - public final List getRollbackSteps() { - return task.getRollbackChain() - .stream() - .map(RollableStep::new) - .collect(Collectors.toList()); + public final RollableStep getRollbackStep() { + return new RollableStep(getTask()); } } diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/navigation/step/afteraudit/RollableStep.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/navigation/step/afteraudit/RollableStep.java index 7d3922a7b..6b1e52938 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/navigation/step/afteraudit/RollableStep.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/navigation/step/afteraudit/RollableStep.java @@ -15,6 +15,7 @@ */ package io.flamingock.internal.core.task.navigation.step.afteraudit; +import io.flamingock.internal.core.task.executable.ExecutableTask; import io.flamingock.internal.core.task.navigation.step.AbstractTaskStep; import io.flamingock.internal.core.task.navigation.step.rolledback.ManualRolledBackStep; import io.flamingock.internal.core.runtime.ExecutionRuntime; @@ -22,21 +23,20 @@ import io.flamingock.internal.util.StopWatch; public final class RollableStep extends AbstractTaskStep { - private final Rollback rollback; - public RollableStep(Rollback rollback) { - super(rollback.getTask()); - this.rollback = rollback; + public RollableStep(ExecutableTask executableChange) { + super(executableChange); } - public ManualRolledBackStep rollback(ExecutionRuntime runtimeHelper) { + public ManualRolledBackStep rollback(ExecutionRuntime executionRuntime) { StopWatch stopWatch = StopWatch.startAndGet(); + ExecutableTask change = this.getTask(); try { - rollback.rollback(runtimeHelper); - return ManualRolledBackStep.successfulRollback(rollback, stopWatch.getElapsed()); + change.rollback(executionRuntime); + return ManualRolledBackStep.successfulRollback(change, stopWatch.getElapsed()); } catch (Throwable throwable) { - return ManualRolledBackStep.failedRollback(rollback, stopWatch.getElapsed(), throwable); + return ManualRolledBackStep.failedRollback(change, stopWatch.getElapsed(), throwable); } } diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/navigation/step/complete/failed/CompleteAutoRolledBackStep.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/navigation/step/complete/failed/CompleteAutoRolledBackStep.java index 6b09c24e8..895a324a5 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/navigation/step/complete/failed/CompleteAutoRolledBackStep.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/navigation/step/complete/failed/CompleteAutoRolledBackStep.java @@ -31,11 +31,7 @@ public CompleteAutoRolledBackStep(ExecutableTask task, boolean rollbackSuccess) @Override - public List getRollbackSteps() { - return task.getRollbackChain() - .stream() - .skip(1)//Skips the first one(its own rollback), because it's AutoRollback - .map(RollableStep::new) - .collect(Collectors.toList()); + public final RollableStep getRollbackStep() { + return new RollableStep(getTask()); } } diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/navigation/step/rolledback/FailedManualRolledBackStep.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/navigation/step/rolledback/FailedManualRolledBackStep.java index 865f1d7e2..adf6925cd 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/navigation/step/rolledback/FailedManualRolledBackStep.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/navigation/step/rolledback/FailedManualRolledBackStep.java @@ -15,6 +15,7 @@ */ package io.flamingock.internal.core.task.navigation.step.rolledback; +import io.flamingock.internal.core.task.executable.ExecutableTask; import io.flamingock.internal.core.task.navigation.step.FailedWithErrorStep; import io.flamingock.internal.core.task.executable.Rollback; @@ -23,8 +24,8 @@ public final class FailedManualRolledBackStep extends ManualRolledBackStep imple private final Throwable error; - FailedManualRolledBackStep(Rollback rollback, long duration, Throwable error) { - super(rollback, false, duration); + FailedManualRolledBackStep(ExecutableTask executableChange, long duration, Throwable error) { + super(executableChange, false, duration); this.error = error; } diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/navigation/step/rolledback/ManualRolledBackStep.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/navigation/step/rolledback/ManualRolledBackStep.java index 0f00111ee..a606eb72a 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/navigation/step/rolledback/ManualRolledBackStep.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/navigation/step/rolledback/ManualRolledBackStep.java @@ -15,6 +15,7 @@ */ package io.flamingock.internal.core.task.navigation.step.rolledback; +import io.flamingock.internal.core.task.executable.ExecutableTask; import io.flamingock.internal.core.task.navigation.step.FailedStep; import io.flamingock.internal.core.task.navigation.step.SuccessableStep; import io.flamingock.internal.core.task.navigation.step.complete.failed.CompletedFailedManualRollback; @@ -24,20 +25,18 @@ public class ManualRolledBackStep extends RolledBackStep implements SuccessableStep, FailedStep { private final long duration; - private final Rollback rollback; - protected ManualRolledBackStep(Rollback rollback, boolean rollbackSuccess, long duration) { - super(rollback.getTask(), rollbackSuccess); - this.rollback = rollback; + protected ManualRolledBackStep(ExecutableTask executableChange, boolean rollbackSuccess, long duration) { + super(executableChange, rollbackSuccess); this.duration = duration; } - public static ManualRolledBackStep successfulRollback(Rollback rollback, long duration) { - return new ManualRolledBackStep(rollback, true, duration); + public static ManualRolledBackStep successfulRollback(ExecutableTask executableChange, long duration) { + return new ManualRolledBackStep(executableChange, true, duration); } - public static ManualRolledBackStep failedRollback(Rollback rollback, long duration, Throwable error) { - return new FailedManualRolledBackStep(rollback, duration, error); + public static ManualRolledBackStep failedRollback(ExecutableTask executableChange, long duration, Throwable error) { + return new FailedManualRolledBackStep(executableChange, duration, error); } public CompletedFailedManualRollback applyAuditResult(Result auditResult) { @@ -48,7 +47,4 @@ public long getDuration() { return duration; } - public Rollback getRollback() { - return rollback; - } } diff --git a/core/flamingock-core/src/test/java/io/flamingock/internal/core/task/executable/SteppableTemplateExecutableTaskTest.java b/core/flamingock-core/src/test/java/io/flamingock/internal/core/task/executable/SteppableTemplateExecutableTaskTest.java new file mode 100644 index 000000000..9d7e344c9 --- /dev/null +++ b/core/flamingock-core/src/test/java/io/flamingock/internal/core/task/executable/SteppableTemplateExecutableTaskTest.java @@ -0,0 +1,481 @@ +/* + * 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.internal.core.task.executable; + +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.ChangeTemplate; +import io.flamingock.api.annotations.Rollback; +import io.flamingock.api.template.AbstractChangeTemplate; +import io.flamingock.api.template.TemplateStep; +import io.flamingock.internal.common.core.error.ChangeExecutionException; +import io.flamingock.internal.common.core.error.FlamingockException; +import io.flamingock.internal.common.core.recovery.action.ChangeAction; +import io.flamingock.internal.core.runtime.ExecutionRuntime; +import io.flamingock.internal.core.task.loaded.SteppableTemplateLoadedChange; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.invocation.InvocationOnMock; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class SteppableTemplateExecutableTaskTest { + + private ExecutionRuntime mockRuntime; + private SteppableTemplateLoadedChange mockDescriptor; + private Method applyMethod; + private Method rollbackMethod; + + // Track payloads for verification + private static List appliedPayloads; + private static List rolledBackPayloads; + private static boolean shouldFailOnApply; + private static int failAtStep; + private static boolean shouldFailOnRollback; + + /** + * Test template that tracks apply and rollback invocations. + */ + @ChangeTemplate(multiStep = true) + public static class TestSteppableTemplate extends AbstractChangeTemplate { + + public TestSteppableTemplate() { + super(); + } + + @Apply + public void apply() { + if (shouldFailOnApply && appliedPayloads.size() == failAtStep) { + throw new RuntimeException("Simulated apply failure at step " + failAtStep); + } + appliedPayloads.add(applyPayload); + } + + @Rollback + public void rollback() { + if (shouldFailOnRollback) { + throw new RuntimeException("Simulated rollback failure"); + } + rolledBackPayloads.add(rollbackPayload); + } + } + + /** + * Test template without rollback method. + */ + @ChangeTemplate(multiStep = true) + public static class TestTemplateWithoutRollback extends AbstractChangeTemplate { + + public TestTemplateWithoutRollback() { + super(); + } + + @Apply + public void apply() { + if (shouldFailOnApply && appliedPayloads.size() == failAtStep) { + throw new RuntimeException("Simulated apply failure at step " + failAtStep); + } + appliedPayloads.add(applyPayload); + } + } + + @BeforeEach + @SuppressWarnings("unchecked") + void setUp() throws NoSuchMethodException { + // Reset static tracking lists + appliedPayloads = new ArrayList<>(); + rolledBackPayloads = new ArrayList<>(); + shouldFailOnApply = false; + failAtStep = -1; + shouldFailOnRollback = false; + + mockRuntime = mock(ExecutionRuntime.class); + mockDescriptor = mock(SteppableTemplateLoadedChange.class); + + when(mockDescriptor.getId()).thenReturn("test-change-id"); + when(mockDescriptor.isTransactional()).thenReturn(true); + when(mockDescriptor.getConfiguration()).thenReturn(null); + when(mockDescriptor.getTemplateClass()).thenReturn((Class) TestSteppableTemplate.class); + + applyMethod = TestSteppableTemplate.class.getMethod("apply"); + rollbackMethod = TestSteppableTemplate.class.getMethod("rollback"); + + // Setup mock to return real template instance and invoke real methods + Constructor constructor = TestSteppableTemplate.class.getConstructor(); + doReturn(constructor).when(mockDescriptor).getConstructor(); + + when(mockRuntime.getInstance(any(Constructor.class))).thenAnswer(invocation -> { + Constructor ctor = invocation.getArgument(0); + return ctor.newInstance(); + }); + + doAnswer((InvocationOnMock invocation) -> { + Object instance = invocation.getArgument(0); + Method method = invocation.getArgument(1); + try { + return method.invoke(instance); + } catch (InvocationTargetException e) { + // Mimic ExecutionRuntime behavior - wrap in FlamingockException + throw FlamingockException.toFlamingockException(e); + } + }).when(mockRuntime).executeMethodWithInjectedDependencies(any(), any(Method.class)); + } + + @Test + @DisplayName("Should apply all steps in sequence (step 0, 1, 2)") + void shouldApplyAllStepsInOrder() { + // Given + List> steps = Arrays.asList( + new TemplateStep<>("apply-0", "rollback-0"), + new TemplateStep<>("apply-1", "rollback-1"), + new TemplateStep<>("apply-2", "rollback-2") + ); + when(mockDescriptor.getSteps()).thenReturn(steps); + + SteppableTemplateExecutableTask task = new SteppableTemplateExecutableTask<>( + "test-stage", + mockDescriptor, + ChangeAction.APPLY, + applyMethod, + rollbackMethod + ); + + // When + task.apply(mockRuntime); + + // Then + assertEquals(3, appliedPayloads.size()); + assertEquals("apply-0", appliedPayloads.get(0)); + assertEquals("apply-1", appliedPayloads.get(1)); + assertEquals("apply-2", appliedPayloads.get(2)); + } + + @Test + @DisplayName("Should rollback from failed step in reverse order") + void shouldRollbackFromFailedStepInReverseOrder() { + // Given + shouldFailOnApply = true; + failAtStep = 2; // Fail at step index 2 (third step) + + List> steps = Arrays.asList( + new TemplateStep<>("apply-0", "rollback-0"), + new TemplateStep<>("apply-1", "rollback-1"), + new TemplateStep<>("apply-2", "rollback-2") + ); + when(mockDescriptor.getSteps()).thenReturn(steps); + + SteppableTemplateExecutableTask task = new SteppableTemplateExecutableTask<>( + "test-stage", + mockDescriptor, + ChangeAction.APPLY, + applyMethod, + rollbackMethod + ); + + // When - apply fails at step 2 + assertThrows(ChangeExecutionException.class, () -> task.apply(mockRuntime)); + + // Then - verify steps 0 and 1 were applied (step 2 failed before completing) + assertEquals(2, appliedPayloads.size()); + assertEquals("apply-0", appliedPayloads.get(0)); + assertEquals("apply-1", appliedPayloads.get(1)); + + // When - rollback from stepIndex=2 (the failed step) down to 0 + task.rollback(mockRuntime); + + // Then - should rollback in reverse order (2, 1, 0) + // Note: step 2's rollback is attempted even though apply failed, because stepIndex was incremented before apply + assertEquals(3, rolledBackPayloads.size()); + assertEquals("rollback-2", rolledBackPayloads.get(0)); + assertEquals("rollback-1", rolledBackPayloads.get(1)); + assertEquals("rollback-0", rolledBackPayloads.get(2)); + } + + @Test + @DisplayName("Should set correct apply payload for each step during apply") + void shouldSetCorrectPayloadDuringApply() { + // Given + List> steps = Arrays.asList( + new TemplateStep<>("payload-A", "rollback-A"), + new TemplateStep<>("payload-B", "rollback-B") + ); + when(mockDescriptor.getSteps()).thenReturn(steps); + + SteppableTemplateExecutableTask task = new SteppableTemplateExecutableTask<>( + "test-stage", + mockDescriptor, + ChangeAction.APPLY, + applyMethod, + rollbackMethod + ); + + // When + task.apply(mockRuntime); + + // Then - payloads should be set correctly for each step + assertEquals(2, appliedPayloads.size()); + assertEquals("payload-A", appliedPayloads.get(0)); + assertEquals("payload-B", appliedPayloads.get(1)); + } + + @Test + @DisplayName("Should set correct rollback payload for each step during rollback") + void shouldSetCorrectPayloadDuringRollback() { + // Given - simulate failure at step 2 so we can rollback steps 0, 1, and 2 + shouldFailOnApply = true; + failAtStep = 2; + + List> steps = Arrays.asList( + new TemplateStep<>("apply-0", "rollback-payload-X"), + new TemplateStep<>("apply-1", "rollback-payload-Y"), + new TemplateStep<>("apply-2", "rollback-payload-Z") + ); + when(mockDescriptor.getSteps()).thenReturn(steps); + + SteppableTemplateExecutableTask task = new SteppableTemplateExecutableTask<>( + "test-stage", + mockDescriptor, + ChangeAction.APPLY, + applyMethod, + rollbackMethod + ); + + // When - apply fails at step 2 + assertThrows(ChangeExecutionException.class, () -> task.apply(mockRuntime)); + task.rollback(mockRuntime); + + // Then - rollback payloads should be set correctly (reverse order: 2, 1, 0) + // stepIndex is 2 when failure occurs, so rollback starts from step 2 + assertEquals(3, rolledBackPayloads.size()); + assertEquals("rollback-payload-Z", rolledBackPayloads.get(0)); // step 2 + assertEquals("rollback-payload-Y", rolledBackPayloads.get(1)); // step 1 + assertEquals("rollback-payload-X", rolledBackPayloads.get(2)); // step 0 + } + + @Test + @DisplayName("Should skip steps without rollback payload during rollback") + void shouldSkipStepsWithoutRollbackPayload() { + // Given - step 1 has no rollback payload, and we have 4 steps to allow failAtStep=3 + shouldFailOnApply = true; + failAtStep = 3; // Fail at step index 3 (fourth step) + + List> steps = Arrays.asList( + new TemplateStep<>("apply-0", "rollback-0"), + new TemplateStep<>("apply-1", null), // No rollback payload - should be skipped + new TemplateStep<>("apply-2", "rollback-2"), + new TemplateStep<>("apply-3", "rollback-3") + ); + when(mockDescriptor.getSteps()).thenReturn(steps); + + SteppableTemplateExecutableTask task = new SteppableTemplateExecutableTask<>( + "test-stage", + mockDescriptor, + ChangeAction.APPLY, + applyMethod, + rollbackMethod + ); + + // When - apply fails at step 3 (after steps 0, 1, 2 succeed) + assertThrows(ChangeExecutionException.class, () -> task.apply(mockRuntime)); + + // Verify 3 steps were applied + assertEquals(3, appliedPayloads.size()); + + task.rollback(mockRuntime); + + // Then - should rollback steps 3, 2, 0 (step 1 skipped due to null rollback payload) + // stepIndex=3 when failure occurs, rollback: 3, 2, 1(skipped), 0 + assertEquals(3, rolledBackPayloads.size()); + assertEquals("rollback-3", rolledBackPayloads.get(0)); // step 3 + assertEquals("rollback-2", rolledBackPayloads.get(1)); // step 2 + assertEquals("rollback-0", rolledBackPayloads.get(2)); // step 0 (step 1 skipped) + } + + @Test + @DisplayName("Should handle empty steps list without error") + void shouldHandleEmptyStepsList() { + // Given + List> emptySteps = Collections.emptyList(); + when(mockDescriptor.getSteps()).thenReturn(emptySteps); + + SteppableTemplateExecutableTask task = new SteppableTemplateExecutableTask<>( + "test-stage", + mockDescriptor, + ChangeAction.APPLY, + applyMethod, + rollbackMethod + ); + + // When & Then - should not throw + assertDoesNotThrow(() -> task.apply(mockRuntime)); + assertEquals(0, appliedPayloads.size()); + + // Rollback should also work with empty list + assertDoesNotThrow(() -> task.rollback(mockRuntime)); + assertEquals(0, rolledBackPayloads.size()); + } + + @Test + @DisplayName("Should not rollback when rollbackMethod is null") + @SuppressWarnings("unchecked") + void shouldNotRollbackWhenRollbackMethodIsNull() throws Exception { + // Given - use template without rollback method + doReturn(TestTemplateWithoutRollback.class).when(mockDescriptor).getTemplateClass(); + Constructor constructor = TestTemplateWithoutRollback.class.getConstructor(); + doReturn(constructor).when(mockDescriptor).getConstructor(); + + Method applyOnlyMethod = TestTemplateWithoutRollback.class.getMethod("apply"); + + shouldFailOnApply = true; + failAtStep = 1; + + List> steps = Arrays.asList( + new TemplateStep<>("apply-0", "rollback-0"), + new TemplateStep<>("apply-1", "rollback-1") + ); + when(mockDescriptor.getSteps()).thenReturn(steps); + + SteppableTemplateExecutableTask task = new SteppableTemplateExecutableTask<>( + "test-stage", + mockDescriptor, + ChangeAction.APPLY, + applyOnlyMethod, + null // No rollback method + ); + + // When + assertThrows(ChangeExecutionException.class, () -> task.apply(mockRuntime)); + task.rollback(mockRuntime); + + // Then - no rollback should have happened + assertEquals(0, rolledBackPayloads.size()); + } + + @Test + @DisplayName("Should throw ChangeExecutionException when apply fails") + void shouldThrowChangeExecutionExceptionOnApplyFailure() { + // Given + shouldFailOnApply = true; + failAtStep = 1; + + List> steps = Arrays.asList( + new TemplateStep<>("apply-0", "rollback-0"), + new TemplateStep<>("apply-1", "rollback-1") + ); + when(mockDescriptor.getSteps()).thenReturn(steps); + + SteppableTemplateExecutableTask task = new SteppableTemplateExecutableTask<>( + "test-stage", + mockDescriptor, + ChangeAction.APPLY, + applyMethod, + rollbackMethod + ); + + // When & Then + ChangeExecutionException exception = assertThrows( + ChangeExecutionException.class, + () -> task.apply(mockRuntime) + ); + + assertEquals("test-change-id", exception.getChangeId()); + assertTrue(exception.getMessage().contains("Simulated apply failure")); + } + + @Test + @DisplayName("Should throw ChangeExecutionException when rollback fails") + void shouldThrowChangeExecutionExceptionOnRollbackFailure() { + // Given + shouldFailOnApply = true; + failAtStep = 1; + shouldFailOnRollback = true; + + List> steps = Arrays.asList( + new TemplateStep<>("apply-0", "rollback-0"), + new TemplateStep<>("apply-1", "rollback-1") + ); + when(mockDescriptor.getSteps()).thenReturn(steps); + + SteppableTemplateExecutableTask task = new SteppableTemplateExecutableTask<>( + "test-stage", + mockDescriptor, + ChangeAction.APPLY, + applyMethod, + rollbackMethod + ); + + // When - apply fails + assertThrows(ChangeExecutionException.class, () -> task.apply(mockRuntime)); + + // When & Then - rollback also fails + ChangeExecutionException exception = assertThrows( + ChangeExecutionException.class, + () -> task.rollback(mockRuntime) + ); + + assertEquals("test-change-id", exception.getChangeId()); + assertTrue(exception.getMessage().contains("Simulated rollback failure")); + } + + @Test + @DisplayName("Should maintain stepIndex across apply and rollback") + void shouldMaintainStepIndexAcrossApplyAndRollback() { + // Given - fail at step 2 to verify stepIndex state + shouldFailOnApply = true; + failAtStep = 2; + + List> steps = Arrays.asList( + new TemplateStep<>("apply-0", "rollback-0"), + new TemplateStep<>("apply-1", "rollback-1"), + new TemplateStep<>("apply-2", "rollback-2"), + new TemplateStep<>("apply-3", "rollback-3") + ); + when(mockDescriptor.getSteps()).thenReturn(steps); + + SteppableTemplateExecutableTask task = new SteppableTemplateExecutableTask<>( + "test-stage", + mockDescriptor, + ChangeAction.APPLY, + applyMethod, + rollbackMethod + ); + + // When - apply fails at step 2 + assertThrows(ChangeExecutionException.class, () -> task.apply(mockRuntime)); + + // Verify - only steps 0 and 1 were applied (step 2 failed before completion) + assertEquals(2, appliedPayloads.size()); + + // When - rollback + task.rollback(mockRuntime); + + // Then - steps 2, 1, 0 should be rolled back + // stepIndex=2 when failure occurs (incremented before execute), so rollback starts from 2 + assertEquals(3, rolledBackPayloads.size()); + assertEquals("rollback-2", rolledBackPayloads.get(0)); + assertEquals("rollback-1", rolledBackPayloads.get(1)); + assertEquals("rollback-0", rolledBackPayloads.get(2)); + } +} diff --git a/core/flamingock-core/src/test/java/io/flamingock/internal/core/task/loaded/SimpleTemplateLoadedTaskBuilderTest.java b/core/flamingock-core/src/test/java/io/flamingock/internal/core/task/loaded/SimpleTemplateLoadedTaskBuilderTest.java index 7d4bc20d3..db8de7b72 100644 --- a/core/flamingock-core/src/test/java/io/flamingock/internal/core/task/loaded/SimpleTemplateLoadedTaskBuilderTest.java +++ b/core/flamingock-core/src/test/java/io/flamingock/internal/core/task/loaded/SimpleTemplateLoadedTaskBuilderTest.java @@ -17,7 +17,8 @@ import io.flamingock.internal.common.core.error.FlamingockException; import io.flamingock.internal.common.core.template.ChangeTemplateManager; -import io.flamingock.api.template.AbstractSimpleTemplate; +import io.flamingock.api.annotations.ChangeTemplate; +import io.flamingock.api.template.AbstractChangeTemplate; import io.flamingock.api.annotations.Apply; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -34,8 +35,9 @@ class SimpleTemplateLoadedTaskBuilderTest { private TemplateLoadedTaskBuilder builder; - // Simple test template implementation using the abstract class - public static class TestChangeTemplate extends AbstractSimpleTemplate { + // Simple test template implementation using the annotation + @ChangeTemplate + public static class TestChangeTemplate extends AbstractChangeTemplate { public TestChangeTemplate() { super(); @@ -67,18 +69,24 @@ void shouldBuildWithOrderInContentWhenOrderInContentPresentAndNoOrderInFileName( .setRunAlways(false) .setTransactional(true) .setSystem(false) - .setConfiguration(new Object()) - .setApply(new Object()) - .setRollback(new Object()); + .setConfiguration("testConfig") + .setApply("applyPayload") + .setRollback("rollbackPayload"); builder.setProfiles(Arrays.asList("test")); // When - TemplateLoadedChange result = builder.build(); + AbstractTemplateLoadedChange result = builder.build(); // Then + assertInstanceOf(SimpleTemplateLoadedChange.class, result); assertEquals("001", result.getOrder().orElse(null)); assertEquals("test-id", result.getId()); assertEquals("test-file.yml", result.getFileName()); + // Verify typed payloads are stored + SimpleTemplateLoadedChange simpleResult = (SimpleTemplateLoadedChange) result; + assertNotNull(simpleResult.getApplyPayload()); + assertNotNull(simpleResult.getRollbackPayload()); + assertTrue(simpleResult.hasRollback()); } } @@ -97,15 +105,16 @@ void shouldBuildWithOrderFromFileNameWhenOrderInContentIsNullAndOrderInFileNameI .setRunAlways(false) .setTransactional(true) .setSystem(false) - .setConfiguration(new Object()) - .setApply(new Object()) - .setRollback(new Object()); + .setConfiguration("testConfig") + .setApply("applyPayload") + .setRollback("rollbackPayload"); builder.setProfiles(Arrays.asList("test")); // When - TemplateLoadedChange result = builder.build(); + AbstractTemplateLoadedChange result = builder.build(); // Then + assertInstanceOf(SimpleTemplateLoadedChange.class, result); assertEquals("0002", result.getOrder().orElse(null)); assertEquals("test-id", result.getId()); assertEquals("_0002__test-file.yml", result.getFileName()); @@ -128,14 +137,15 @@ void shouldBuildWithOrderInContentWhenOrderInContentMatchesOrderInFileName() { builder.setProfiles(Arrays.asList("test")); builder.setTransactional(true) .setSystem(false) - .setConfiguration(new Object()) - .setApply(new Object()) - .setRollback(new Object()); + .setConfiguration("testConfig") + .setApply("applyPayload") + .setRollback("rollbackPayload"); // When - TemplateLoadedChange result = builder.build(); + AbstractTemplateLoadedChange result = builder.build(); // Then + assertInstanceOf(SimpleTemplateLoadedChange.class, result); assertEquals("003", result.getOrder().orElse(null)); assertEquals("test-id", result.getId()); assertEquals("_003__test-file.yml", result.getFileName()); diff --git a/core/flamingock-core/src/test/java/io/flamingock/internal/core/task/loaded/SteppableTemplateLoadedTaskBuilderTest.java b/core/flamingock-core/src/test/java/io/flamingock/internal/core/task/loaded/SteppableTemplateLoadedTaskBuilderTest.java index 24262e1ff..86eb18e0d 100644 --- a/core/flamingock-core/src/test/java/io/flamingock/internal/core/task/loaded/SteppableTemplateLoadedTaskBuilderTest.java +++ b/core/flamingock-core/src/test/java/io/flamingock/internal/core/task/loaded/SteppableTemplateLoadedTaskBuilderTest.java @@ -15,10 +15,12 @@ */ package io.flamingock.internal.core.task.loaded; +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.ChangeTemplate; +import io.flamingock.api.template.AbstractChangeTemplate; +import io.flamingock.api.template.TemplateStep; import io.flamingock.internal.common.core.error.FlamingockException; import io.flamingock.internal.common.core.template.ChangeTemplateManager; -import io.flamingock.api.template.AbstractSteppableTemplate; -import io.flamingock.api.annotations.Apply; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -38,8 +40,9 @@ class SteppableTemplateLoadedTaskBuilderTest { private TemplateLoadedTaskBuilder builder; - // Steppable test template implementation using the abstract class - public static class TestSteppableTemplate extends AbstractSteppableTemplate { + // Steppable test template implementation using the annotation + @ChangeTemplate(multiStep = true) + public static class TestSteppableTemplate extends AbstractChangeTemplate { public TestSteppableTemplate() { super(); @@ -77,15 +80,19 @@ void shouldBuildWithStepsWhenStepsProvided() { builder.setProfiles(Arrays.asList("test")); // When - TemplateLoadedChange result = builder.build(); + AbstractTemplateLoadedChange result = builder.build(); // Then + assertInstanceOf(SteppableTemplateLoadedChange.class, result); + SteppableTemplateLoadedChange steppableResult = (SteppableTemplateLoadedChange) result; assertNotNull(result); assertEquals("test-id", result.getId()); assertEquals("test-file.yml", result.getFileName()); - assertNotNull(result.getSteps()); - // Steps are stored as raw Object - conversion happens at execution time - assertEquals(rawSteps, result.getSteps()); + // Steps are now converted to List at load time + List> steps = steppableResult.getSteps(); + assertNotNull(steps); + assertEquals(2, steps.size()); + assertInstanceOf(TemplateStep.class, steps.get(0)); } } @@ -113,9 +120,10 @@ void shouldBuildWithOrderInContentForSteppableTemplate() { builder.setProfiles(Arrays.asList("test")); // When - TemplateLoadedChange result = builder.build(); + AbstractTemplateLoadedChange result = builder.build(); // Then + assertInstanceOf(SteppableTemplateLoadedChange.class, result); assertEquals("001", result.getOrder().orElse(null)); assertEquals("test-id", result.getId()); assertEquals("test-file.yml", result.getFileName()); @@ -146,9 +154,10 @@ void shouldBuildWithOrderFromFileNameForSteppableTemplate() { builder.setProfiles(Arrays.asList("test")); // When - TemplateLoadedChange result = builder.build(); + AbstractTemplateLoadedChange result = builder.build(); // Then + assertInstanceOf(SteppableTemplateLoadedChange.class, result); assertEquals("0002", result.getOrder().orElse(null)); assertEquals("test-id", result.getId()); assertEquals("_0002__test-file.yml", result.getFileName()); @@ -173,13 +182,16 @@ void shouldBuildWithEmptyStepsList() { builder.setProfiles(Arrays.asList("test")); // When - TemplateLoadedChange result = builder.build(); + AbstractTemplateLoadedChange result = builder.build(); // Then + assertInstanceOf(SteppableTemplateLoadedChange.class, result); + SteppableTemplateLoadedChange steppableResult = (SteppableTemplateLoadedChange) result; assertNotNull(result); assertEquals("test-id", result.getId()); - assertNotNull(result.getSteps()); - assertTrue(((List) result.getSteps()).isEmpty()); + List> steps = steppableResult.getSteps(); + assertNotNull(steps); + assertTrue(steps.isEmpty()); } } @@ -206,19 +218,21 @@ void shouldBuildWithMultipleSteps() { builder.setProfiles(Arrays.asList("test")); // When - TemplateLoadedChange result = builder.build(); + AbstractTemplateLoadedChange result = builder.build(); // Then + assertInstanceOf(SteppableTemplateLoadedChange.class, result); + SteppableTemplateLoadedChange steppableResult = (SteppableTemplateLoadedChange) result; assertNotNull(result); assertEquals("multi-step-change", result.getId()); - assertNotNull(result.getSteps()); - @SuppressWarnings("unchecked") - List> resultSteps = (List>) result.getSteps(); - assertEquals(3, resultSteps.size()); - // Verify steps are preserved in order - assertEquals("createCollection", resultSteps.get(0).get("apply")); - assertEquals("insertDocument", resultSteps.get(1).get("apply")); - assertEquals("createIndex", resultSteps.get(2).get("apply")); + // Steps are now converted to List at load time + List> steps = steppableResult.getSteps(); + assertNotNull(steps); + assertEquals(3, steps.size()); + // Verify steps are preserved in order - payloads are now typed objects + assertEquals("createCollection", steps.get(0).getApplyPayload()); + assertEquals("insertDocument", steps.get(1).getApplyPayload()); + assertEquals("createIndex", steps.get(2).getApplyPayload()); } } @@ -268,15 +282,17 @@ void shouldBuildWithStepsHavingOnlyApply() { builder.setProfiles(Arrays.asList("test")); // When - TemplateLoadedChange result = builder.build(); + AbstractTemplateLoadedChange result = builder.build(); // Then + assertInstanceOf(SteppableTemplateLoadedChange.class, result); + SteppableTemplateLoadedChange steppableResult = (SteppableTemplateLoadedChange) result; assertNotNull(result); - @SuppressWarnings("unchecked") - List> resultSteps = (List>) result.getSteps(); - assertEquals(2, resultSteps.size()); - assertNull(resultSteps.get(0).get("rollback")); - assertNull(resultSteps.get(1).get("rollback")); + // Steps are now converted to List at load time + List> steps = steppableResult.getSteps(); + assertEquals(2, steps.size()); + assertNull(steps.get(0).getRollbackPayload()); + assertNull(steps.get(1).getRollbackPayload()); } } @@ -296,12 +312,15 @@ void shouldBuildWithNullSteps() { builder.setProfiles(Arrays.asList("test")); // When - TemplateLoadedChange result = builder.build(); + AbstractTemplateLoadedChange result = builder.build(); // Then + assertInstanceOf(SteppableTemplateLoadedChange.class, result); + SteppableTemplateLoadedChange steppableResult = (SteppableTemplateLoadedChange) result; assertNotNull(result); assertEquals("test-id", result.getId()); - assertNull(result.getSteps()); + // Null steps remain null after conversion + assertNull(steppableResult.getSteps()); } } 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 47a128782..229b0da89 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 @@ -16,8 +16,6 @@ package io.flamingock.graalvm; import io.flamingock.api.template.AbstractChangeTemplate; -import io.flamingock.api.template.AbstractSimpleTemplate; -import io.flamingock.api.template.AbstractSteppableTemplate; import io.flamingock.api.template.ChangeTemplate; import io.flamingock.api.template.TemplateStep; import io.flamingock.internal.common.core.metadata.FlamingockMetadata; @@ -32,8 +30,10 @@ import io.flamingock.internal.core.task.loaded.AbstractLoadedChange; import io.flamingock.internal.core.task.loaded.AbstractLoadedTask; import io.flamingock.internal.core.task.loaded.AbstractReflectionLoadedTask; +import io.flamingock.internal.core.task.loaded.AbstractTemplateLoadedChange; import io.flamingock.internal.core.task.loaded.CodeLoadedChange; -import io.flamingock.internal.core.task.loaded.TemplateLoadedChange; +import io.flamingock.internal.core.task.loaded.SimpleTemplateLoadedChange; +import io.flamingock.internal.core.task.loaded.SteppableTemplateLoadedChange; import io.flamingock.internal.util.log.FlamingockLoggerFactory; import org.graalvm.nativeimage.hosted.Feature; import org.graalvm.nativeimage.hosted.RuntimeClassInitialization; @@ -73,7 +73,9 @@ private static void registerInternalClasses() { registerClassForReflection(AbstractReflectionLoadedTask.class.getName()); registerClassForReflection(AbstractLoadedChange.class.getName()); registerClassForReflection(CodeLoadedChange.class.getName()); - registerClassForReflection(TemplateLoadedChange.class.getName()); + registerClassForReflection(AbstractTemplateLoadedChange.class); + registerClassForReflection(SimpleTemplateLoadedChange.class); + registerClassForReflection(SteppableTemplateLoadedChange.class); //others registerClassForReflection(CoderResult.class.getName()); @@ -86,7 +88,9 @@ private static void initializeInternalClassesAtBuildTime() { logger.startInitializationProcess("internal classes"); initializeClassAtBuildTime(CodeLoadedChange.class); initializeClassAtBuildTime(AbstractLoadedChange.class); - initializeClassAtBuildTime(TemplateLoadedChange.class); + initializeClassAtBuildTime(AbstractTemplateLoadedChange.class); + initializeClassAtBuildTime(SimpleTemplateLoadedChange.class); + initializeClassAtBuildTime(SteppableTemplateLoadedChange.class); initializeClassAtBuildTime(ChangeTemplateManager.class); initializeClassAtBuildTime(RecoveryDescriptor.class); initializeClassAtBuildTime(FlamingockLoggerFactory.class); @@ -145,8 +149,6 @@ private void registerTemplates() { registerClassForReflection(ChangeTemplateManager.class); registerClassForReflection(ChangeTemplate.class); registerClassForReflection(AbstractChangeTemplate.class); - registerClassForReflection(AbstractSimpleTemplate.class); - registerClassForReflection(AbstractSteppableTemplate.class); registerClassForReflection(TemplateStep.class); ChangeTemplateManager.getTemplates().forEach(template -> { registerClassForReflection(template.getClass()); diff --git a/platform-plugins/flamingock-springboot-integration/src/main/java/io/flamingock/springboot/SpringbootProfileFilter.java b/platform-plugins/flamingock-springboot-integration/src/main/java/io/flamingock/springboot/SpringbootProfileFilter.java index 3fd787c8a..38394680f 100644 --- a/platform-plugins/flamingock-springboot-integration/src/main/java/io/flamingock/springboot/SpringbootProfileFilter.java +++ b/platform-plugins/flamingock-springboot-integration/src/main/java/io/flamingock/springboot/SpringbootProfileFilter.java @@ -18,8 +18,8 @@ import io.flamingock.internal.core.task.filter.TaskFilter; import io.flamingock.internal.core.task.loaded.AbstractLoadedTask; import io.flamingock.internal.core.task.loaded.AbstractReflectionLoadedTask; +import io.flamingock.internal.core.task.loaded.AbstractTemplateLoadedChange; import io.flamingock.internal.core.task.loaded.CodeLoadedChange; -import io.flamingock.internal.core.task.loaded.TemplateLoadedChange; import org.springframework.context.annotation.Profile; import java.util.Arrays; @@ -49,8 +49,8 @@ public boolean filter(AbstractLoadedTask descriptor) { } private boolean filter(AbstractReflectionLoadedTask reflectionDescriptor) { - if (TemplateLoadedChange.class.isAssignableFrom(reflectionDescriptor.getClass())) { - return filterTemplateChange((TemplateLoadedChange) reflectionDescriptor); + if (AbstractTemplateLoadedChange.class.isAssignableFrom(reflectionDescriptor.getClass())) { + return filterTemplateChange((AbstractTemplateLoadedChange) reflectionDescriptor); } else if (CodeLoadedChange.class.isAssignableFrom(reflectionDescriptor.getClass())) { return filterCodeChange((CodeLoadedChange) reflectionDescriptor); @@ -65,7 +65,7 @@ private boolean filter(AbstractReflectionLoadedTask reflectionDescriptor) { } - private boolean filterTemplateChange(TemplateLoadedChange reflectionDescriptor) { + private boolean filterTemplateChange(AbstractTemplateLoadedChange reflectionDescriptor) { return filterProfiles(reflectionDescriptor.getProfiles()); } diff --git a/platform-plugins/flamingock-springboot-integration/src/test/java/io/flamingock/springboot/SpringProfileFilterTemplateTaskTest.java b/platform-plugins/flamingock-springboot-integration/src/test/java/io/flamingock/springboot/SpringProfileFilterTemplateTaskTest.java index 5cc9da350..60c477696 100644 --- a/platform-plugins/flamingock-springboot-integration/src/test/java/io/flamingock/springboot/SpringProfileFilterTemplateTaskTest.java +++ b/platform-plugins/flamingock-springboot-integration/src/test/java/io/flamingock/springboot/SpringProfileFilterTemplateTaskTest.java @@ -16,9 +16,10 @@ package io.flamingock.springboot; import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.ChangeTemplate; import io.flamingock.internal.common.core.template.ChangeTemplateFileContent; import io.flamingock.internal.common.core.task.RecoveryDescriptor; -import io.flamingock.api.template.AbstractSimpleTemplate; +import io.flamingock.api.template.AbstractChangeTemplate; import io.flamingock.internal.common.core.template.ChangeTemplateManager; import io.flamingock.internal.common.core.preview.TemplatePreviewChange; import io.flamingock.internal.common.core.preview.builder.PreviewTaskBuilder; @@ -126,7 +127,8 @@ private AbstractLoadedTask getTemplateLoadedChange(String profiles) { } - public static abstract class TemplateSimulate extends AbstractSimpleTemplate { + @ChangeTemplate + public static abstract class TemplateSimulate extends AbstractChangeTemplate { public TemplateSimulate() { super(); } diff --git a/utils/general-util/src/main/java/io/flamingock/internal/util/NotThreadSafe.java b/utils/general-util/src/main/java/io/flamingock/internal/util/NotThreadSafe.java new file mode 100644 index 000000000..241d868b2 --- /dev/null +++ b/utils/general-util/src/main/java/io/flamingock/internal/util/NotThreadSafe.java @@ -0,0 +1,27 @@ +/* + * Copyright 2023 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.internal.util; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface NotThreadSafe { + +}