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 index fc2239fae..d9c8b4131 100644 --- 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 @@ -26,20 +26,20 @@ * 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. + * this annotation to specify their unique identifier and whether they process single or multiple steps. * - *
Simple templates (default, {@code steppable = false}): + *
Simple templates (default, {@code multiStep = false}): *
* id: create-users-table - * template: SqlTemplate + * template: sql # Uses the template id * apply: "CREATE TABLE users (id INT PRIMARY KEY)" * rollback: "DROP TABLE users" ** - *
Steppable templates ({@code steppable = true}) process multiple operations: + *
Steppable templates ({@code multiStep = true}) process multiple operations: *
* id: setup-orders
- * template: MongoTemplate
+ * template: mongo # Uses the template id
* steps:
* - apply: { type: createCollection, collection: orders }
* rollback: { type: dropCollection, collection: orders }
@@ -59,6 +59,18 @@
@Target(ElementType.TYPE)
public @interface ChangeTemplate {
+ /**
+ * Unique identifier for the template. Used in YAML files to reference
+ * the template (e.g., {@code template: "sql"}).
+ *
+ * This is a mandatory field - all templates must have a unique identifier.
+ * The id should be short, descriptive, and use lowercase with hyphens
+ * (e.g., "sql", "mongodb", "dynamodb").
+ *
+ * @return the unique template identifier
+ */
+ String id();
+
/**
* 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.
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 6227c5a24..88b24fffd 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
@@ -52,7 +52,7 @@ public static class AnotherAdditionalClass {
}
// Test template with custom generic types
- @ChangeTemplate
+ @ChangeTemplate(id = "test-custom-types")
public static class TestTemplateWithCustomTypes
extends AbstractChangeTemplate {
@@ -67,7 +67,7 @@ public void apply() {
}
// Test template with additional reflective classes
- @ChangeTemplate
+ @ChangeTemplate(id = "test-additional-classes")
public static class TestTemplateWithAdditionalClasses
extends AbstractChangeTemplate {
@@ -82,7 +82,7 @@ public void apply() {
}
// Test template with Void configuration
- @ChangeTemplate
+ @ChangeTemplate(id = "test-void-config")
public static class TestTemplateWithVoidConfig
extends AbstractChangeTemplate {
diff --git a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/metadata/FlamingockMetadata.java b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/metadata/FlamingockMetadata.java
index cb5f5e2ce..11895bfe3 100644
--- a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/metadata/FlamingockMetadata.java
+++ b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/metadata/FlamingockMetadata.java
@@ -16,7 +16,9 @@
package io.flamingock.internal.common.core.metadata;
import io.flamingock.internal.common.core.preview.PreviewPipeline;
+import io.flamingock.internal.common.core.template.TemplateMetadata;
+import java.util.List;
import java.util.Map;
public class FlamingockMetadata {
@@ -25,6 +27,7 @@ public class FlamingockMetadata {
private String configFile;
private Map properties;
private BuilderProviderInfo builderProvider;
+ private List templates;
public FlamingockMetadata() {
}
@@ -75,11 +78,20 @@ public boolean hasValidBuilderProvider() {
return builderProvider != null && builderProvider.isValid();
}
+ public List getTemplates() {
+ return templates;
+ }
+
+ public void setTemplates(List templates) {
+ this.templates = templates;
+ }
+
@Override
public String toString() {
return "FlamingockMetadata{" + "pipeline=" + pipeline +
", configFile='" + configFile + '\'' +
", builderProvider=" + builderProvider +
+ ", templates=" + templates +
'}';
}
}
diff --git a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/template/ChangeTemplateFactory.java b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/template/ChangeTemplateFactory.java
deleted file mode 100644
index 42fa137f1..000000000
--- a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/template/ChangeTemplateFactory.java
+++ /dev/null
@@ -1,68 +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.internal.common.core.template;
-
-import io.flamingock.api.template.ChangeTemplate;
-
-import java.util.Collection;
-import java.util.ServiceLoader;
-
-/**
- * Service provider interface for factories that produce {@link ChangeTemplate} instances.
- *
- * This interface enables a federated approach to template discovery, allowing modules
- * to provide multiple templates through a single service provider. Implementations of
- * this interface are discovered via Java's {@link ServiceLoader} mechanism during both:
- *
- * - GraalVM build-time processing for native image reflection registration
- * - Runtime template discovery and registration
- *
- *
- * To register a factory implementation, create a file at:
- * {@code META-INF/services/io.flamingock.internal.common.core.template.ChangeTemplateFactory}
- * containing the fully qualified class name of your implementation.
- *
- * Factory implementations should be stateless and thread-safe, as they may be
- * instantiated multiple times and accessed concurrently.
- *
- * @see ChangeTemplateManager
- * @see ServiceLoader
- */
-
-public interface ChangeTemplateFactory {
-
- /**
- * Returns a collection of {@link ChangeTemplate} instances provided by this factory.
- *
- * This method is called by {@link ChangeTemplateManager#getTemplates()} to discover templates
- * in a federated manner. It is invoked in two contexts:
- *
- * - During GraalVM build-time processing to register template classes for reflection
- * - During runtime initialization to populate the template registry
- *
- *
- * Implementations should:
- *
- * - Create and return new instances of templates each time this method is called
- * - Not maintain any state between invocations
- * - Be thread-safe
- * - Handle any exceptions internally to prevent disrupting the template discovery process
- *
- *
- * @return A collection of template instances provided by this factory
- */
- Collection> getTemplates();
-}
diff --git a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/template/ChangeTemplateManager.java b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/template/ChangeTemplateManager.java
index 17962bee0..6b0cb433c 100644
--- a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/template/ChangeTemplateManager.java
+++ b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/template/ChangeTemplateManager.java
@@ -20,45 +20,43 @@
import io.flamingock.internal.util.log.FlamingockLoggerFactory;
import org.slf4j.Logger;
-import java.util.ArrayList;
-import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
-import java.util.ServiceLoader;
/**
- * Manages the discovery, registration, and retrieval of {@link ChangeTemplate} implementations.
- *
- * This class serves two primary purposes in different contexts:
+ * Manages the registration and retrieval of {@link ChangeTemplate} implementations.
+ *
+ *
This class serves as a central registry for templates, which are initialized from
+ * {@link TemplateMetadata} discovered during annotation processing. Templates are
+ * indexed by their unique ID (from {@code @ChangeTemplate.id()}).
+ *
+ *
The initialization flow:
*
- * - GraalVM Build-time Context - The {@link #getTemplates()} method is called by
- * the GraalVM RegistrationFeature to discover all available templates. For each template,
- * the feature registers both the template class itself and all classes returned by
- * {@link ChangeTemplate#getReflectiveClasses()} for reflection in native images.
- * - Runtime Context - The {@link #loadTemplates()} method is called during
- * Flamingock initialization to populate the internal registry with all available templates
- * for use during execution.
+ * - Annotation processor discovers templates and serializes metadata to FlamingockMetadata
+ * - At runtime, {@link #initializeFromMetadata(List)} loads classes from metadata
+ * - Templates are looked up by ID via {@link #getTemplate(String)}
*
- *
- * Templates are discovered through Java's {@link ServiceLoader} mechanism from two sources:
- *
- * - Direct implementations of {@link ChangeTemplate} registered via SPI
- * - Templates provided by {@link ChangeTemplateFactory} implementations registered via SPI
- *
- *
- * Thread Safety Note: This class is not thread-safe during initialization. The
- * {@link #loadTemplates()} method modifies static state and is intended to be called only once
- * during application startup from a single thread. After initialization, the template registry
- * is effectively read-only and can be safely accessed concurrently.
+ *
+ *
Thread Safety Note: This class is not thread-safe during initialization.
+ * The {@link #initializeFromMetadata(List)} method modifies static state and should be called
+ * only once during application startup. After initialization, the registry is read-only and
+ * can be safely accessed concurrently.
*/
-
public final class ChangeTemplateManager {
private static final Logger logger = FlamingockLoggerFactory.getLogger("TemplateManager");
- private static final Map>> templates = new HashMap<>();
+ /**
+ * Internal storage for template entries, indexed by template ID.
+ */
+ private static final Map templates = new HashMap<>();
+
+ /**
+ * Flag to track if templates have been initialized.
+ */
+ private static boolean initialized = false;
/**
* Private constructor to prevent instantiation of this utility class.
@@ -66,96 +64,150 @@ public final class ChangeTemplateManager {
private ChangeTemplateManager() {
}
-
/**
- * Loads and registers all available templates from the classpath into the internal registry.
- *
- * This method is intended to be called once during Flamingock runtime initialization.
- * It discovers all templates via {@link #getTemplates()} and registers them in the internal
- * registry, indexed by their simple class name.
- *
- * This method is not thread-safe and should be called from a single thread during application
- * startup before any template lookups are performed.
+ * Initializes the template registry from metadata discovered during annotation processing.
+ *
+ *
This method loads template classes using their fully qualified class names from metadata
+ * and registers them by their unique ID. It should be called once during Flamingock
+ * initialization before any template lookups are performed.
+ *
+ *
If templates are already initialized, this method returns immediately.
+ *
+ * @param templateMetadataList list of template metadata from annotation processing
+ * @throws RuntimeException if a template class cannot be loaded
*/
@SuppressWarnings("unchecked")
- public static void loadTemplates() {
- logger.debug("Registering templates");
- getTemplates().forEach(template -> {
- Class extends ChangeTemplate, ?, ?>> templateClass = (Class extends ChangeTemplate, ?, ?>>) template.getClass();
- templates.put(templateClass.getSimpleName(), templateClass);
- logger.debug("registered template: {}", templateClass.getSimpleName());
- });
+ public static void initializeFromMetadata(List templateMetadataList) {
+ if (initialized) {
+ logger.debug("Templates already initialized, skipping");
+ return;
+ }
+
+ if (templateMetadataList == null || templateMetadataList.isEmpty()) {
+ logger.debug("No templates to initialize");
+ initialized = true;
+ return;
+ }
+
+ logger.debug("Initializing templates from metadata");
+
+ for (TemplateMetadata meta : templateMetadataList) {
+ try {
+ Class> clazz = Class.forName(meta.getFullyQualifiedClassName());
+ Class extends ChangeTemplate, ?, ?>> templateClass =
+ (Class extends ChangeTemplate, ?, ?>>) clazz;
+
+ TemplateEntry entry = new TemplateEntry(templateClass, meta);
+
+ // Register by ID only
+ templates.put(meta.getId(), entry);
+ logger.debug("Registered template: {} -> {}", meta.getId(), meta.getFullyQualifiedClassName());
+ } catch (ClassNotFoundException e) {
+ throw new RuntimeException("Template class not found: " + meta.getFullyQualifiedClassName(), e);
+ } catch (ClassCastException e) {
+ throw new RuntimeException("Class " + meta.getFullyQualifiedClassName() +
+ " does not implement ChangeTemplate", e);
+ }
+ }
+
+ initialized = true;
+ logger.debug("Initialized {} templates", templates.size());
}
/**
- * Discovers and returns all available templates from the classpath.
- *
- * This method is used in two contexts:
- *
- * - By the GraalVM RegistrationFeature during build time to discover templates that need
- * reflection registration for native image generation
- * - By the {@link #loadTemplates()} method during runtime initialization to populate
- * the internal template registry
- *
- *
- * Templates are discovered from two sources:
- *
- * - Direct implementations of {@link ChangeTemplate} registered via SPI
- * - Templates provided by {@link ChangeTemplateFactory} implementations registered via SPI
- *
- *
- * This method creates new instances of templates each time it's called and does not modify
- * any internal state.
+ * Retrieves a template class by its unique ID.
+ *
+ *
Template IDs are defined in the {@code @ChangeTemplate.id()} annotation
+ * and used in YAML files to reference templates (e.g., {@code template: sql}).
*
- * @return A collection of all discovered template instances
+ * @param templateId the unique template identifier
+ * @return an Optional containing the template class if found, or empty if not found
*/
- public static Collection> getTemplates() {
- logger.debug("Retrieving ChangeTemplates");
-
- //Loads the ChangeTemplates directly registered with SPI
- List> templateClasses = new ArrayList<>();
- for (ChangeTemplate, ?, ?> template : ServiceLoader.load(ChangeTemplate.class)) {
- templateClasses.add(template);
- }
-
- //Loads the ChangeTemplates from the federated ChangeTemplateFactory, registered with SPI
- for (ChangeTemplateFactory factory : ServiceLoader.load(ChangeTemplateFactory.class)) {
- templateClasses.addAll(factory.getTemplates());
- }
- logger.debug("returning ChangeTemplates");
+ public static Optional>> getTemplate(String templateId) {
+ TemplateEntry entry = templates.get(templateId);
+ return entry != null ? Optional.of(entry.templateClass) : Optional.empty();
+ }
- return templateClasses;
+ /**
+ * Retrieves template metadata by its unique ID.
+ *
+ * @param templateId the unique template identifier
+ * @return an Optional containing the template metadata if found, or empty if not found
+ */
+ public static Optional getTemplateMetadata(String templateId) {
+ TemplateEntry entry = templates.get(templateId);
+ return entry != null ? Optional.of(entry.metadata) : Optional.empty();
}
+ /**
+ * Checks if the template manager has been initialized.
+ *
+ * @return true if templates have been initialized, false otherwise
+ */
+ public static boolean isInitialized() {
+ return initialized;
+ }
/**
* Adds a template to the internal registry for testing purposes.
- *
- * This method is intended for use in test environments only to register mock or test templates.
+ *
+ *
This method is intended for use in test environments only to register mock or test templates.
* It directly modifies the internal template registry and is not thread-safe.
*
- * @param templateName The name to register the template under (typically the simple class name)
- * @param templateClass The template class to register
+ * @param templateId the unique template identifier
+ * @param templateClass the template class to register
+ * @param metadata the template metadata
+ */
+ @TestOnly
+ public static void addTemplate(String templateId, Class extends ChangeTemplate, ?, ?>> templateClass,
+ TemplateMetadata metadata) {
+ templates.put(templateId, new TemplateEntry(templateClass, metadata));
+ }
+
+ /**
+ * Adds a template to the internal registry for testing purposes.
+ *
+ *
This is a convenience overload that extracts multiStep from the @ChangeTemplate annotation.
+ *
+ * @param templateId the unique template identifier
+ * @param templateClass the template class to register
*/
@TestOnly
- public static void addTemplate(String templateName, Class extends ChangeTemplate, ?, ?>> templateClass) {
- templates.put(templateName, templateClass);
+ public static void addTemplate(String templateId, Class extends ChangeTemplate, ?, ?>> templateClass) {
+ // Extract multiStep from @ChangeTemplate annotation if present
+ boolean multiStep = false;
+ io.flamingock.api.annotations.ChangeTemplate annotation =
+ templateClass.getAnnotation(io.flamingock.api.annotations.ChangeTemplate.class);
+ if (annotation != null) {
+ multiStep = annotation.multiStep();
+ }
+ TemplateMetadata metadata = new TemplateMetadata(templateId, multiStep, templateClass.getName());
+ templates.put(templateId, new TemplateEntry(templateClass, metadata));
}
/**
- * Retrieves a template class by name from the internal registry.
- *
- * This method is used during runtime to look up template classes by their simple name.
- * It returns an {@link Optional} that will be empty if no template with the specified
- * name has been registered.
- *
- * This method is thread-safe after initialization (after {@link #loadTemplates()} has been called).
+ * Clears all templates from the internal registry and resets initialization state.
*
- * @param templateName The simple class name of the template to retrieve
- * @return An Optional containing the template class if found, or empty if not found
+ *
This method is intended for use in test environments only to reset the template registry
+ * between tests, ensuring test isolation.
*/
- public static Optional>> getTemplate(String templateName) {
- return Optional.ofNullable(templates.get(templateName));
+ @TestOnly
+ public static void clearTemplates() {
+ templates.clear();
+ initialized = false;
+ }
+
+ /**
+ * Internal class to hold template class and metadata together.
+ */
+ private static class TemplateEntry {
+ final Class extends ChangeTemplate, ?, ?>> templateClass;
+ final TemplateMetadata metadata;
+
+ TemplateEntry(Class extends ChangeTemplate, ?, ?>> templateClass, TemplateMetadata metadata) {
+ this.templateClass = templateClass;
+ this.metadata = metadata;
+ }
}
}
diff --git a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/template/TemplateMetadata.java b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/template/TemplateMetadata.java
new file mode 100644
index 000000000..7a6fc1ecf
--- /dev/null
+++ b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/template/TemplateMetadata.java
@@ -0,0 +1,146 @@
+/*
+ * 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.common.core.template;
+
+import java.util.Objects;
+
+/**
+ * Metadata describing a change template discovered during annotation processing.
+ *
+ * This class holds compile-time information about templates that is serialized
+ * to {@code FlamingockMetadata} and used at runtime to initialize the
+ * {@link ChangeTemplateManager}.
+ *
+ *
Templates are discovered via two mechanisms:
+ *
+ * - Annotation processing: Local templates with {@code @ChangeTemplate} annotation
+ * - File-based: External templates listed in {@code META-INF/flamingock/templates}
+ *
+ */
+public class TemplateMetadata {
+
+ /**
+ * The unique identifier from {@code @ChangeTemplate.id()}.
+ * Used in YAML files to reference the template.
+ */
+ private String id;
+
+ /**
+ * Whether this template processes multiple steps ({@code @ChangeTemplate.multiStep()}).
+ */
+ private boolean multiStep;
+
+ /**
+ * The fully qualified class name of the template for {@code Class.forName()}.
+ */
+ private String fullyQualifiedClassName;
+
+ /**
+ * Whether this template was discovered via a file-based registration
+ * ({@code META-INF/flamingock/templates}). This is operational metadata
+ * and does not affect identity ({@code equals}/{@code hashCode}).
+ */
+ private boolean fileRegistered;
+
+ /**
+ * Default constructor for Jackson deserialization.
+ */
+ public TemplateMetadata() {
+ }
+
+ /**
+ * Creates a new TemplateMetadata instance with {@code fileRegistered = false}.
+ *
+ * @param id the unique template identifier
+ * @param multiStep whether the template processes multiple steps
+ * @param fullyQualifiedClassName the fully qualified class name
+ */
+ public TemplateMetadata(String id, boolean multiStep, String fullyQualifiedClassName) {
+ this(id, multiStep, fullyQualifiedClassName, false);
+ }
+
+ /**
+ * Creates a new TemplateMetadata instance.
+ *
+ * @param id the unique template identifier
+ * @param multiStep whether the template processes multiple steps
+ * @param fullyQualifiedClassName the fully qualified class name
+ * @param fileRegistered whether this template was discovered via file registration
+ */
+ public TemplateMetadata(String id, boolean multiStep, String fullyQualifiedClassName, boolean fileRegistered) {
+ this.id = id;
+ this.multiStep = multiStep;
+ this.fullyQualifiedClassName = fullyQualifiedClassName;
+ this.fileRegistered = fileRegistered;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public boolean isMultiStep() {
+ return multiStep;
+ }
+
+ public void setMultiStep(boolean multiStep) {
+ this.multiStep = multiStep;
+ }
+
+ public String getFullyQualifiedClassName() {
+ return fullyQualifiedClassName;
+ }
+
+ public void setFullyQualifiedClassName(String fullyQualifiedClassName) {
+ this.fullyQualifiedClassName = fullyQualifiedClassName;
+ }
+
+ public boolean isFileRegistered() {
+ return fileRegistered;
+ }
+
+ public void setFileRegistered(boolean fileRegistered) {
+ this.fileRegistered = fileRegistered;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ TemplateMetadata that = (TemplateMetadata) o;
+ return multiStep == that.multiStep &&
+ Objects.equals(id, that.id) &&
+ Objects.equals(fullyQualifiedClassName, that.fullyQualifiedClassName);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, multiStep, fullyQualifiedClassName);
+ }
+
+ @Override
+ public String toString() {
+ return "TemplateMetadata{" +
+ "id='" + id + '\'' +
+ ", multiStep=" + multiStep +
+ ", fullyQualifiedClassName='" + fullyQualifiedClassName + '\'' +
+ ", fileRegistered=" + fileRegistered +
+ '}';
+ }
+}
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 055314921..e1d5a8e0d 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
@@ -19,6 +19,7 @@
import io.flamingock.internal.common.core.error.validation.ValidationError;
import io.flamingock.internal.common.core.error.validation.ValidationResult;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -47,12 +48,12 @@ public class TemplateValidator {
*/
public enum TemplateType {
/**
- * Template annotated with {@code @ChangeTemplate(steppable = false)} or without annotation.
+ * Template annotated with {@code @ChangeTemplate(multiStep = false)} or without annotation.
* Uses apply/rollback fields.
*/
SIMPLE,
/**
- * Template annotated with {@code @ChangeTemplate(steppable = true)}.
+ * Template annotated with {@code @ChangeTemplate(multiStep = true)}.
* Uses steps field.
*/
STEPPABLE,
@@ -65,10 +66,30 @@ public enum TemplateType {
private static final String ENTITY_TYPE = "template-change";
/**
- * Creates a new TemplateValidator and ensures templates are loaded.
+ * Map of template ID to metadata for compile-time validation.
+ */
+ private final Map templateMetadataMap;
+
+ /**
+ * Creates a TemplateValidator with templates from discovery result.
+ *
+ * @param templates list of discovered template metadata
+ */
+ public TemplateValidator(List templates) {
+ this.templateMetadataMap = new HashMap<>();
+ if (templates != null) {
+ for (TemplateMetadata meta : templates) {
+ templateMetadataMap.put(meta.getId(), meta);
+ }
+ }
+ }
+
+ /**
+ * Creates a TemplateValidator using ChangeTemplateManager (for runtime validation).
+ * This constructor is used when templates have already been initialized.
*/
public TemplateValidator() {
- ChangeTemplateManager.loadTemplates();
+ this.templateMetadataMap = null; // Will use ChangeTemplateManager
}
/**
@@ -88,20 +109,12 @@ public ValidationResult validate(ChangeTemplateFileContent content) {
return result;
}
- Optional>> templateClassOpt = ChangeTemplateManager.getTemplate(templateName);
+ TemplateType type = getTemplateType(templateName, changeId, result);
- if (!templateClassOpt.isPresent()) {
- result.add(new ValidationError(
- "Template '" + templateName + "' not found. Ensure the template is registered via SPI.",
- changeId,
- ENTITY_TYPE
- ));
+ if (result.hasErrors()) {
return result;
}
- Class extends io.flamingock.api.template.ChangeTemplate, ?, ?>> templateClass = templateClassOpt.get();
- TemplateType type = getTemplateType(templateClass);
-
switch (type) {
case SIMPLE:
validateSimpleTemplate(content, changeId, result);
@@ -117,18 +130,65 @@ public ValidationResult validate(ChangeTemplateFileContent content) {
return result;
}
+ /**
+ * Determines the template type based on metadata or runtime lookup.
+ *
+ * @param templateId the template identifier (ID, not class name)
+ * @param changeId the change ID for error reporting
+ * @param result the validation result to add errors to
+ * @return the TemplateType
+ */
+ private TemplateType getTemplateType(String templateId, String changeId, ValidationResult result) {
+ // Try compile-time metadata first
+ if (templateMetadataMap != null) {
+ TemplateMetadata meta = templateMetadataMap.get(templateId);
+ if (meta != null) {
+ return meta.isMultiStep() ? TemplateType.STEPPABLE : TemplateType.SIMPLE;
+ }
+ // Template not found in compile-time metadata
+ result.add(new ValidationError(
+ "Template '" + templateId + "' not found. Ensure the template class has @ChangeTemplate(id = \"" +
+ templateId + "\") annotation.",
+ changeId,
+ ENTITY_TYPE
+ ));
+ return TemplateType.UNKNOWN;
+ }
+
+ // Fall back to runtime lookup via ChangeTemplateManager
+ Optional metaOpt = ChangeTemplateManager.getTemplateMetadata(templateId);
+ if (metaOpt.isPresent()) {
+ return metaOpt.get().isMultiStep() ? TemplateType.STEPPABLE : TemplateType.SIMPLE;
+ }
+
+ // Try class lookup as fallback for old-style templates
+ Optional>> templateClassOpt =
+ ChangeTemplateManager.getTemplate(templateId);
+
+ if (!templateClassOpt.isPresent()) {
+ result.add(new ValidationError(
+ "Template '" + templateId + "' not found. Ensure the template is registered.",
+ changeId,
+ ENTITY_TYPE
+ ));
+ return TemplateType.UNKNOWN;
+ }
+
+ return getTemplateTypeFromClass(templateClassOpt.get());
+ }
+
/**
* Determines the template type based on the {@link ChangeTemplate} annotation.
*
* @param templateClass the template class to check
* @return the TemplateType (SIMPLE or STEPPABLE). Returns SIMPLE by default if annotation is missing.
*/
- public TemplateType getTemplateType(Class extends io.flamingock.api.template.ChangeTemplate, ?, ?>> templateClass) {
+ public TemplateType getTemplateTypeFromClass(Class extends io.flamingock.api.template.ChangeTemplate, ?, ?>> templateClass) {
ChangeTemplate annotation = templateClass.getAnnotation(ChangeTemplate.class);
if (annotation != null && annotation.multiStep()) {
return TemplateType.STEPPABLE;
}
- // Default to SIMPLE (including when annotation is missing or steppable=false)
+ // Default to SIMPLE (including when annotation is missing or multiStep=false)
return TemplateType.SIMPLE;
}
diff --git a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/util/Serializer.java b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/util/Serializer.java
index 2d7ef340c..f45995703 100644
--- a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/util/Serializer.java
+++ b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/util/Serializer.java
@@ -23,6 +23,7 @@
import io.flamingock.internal.common.core.metadata.FlamingockMetadata;
import io.flamingock.internal.common.core.preview.PreviewStage;
import io.flamingock.internal.common.core.task.TaskDescriptor;
+import io.flamingock.internal.common.core.template.TemplateMetadata;
import javax.annotation.processing.ProcessingEnvironment;
import javax.tools.FileObject;
@@ -69,6 +70,18 @@ private void serializeClassesList(FlamingockMetadata metadata) {
}
}
+ // Add template classes for GraalVM reflection
+ if (metadata.getTemplates() != null) {
+ for (TemplateMetadata template : metadata.getTemplates()) {
+ try {
+ writer.write(template.getFullyQualifiedClassName());
+ writer.write(System.lineSeparator());
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
PreviewPipeline pipeline = metadata.getPipeline();
if(pipeline.getSystemStage() != null) {
serializeClassesFromStage(writer, pipeline.getSystemStage());;
diff --git a/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/template/ChangeTemplateManagerTest.java b/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/template/ChangeTemplateManagerTest.java
new file mode 100644
index 000000000..a94a7de0f
--- /dev/null
+++ b/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/template/ChangeTemplateManagerTest.java
@@ -0,0 +1,330 @@
+/*
+ * Copyright 2026 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.common.core.template;
+
+import io.flamingock.api.annotations.Apply;
+import io.flamingock.api.annotations.ChangeTemplate;
+import io.flamingock.api.template.AbstractChangeTemplate;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class ChangeTemplateManagerTest {
+
+ @BeforeEach
+ void setUp() {
+ // Clear templates before each test for isolation
+ ChangeTemplateManager.clearTemplates();
+ }
+
+ @AfterEach
+ void tearDown() {
+ // Clear templates after each test to avoid polluting other tests
+ ChangeTemplateManager.clearTemplates();
+ }
+
+ // Test template for unit tests
+ @ChangeTemplate(id = "UnitTestTemplate")
+ public static class UnitTestTemplate extends AbstractChangeTemplate {
+ public UnitTestTemplate() {
+ super();
+ }
+
+ @Apply
+ public void apply() {
+ // Test implementation
+ }
+ }
+
+ // Second test template for multiple registration tests
+ @ChangeTemplate(id = "SecondUnitTestTemplate")
+ public static class SecondUnitTestTemplate extends AbstractChangeTemplate {
+ public SecondUnitTestTemplate() {
+ super();
+ }
+
+ @Apply
+ public void apply() {
+ // Test implementation
+ }
+ }
+
+ // Third test template for overwrite tests
+ @ChangeTemplate(id = "OverwriteTestTemplate")
+ public static class OverwriteTestTemplate extends AbstractChangeTemplate {
+ public OverwriteTestTemplate() {
+ super();
+ }
+
+ @Apply
+ public void apply() {
+ // Test implementation
+ }
+ }
+
+ @Nested
+ @DisplayName("getTemplate tests")
+ class GetTemplateTests {
+
+ @Test
+ @DisplayName("Should return empty Optional for non-existent template")
+ void shouldReturnEmptyOptionalForNonExistentTemplate() {
+ Optional>> result =
+ ChangeTemplateManager.getTemplate("NonExistentTemplate");
+
+ assertFalse(result.isPresent(), "Should return empty Optional for non-existent template");
+ }
+
+ @Test
+ @DisplayName("Should return template class when registered")
+ void shouldReturnTemplateClassWhenRegistered() {
+ ChangeTemplateManager.addTemplate("UnitTestTemplate", UnitTestTemplate.class);
+
+ Optional>> result =
+ ChangeTemplateManager.getTemplate("UnitTestTemplate");
+
+ assertTrue(result.isPresent(), "Should return present Optional for registered template");
+ assertEquals(UnitTestTemplate.class, result.get());
+ }
+
+ @Test
+ @DisplayName("Should return empty Optional for null template name")
+ void shouldReturnEmptyOptionalForNullTemplateName() {
+ ChangeTemplateManager.addTemplate("UnitTestTemplate", UnitTestTemplate.class);
+
+ Optional>> result =
+ ChangeTemplateManager.getTemplate(null);
+
+ assertFalse(result.isPresent(), "Should return empty Optional for null template name");
+ }
+ }
+
+ @Nested
+ @DisplayName("addTemplate tests")
+ class AddTemplateTests {
+
+ @Test
+ @DisplayName("Should add template successfully")
+ void shouldAddTemplateSuccessfully() {
+ ChangeTemplateManager.addTemplate("UnitTestTemplate", UnitTestTemplate.class);
+
+ Optional>> result =
+ ChangeTemplateManager.getTemplate("UnitTestTemplate");
+
+ assertTrue(result.isPresent());
+ assertEquals(UnitTestTemplate.class, result.get());
+ }
+
+ @Test
+ @DisplayName("Should add multiple templates")
+ void shouldAddMultipleTemplates() {
+ ChangeTemplateManager.addTemplate("UnitTestTemplate", UnitTestTemplate.class);
+ ChangeTemplateManager.addTemplate("SecondUnitTestTemplate", SecondUnitTestTemplate.class);
+
+ Optional>> result1 =
+ ChangeTemplateManager.getTemplate("UnitTestTemplate");
+ Optional>> result2 =
+ ChangeTemplateManager.getTemplate("SecondUnitTestTemplate");
+
+ assertTrue(result1.isPresent());
+ assertTrue(result2.isPresent());
+ assertEquals(UnitTestTemplate.class, result1.get());
+ assertEquals(SecondUnitTestTemplate.class, result2.get());
+ }
+
+ @Test
+ @DisplayName("Should overwrite existing template with same name")
+ void shouldOverwriteExistingTemplateWithSameName() {
+ ChangeTemplateManager.addTemplate("SharedName", UnitTestTemplate.class);
+ ChangeTemplateManager.addTemplate("SharedName", OverwriteTestTemplate.class);
+
+ Optional>> result =
+ ChangeTemplateManager.getTemplate("SharedName");
+
+ assertTrue(result.isPresent());
+ assertEquals(OverwriteTestTemplate.class, result.get(),
+ "Should have overwritten with the new template class");
+ }
+ }
+
+ @Nested
+ @DisplayName("clearTemplates tests")
+ class ClearTemplatesTests {
+
+ @Test
+ @DisplayName("Should clear all templates")
+ void shouldClearAllTemplates() {
+ ChangeTemplateManager.addTemplate("UnitTestTemplate", UnitTestTemplate.class);
+ ChangeTemplateManager.addTemplate("SecondUnitTestTemplate", SecondUnitTestTemplate.class);
+
+ // Verify templates are registered
+ assertTrue(ChangeTemplateManager.getTemplate("UnitTestTemplate").isPresent());
+ assertTrue(ChangeTemplateManager.getTemplate("SecondUnitTestTemplate").isPresent());
+
+ // Clear templates
+ ChangeTemplateManager.clearTemplates();
+
+ // Verify templates are cleared
+ assertFalse(ChangeTemplateManager.getTemplate("UnitTestTemplate").isPresent());
+ assertFalse(ChangeTemplateManager.getTemplate("SecondUnitTestTemplate").isPresent());
+ }
+
+ @Test
+ @DisplayName("Should be safe to clear empty registry")
+ void shouldBeSafeToClearEmptyRegistry() {
+ assertDoesNotThrow(() -> ChangeTemplateManager.clearTemplates());
+ }
+
+ @Test
+ @DisplayName("Should reset initialized flag when clearing")
+ void shouldResetInitializedFlagWhenClearing() {
+ // Initialize with some templates
+ List metadataList = Collections.singletonList(
+ new TemplateMetadata("UnitTestTemplate", false, UnitTestTemplate.class.getName())
+ );
+ ChangeTemplateManager.initializeFromMetadata(metadataList);
+ assertTrue(ChangeTemplateManager.isInitialized());
+
+ // Clear templates
+ ChangeTemplateManager.clearTemplates();
+
+ // Should be able to reinitialize
+ assertFalse(ChangeTemplateManager.isInitialized());
+ }
+ }
+
+ @Nested
+ @DisplayName("initializeFromMetadata tests")
+ class InitializeFromMetadataTests {
+
+ @Test
+ @DisplayName("Should initialize templates from metadata")
+ void shouldInitializeTemplatesFromMetadata() {
+ List metadataList = Arrays.asList(
+ new TemplateMetadata("UnitTestTemplate", false, UnitTestTemplate.class.getName()),
+ new TemplateMetadata("SecondUnitTestTemplate", false, SecondUnitTestTemplate.class.getName())
+ );
+
+ ChangeTemplateManager.initializeFromMetadata(metadataList);
+
+ assertTrue(ChangeTemplateManager.isInitialized());
+ assertTrue(ChangeTemplateManager.getTemplate("UnitTestTemplate").isPresent());
+ assertTrue(ChangeTemplateManager.getTemplate("SecondUnitTestTemplate").isPresent());
+ assertEquals(UnitTestTemplate.class, ChangeTemplateManager.getTemplate("UnitTestTemplate").get());
+ assertEquals(SecondUnitTestTemplate.class, ChangeTemplateManager.getTemplate("SecondUnitTestTemplate").get());
+ }
+
+ @Test
+ @DisplayName("Should handle empty metadata list")
+ void shouldHandleEmptyMetadataList() {
+ ChangeTemplateManager.initializeFromMetadata(Collections.emptyList());
+
+ assertTrue(ChangeTemplateManager.isInitialized());
+ assertFalse(ChangeTemplateManager.getTemplate("UnitTestTemplate").isPresent());
+ }
+
+ @Test
+ @DisplayName("Should handle null metadata list")
+ void shouldHandleNullMetadataList() {
+ ChangeTemplateManager.initializeFromMetadata(null);
+
+ assertTrue(ChangeTemplateManager.isInitialized());
+ }
+
+ @Test
+ @DisplayName("Should not reinitialize if already initialized")
+ void shouldNotReinitializeIfAlreadyInitialized() {
+ List firstMetadata = Collections.singletonList(
+ new TemplateMetadata("UnitTestTemplate", false, UnitTestTemplate.class.getName())
+ );
+ List secondMetadata = Collections.singletonList(
+ new TemplateMetadata("SecondUnitTestTemplate", false, SecondUnitTestTemplate.class.getName())
+ );
+
+ ChangeTemplateManager.initializeFromMetadata(firstMetadata);
+ ChangeTemplateManager.initializeFromMetadata(secondMetadata);
+
+ // Only first template should be present (second initialization was skipped)
+ assertTrue(ChangeTemplateManager.getTemplate("UnitTestTemplate").isPresent());
+ assertFalse(ChangeTemplateManager.getTemplate("SecondUnitTestTemplate").isPresent());
+ }
+
+ @Test
+ @DisplayName("Should throw exception for non-existent class")
+ void shouldThrowExceptionForNonExistentClass() {
+ List metadataList = Collections.singletonList(
+ new TemplateMetadata("NonExistent", false, "com.example.NonExistentClass")
+ );
+
+ assertThrows(RuntimeException.class, () ->
+ ChangeTemplateManager.initializeFromMetadata(metadataList));
+ }
+ }
+
+ @Nested
+ @DisplayName("getTemplateMetadata tests")
+ class GetTemplateMetadataTests {
+
+ @Test
+ @DisplayName("Should return metadata when template is registered")
+ void shouldReturnMetadataWhenTemplateIsRegistered() {
+ TemplateMetadata metadata = new TemplateMetadata("UnitTestTemplate", true, UnitTestTemplate.class.getName());
+ ChangeTemplateManager.addTemplate("UnitTestTemplate", UnitTestTemplate.class, metadata);
+
+ Optional result = ChangeTemplateManager.getTemplateMetadata("UnitTestTemplate");
+
+ assertTrue(result.isPresent());
+ assertEquals("UnitTestTemplate", result.get().getId());
+ assertTrue(result.get().isMultiStep());
+ assertEquals(UnitTestTemplate.class.getName(), result.get().getFullyQualifiedClassName());
+ }
+
+ @Test
+ @DisplayName("Should return empty Optional for non-existent template")
+ void shouldReturnEmptyOptionalForNonExistentTemplate() {
+ Optional result = ChangeTemplateManager.getTemplateMetadata("NonExistent");
+
+ assertFalse(result.isPresent());
+ }
+ }
+
+ @Nested
+ @DisplayName("isInitialized tests")
+ class IsInitializedTests {
+
+ @Test
+ @DisplayName("Should return false before initialization")
+ void shouldReturnFalseBeforeInitialization() {
+ assertFalse(ChangeTemplateManager.isInitialized());
+ }
+
+ @Test
+ @DisplayName("Should return true after initialization")
+ void shouldReturnTrueAfterInitialization() {
+ ChangeTemplateManager.initializeFromMetadata(Collections.emptyList());
+ assertTrue(ChangeTemplateManager.isInitialized());
+ }
+ }
+}
diff --git a/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/template/FactoryProvidedTemplate.java b/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/template/FactoryProvidedTemplate.java
new file mode 100644
index 000000000..ff8ca0dd6
--- /dev/null
+++ b/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/template/FactoryProvidedTemplate.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2026 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.common.core.template;
+
+import io.flamingock.api.annotations.Apply;
+import io.flamingock.api.annotations.ChangeTemplate;
+import io.flamingock.api.template.AbstractChangeTemplate;
+
+/**
+ * Test template that is provided by TestChangeTemplateFactory.
+ * This template is NOT registered directly via SPI, but is instead provided
+ * by a ChangeTemplateFactory implementation to test the federated loading mechanism.
+ */
+@ChangeTemplate(id = "FactoryProvidedTemplate")
+public class FactoryProvidedTemplate extends AbstractChangeTemplate {
+
+ public FactoryProvidedTemplate() {
+ super();
+ }
+
+ @Apply
+ public void apply() {
+ // Test implementation - does nothing
+ }
+}
diff --git a/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/template/SPITestSteppableTemplate.java b/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/template/SPITestSteppableTemplate.java
new file mode 100644
index 000000000..826c56447
--- /dev/null
+++ b/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/template/SPITestSteppableTemplate.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2026 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.common.core.template;
+
+import io.flamingock.api.annotations.Apply;
+import io.flamingock.api.annotations.ChangeTemplate;
+import io.flamingock.api.template.AbstractChangeTemplate;
+
+/**
+ * Test steppable template for SPI loading tests.
+ * This template is registered via META-INF/services/io.flamingock.api.template.ChangeTemplate
+ * and is used to verify that ChangeTemplateManager correctly discovers steppable templates via ServiceLoader.
+ */
+@ChangeTemplate(id = "SPITestSteppableTemplate", multiStep = true)
+public class SPITestSteppableTemplate extends AbstractChangeTemplate {
+
+ public SPITestSteppableTemplate() {
+ super();
+ }
+
+ @Apply
+ public void apply() {
+ // Test implementation - does nothing
+ }
+}
diff --git a/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/template/SPITestTemplate.java b/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/template/SPITestTemplate.java
new file mode 100644
index 000000000..03b3cb6f3
--- /dev/null
+++ b/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/template/SPITestTemplate.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2026 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.common.core.template;
+
+import io.flamingock.api.annotations.Apply;
+import io.flamingock.api.annotations.ChangeTemplate;
+import io.flamingock.api.template.AbstractChangeTemplate;
+
+/**
+ * Test template for SPI loading tests.
+ * This template is registered via META-INF/services/io.flamingock.api.template.ChangeTemplate
+ * and is used to verify that ChangeTemplateManager correctly discovers templates via ServiceLoader.
+ */
+@ChangeTemplate(id = "SPITestTemplate")
+public class SPITestTemplate extends AbstractChangeTemplate {
+
+ public SPITestTemplate() {
+ super();
+ }
+
+ @Apply
+ public void apply() {
+ // Test implementation - does nothing
+ }
+}
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 ba399f8e6..a1d605d4c 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
@@ -37,7 +37,7 @@ class TemplateValidatorTest {
private TemplateValidator validator;
// Test template with @ChangeTemplate (simple template)
- @ChangeTemplate
+ @ChangeTemplate(id = "TestSimpleTemplate")
public static class TestSimpleTemplate extends AbstractChangeTemplate {
public TestSimpleTemplate() {
super();
@@ -49,8 +49,8 @@ public void apply() {
}
}
- // Test template with @ChangeTemplate(steppable = true)
- @ChangeTemplate(multiStep = true)
+ // Test template with @ChangeTemplate(multiStep = true)
+ @ChangeTemplate(id = "TestSteppableTemplate", multiStep = true)
public static class TestSteppableTemplate extends AbstractChangeTemplate {
public TestSteppableTemplate() {
super();
@@ -71,20 +71,20 @@ void setUp() {
}
@Nested
- @DisplayName("getTemplateType tests")
- class GetTemplateTypeTests {
+ @DisplayName("getTemplateTypeFromClass tests")
+ class GetTemplateTypeFromClassTests {
@Test
- @DisplayName("Should return SIMPLE for AbstractSimpleTemplate subclass")
- void shouldReturnSimpleForAbstractSimpleTemplateSubclass() {
- TemplateValidator.TemplateType type = validator.getTemplateType(TestSimpleTemplate.class);
+ @DisplayName("Should return SIMPLE for simple template class")
+ void shouldReturnSimpleForSimpleTemplateClass() {
+ TemplateValidator.TemplateType type = validator.getTemplateTypeFromClass(TestSimpleTemplate.class);
assertEquals(TemplateValidator.TemplateType.SIMPLE, type);
}
@Test
- @DisplayName("Should return STEPPABLE for AbstractSteppableTemplate subclass")
- void shouldReturnSteppableForAbstractSteppableTemplateSubclass() {
- TemplateValidator.TemplateType type = validator.getTemplateType(TestSteppableTemplate.class);
+ @DisplayName("Should return STEPPABLE for steppable template class")
+ void shouldReturnSteppableForSteppableTemplateClass() {
+ TemplateValidator.TemplateType type = validator.getTemplateTypeFromClass(TestSteppableTemplate.class);
assertEquals(TemplateValidator.TemplateType.STEPPABLE, type);
}
}
diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/AbstractChangeRunnerBuilder.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/AbstractChangeRunnerBuilder.java
index 3bb757e18..b08107e8f 100644
--- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/AbstractChangeRunnerBuilder.java
+++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/AbstractChangeRunnerBuilder.java
@@ -187,15 +187,20 @@ public HOLDER setApplicationArguments(String[] args) {
@Override
public final Runner build() {
- ChangeTemplateManager.loadTemplates();
+ FlamingockMetadata flamingockMetadata = coreConfiguration.getFlamingockMetadata();
+
+ // Initialize templates from metadata (if available)
+ if (flamingockMetadata != null && flamingockMetadata.getTemplates() != null
+ && !flamingockMetadata.getTemplates().isEmpty()) {
+ ChangeTemplateManager.initializeFromMetadata(flamingockMetadata.getTemplates());
+ }
+
pluginManager.initialize(context);
validateAuditStore();
RunnerId runnerId = generateRunnerId();
- FlamingockMetadata flamingockMetadata = coreConfiguration.getFlamingockMetadata();
-
PriorityContext hierarchicalContext = buildContext(flamingockMetadata);
configureStoreAndTargetSystem(hierarchicalContext);
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
index 9d7e344c9..32ad7bf43 100644
--- 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
@@ -58,7 +58,7 @@ class SteppableTemplateExecutableTaskTest {
/**
* Test template that tracks apply and rollback invocations.
*/
- @ChangeTemplate(multiStep = true)
+ @ChangeTemplate(id = "test-steppable", multiStep = true)
public static class TestSteppableTemplate extends AbstractChangeTemplate {
public TestSteppableTemplate() {
@@ -85,7 +85,7 @@ public void rollback() {
/**
* Test template without rollback method.
*/
- @ChangeTemplate(multiStep = true)
+ @ChangeTemplate(id = "test-no-rollback", multiStep = true)
public static class TestTemplateWithoutRollback extends AbstractChangeTemplate {
public TestTemplateWithoutRollback() {
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 db8de7b72..01d4f93cb 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
@@ -36,7 +36,7 @@ class SimpleTemplateLoadedTaskBuilderTest {
private TemplateLoadedTaskBuilder builder;
// Simple test template implementation using the annotation
- @ChangeTemplate
+ @ChangeTemplate(id = "test-simple-template")
public static class TestChangeTemplate extends AbstractChangeTemplate