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: - *

- *

- * 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: - *

- *

- * Implementations should: - *

- * - * @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: *

    - *
  1. 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.
  2. - *
  3. Runtime Context - The {@link #loadTemplates()} method is called during - * Flamingock initialization to populate the internal registry with all available templates - * for use during execution.
  4. + *
  5. Annotation processor discovers templates and serializes metadata to FlamingockMetadata
  6. + *
  7. At runtime, {@link #initializeFromMetadata(List)} loads classes from metadata
  8. + *
  9. Templates are looked up by ID via {@link #getTemplate(String)}
  10. *
- *

- * Templates are discovered through Java's {@link ServiceLoader} mechanism from two sources: - *

- *

- * 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> templateClass = (Class>) 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> templateClass = + (Class>) 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: - *

- *

- * Templates are discovered from two sources: - *

    - *
  1. Direct implementations of {@link ChangeTemplate} registered via SPI
  2. - *
  3. Templates provided by {@link ChangeTemplateFactory} implementations registered via SPI
  4. - *
- *

- * 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> 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> templateClass) { - templates.put(templateName, templateClass); + public static void addTemplate(String templateId, Class> 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> templateClass; + final TemplateMetadata metadata; + + TemplateEntry(Class> 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: + *

+ */ +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> 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> templateClass) { + public TemplateType getTemplateTypeFromClass(Class> 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 { public TestChangeTemplate() { 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 86eb18e0d..3caa62962 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 @@ -41,7 +41,7 @@ class SteppableTemplateLoadedTaskBuilderTest { private TemplateLoadedTaskBuilder builder; // Steppable test template implementation using the annotation - @ChangeTemplate(multiStep = true) + @ChangeTemplate(id = "test-steppable-template", multiStep = true) public static class TestSteppableTemplate extends AbstractChangeTemplate { public TestSteppableTemplate() { 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 229b0da89..3ed5162e3 100644 --- a/core/flamingock-graalvm/src/main/java/io/flamingock/graalvm/RegistrationFeature.java +++ b/core/flamingock-graalvm/src/main/java/io/flamingock/graalvm/RegistrationFeature.java @@ -17,6 +17,7 @@ import io.flamingock.api.template.AbstractChangeTemplate; import io.flamingock.api.template.ChangeTemplate; +import io.flamingock.api.template.ReflectionMetadataProvider; import io.flamingock.api.template.TemplateStep; import io.flamingock.internal.common.core.metadata.FlamingockMetadata; import io.flamingock.internal.common.core.preview.*; @@ -146,14 +147,35 @@ public void beforeAnalysis(BeforeAnalysisAccess access) { private void registerTemplates() { logger.startRegistrationProcess("templates"); + + // Register core template infrastructure classes registerClassForReflection(ChangeTemplateManager.class); registerClassForReflection(ChangeTemplate.class); registerClassForReflection(AbstractChangeTemplate.class); registerClassForReflection(TemplateStep.class); - ChangeTemplateManager.getTemplates().forEach(template -> { - registerClassForReflection(template.getClass()); - template.getReflectiveClasses().forEach(RegistrationFeature::registerClassForReflection); - }); + + // Template classes are now included in reflection-classes.txt by the annotation processor + // For each template class, we also check if it implements ReflectionMetadataProvider + // and register those additional classes + List classesToRegister = FileUtil.getClassesForRegistration(); + for (String className : classesToRegister) { + try { + Class clazz = Class.forName(className); + + // If class implements ReflectionMetadataProvider, register its reflective classes + if (ReflectionMetadataProvider.class.isAssignableFrom(clazz)) { + try { + ReflectionMetadataProvider provider = + (ReflectionMetadataProvider) clazz.getDeclaredConstructor().newInstance(); + provider.getReflectiveClasses().forEach(RegistrationFeature::registerClassForReflection); + } catch (Exception e) { + System.out.println("[Flamingock] Warning: Failed to get reflective classes from " + className + ": " + e.getMessage()); + } + } + } catch (ClassNotFoundException e) { + // Class already handled by registerUserClasses, skip here + } + } logger.completedRegistrationProcess("templates"); } diff --git a/core/flamingock-processor/src/main/java/io/flamingock/core/processor/FlamingockAnnotationProcessor.java b/core/flamingock-processor/src/main/java/io/flamingock/core/processor/FlamingockAnnotationProcessor.java index 69a416d00..645e3bc05 100644 --- a/core/flamingock-processor/src/main/java/io/flamingock/core/processor/FlamingockAnnotationProcessor.java +++ b/core/flamingock-processor/src/main/java/io/flamingock/core/processor/FlamingockAnnotationProcessor.java @@ -17,9 +17,11 @@ import io.flamingock.api.StageType; import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.ChangeTemplate; import io.flamingock.api.annotations.EnableFlamingock; import io.flamingock.api.annotations.FlamingockCliBuilder; import io.flamingock.api.annotations.Stage; +import io.flamingock.core.processor.template.TemplateDiscoveryOrchestrator; import io.flamingock.core.processor.util.AnnotationFinder; import io.flamingock.core.processor.util.PathResolver; import io.flamingock.core.processor.util.ProjectRootDetector; @@ -195,6 +197,7 @@ public Set getSupportedAnnotationTypes() { return new HashSet<>(Arrays.asList( EnableFlamingock.class.getName(), Change.class.getName(), + ChangeTemplate.class.getName(), FlamingockCliBuilder.class.getName() )); } @@ -233,12 +236,24 @@ public boolean process(Set annotations, RoundEnvironment ); validateAllChangesAreMappedToStages(standardChangesMapByPackage, pipeline, flamingockAnnotation.strictStageMapping()); - validateTemplateStructures(pipeline, flamingockAnnotation.strictTemplateValidation()); + + // Discover templates and add to metadata + TemplateDiscoveryOrchestrator.DiscoveryResult templateDiscoveryResult = annotationFinder.discoverTemplates(); + if (templateDiscoveryResult.hasErrors()) { + for (String error : templateDiscoveryResult.getErrors()) { + logger.error(error); + } + throw new RuntimeException("Template discovery failed: " + String.join("; ", templateDiscoveryResult.getErrors())); + } + + // Validate template structures (needs templates to be discovered first) + validateTemplateStructures(pipeline, flamingockAnnotation.strictTemplateValidation(), templateDiscoveryResult); Serializer serializer = new Serializer(processingEnv, logger); String configFile = flamingockAnnotation.configFile(); FlamingockMetadata flamingockMetadata = new FlamingockMetadata(pipeline, configFile, properties); builderProvider.ifPresent(flamingockMetadata::setBuilderProvider); + flamingockMetadata.setTemplates(templateDiscoveryResult.getTemplates()); serializer.serializeFullPipeline(flamingockMetadata); // Generate summary - count all changes from the final pipeline (code-based + template-based) @@ -783,9 +798,11 @@ private void validateAllChangesAreMappedToStages(Map allErrors = new ArrayList<>(); diff --git a/core/flamingock-processor/src/main/java/io/flamingock/core/processor/template/AnnotationTemplateDiscoverer.java b/core/flamingock-processor/src/main/java/io/flamingock/core/processor/template/AnnotationTemplateDiscoverer.java new file mode 100644 index 000000000..73ce2ed31 --- /dev/null +++ b/core/flamingock-processor/src/main/java/io/flamingock/core/processor/template/AnnotationTemplateDiscoverer.java @@ -0,0 +1,81 @@ +/* + * 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.core.processor.template; + +import io.flamingock.api.annotations.ChangeTemplate; +import io.flamingock.internal.common.core.template.TemplateMetadata; +import io.flamingock.internal.common.core.util.LoggerPreProcessor; + +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.TypeElement; +import java.util.Collection; +import java.util.stream.Collectors; + +/** + * Discovers templates via {@code @ChangeTemplate} annotation during annotation processing. + * + *

This discoverer finds all classes annotated with {@code @ChangeTemplate} in the + * current compilation round and extracts their metadata. + */ +public class AnnotationTemplateDiscoverer implements ChangeTemplateDiscoverer { + + private final RoundEnvironment roundEnv; + private final LoggerPreProcessor logger; + + /** + * Creates a new AnnotationTemplateDiscoverer. + * + * @param roundEnv the current annotation processing round environment + * @param logger the logger for diagnostic messages + */ + public AnnotationTemplateDiscoverer(RoundEnvironment roundEnv, LoggerPreProcessor logger) { + this.roundEnv = roundEnv; + this.logger = logger; + } + + @Override + public Collection discover() { + logger.verbose("Discovering templates via @ChangeTemplate annotation"); + + Collection templates = roundEnv.getElementsAnnotatedWith(ChangeTemplate.class) + .stream() + .filter(e -> e.getKind() == ElementKind.CLASS) + .map(e -> (TypeElement) e) + .map(this::buildTemplateMetadata) + .collect(Collectors.toList()); + + logger.verbose("Discovered " + templates.size() + " templates via annotation"); + return templates; + } + + /** + * Builds TemplateMetadata from a TypeElement annotated with @ChangeTemplate. + * + * @param element the type element representing the template class + * @return the extracted template metadata + */ + private TemplateMetadata buildTemplateMetadata(TypeElement element) { + ChangeTemplate annotation = element.getAnnotation(ChangeTemplate.class); + String fullyQualifiedClassName = element.getQualifiedName().toString(); + String id = annotation.id(); + boolean multiStep = annotation.multiStep(); + + logger.verbose(" Found template: " + id + " (" + fullyQualifiedClassName + ")"); + + return new TemplateMetadata(id, multiStep, fullyQualifiedClassName); + } +} diff --git a/core/flamingock-processor/src/main/java/io/flamingock/core/processor/template/ChangeTemplateDiscoverer.java b/core/flamingock-processor/src/main/java/io/flamingock/core/processor/template/ChangeTemplateDiscoverer.java new file mode 100644 index 000000000..39cc888c3 --- /dev/null +++ b/core/flamingock-processor/src/main/java/io/flamingock/core/processor/template/ChangeTemplateDiscoverer.java @@ -0,0 +1,43 @@ +/* + * 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.core.processor.template; + +import io.flamingock.internal.common.core.template.TemplateMetadata; + +import java.util.Collection; + +/** + * Interface for template discovery mechanisms. + * + *

Implementations of this interface are responsible for discovering + * {@code @ChangeTemplate} annotated classes from various sources: + *

    + *
  • Annotation processing: Discovers templates in the current compilation round
  • + *
  • File-based: Reads template class names from {@code META-INF/flamingock/templates}
  • + *
+ * + *

This follows the Dependency Inversion Principle, allowing the + * {@link TemplateDiscoveryOrchestrator} to work with any discovery mechanism. + */ +public interface ChangeTemplateDiscoverer { + + /** + * Discovers templates and returns their metadata. + * + * @return collection of discovered template metadata + */ + Collection discover(); +} diff --git a/core/flamingock-processor/src/main/java/io/flamingock/core/processor/template/FileTemplateDiscoverer.java b/core/flamingock-processor/src/main/java/io/flamingock/core/processor/template/FileTemplateDiscoverer.java new file mode 100644 index 000000000..b0f9e146a --- /dev/null +++ b/core/flamingock-processor/src/main/java/io/flamingock/core/processor/template/FileTemplateDiscoverer.java @@ -0,0 +1,266 @@ +/* + * 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.core.processor.template; + +import io.flamingock.api.annotations.ChangeTemplate; +import io.flamingock.internal.common.core.template.TemplateMetadata; +import io.flamingock.internal.common.core.util.LoggerPreProcessor; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.TypeElement; +import javax.tools.FileObject; +import javax.tools.StandardLocation; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Discovers templates via {@code META-INF/flamingock/templates} files on the classpath. + * + *

This discoverer reads template class names from files and loads them to extract + * metadata from their {@code @ChangeTemplate} annotations. This enables external + * template libraries to register their templates. + * + *

File format: One fully qualified class name per line, blank lines and lines + * starting with '#' are ignored. + */ +public class FileTemplateDiscoverer implements ChangeTemplateDiscoverer { + + /** + * The path to the templates registration file. + */ + public static final String TEMPLATES_FILE_PATH = "META-INF/flamingock/templates"; + + private final ClassLoader classLoader; + private final LoggerPreProcessor logger; + private final ProcessingEnvironment processingEnv; + + /** + * Creates a new FileTemplateDiscoverer. + * + * @param classLoader the class loader to use for reading resources and loading classes + * @param logger the logger for diagnostic messages + * @param processingEnv the annotation processing environment for accessing the compilation classpath + */ + public FileTemplateDiscoverer(ClassLoader classLoader, LoggerPreProcessor logger, ProcessingEnvironment processingEnv) { + this.classLoader = classLoader; + this.logger = logger; + this.processingEnv = processingEnv; + } + + @Override + public Collection discover() { + logger.verbose("Discovering templates via " + TEMPLATES_FILE_PATH); + + Set classNames = readTemplateClassNamesFromFiles(); + + List templates = classNames.stream() + .map(this::loadAndBuildMetadata) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + logger.verbose("Discovered " + templates.size() + " templates via file"); + return templates; + } + + /** + * Reads all template class names from META-INF/flamingock/templates files. + * + *

Uses two strategies: + *

    + *
  1. The annotation processing Filer API ({@code StandardLocation.CLASS_PATH}), + * which sees the compilation classpath including dependency JARs and the + * project's own main output.
  2. + *
  3. The thread context classloader as a fallback.
  4. + *
+ * + * @return set of fully qualified class names + */ + private Set readTemplateClassNamesFromFiles() { + Set classNames = new HashSet<>(); + + // Strategy 1: Use Filer API to read from the compilation classpath. + // This is the reliable way to access resources from dependencies and the + // project's own main classes during annotation processing. + classNames.addAll(readClassNamesViaFiler()); + + // Strategy 2: Fall back to classloader for any additional entries. + classNames.addAll(readClassNamesViaClassLoader()); + + return classNames; + } + + /** + * Reads template class names via the annotation processing Filer API. + * + * @return set of class names found via Filer, or empty set on failure + */ + private Set readClassNamesViaFiler() { + Set classNames = new HashSet<>(); + if (processingEnv == null) { + return classNames; + } + try { + FileObject resource = processingEnv.getFiler() + .getResource(StandardLocation.CLASS_PATH, "", TEMPLATES_FILE_PATH); + try (InputStream is = resource.openInputStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(is))) { + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (!line.isEmpty() && !line.startsWith("#")) { + classNames.add(line); + } + } + } + logger.verbose("Found " + classNames.size() + " template class names via Filer API"); + } catch (Exception e) { + logger.verbose("No templates found via Filer API: " + e.getMessage()); + } + return classNames; + } + + /** + * Reads template class names via the classloader. + * + * @return set of class names found via classloader + */ + private Set readClassNamesViaClassLoader() { + Set classNames = new HashSet<>(); + try { + Enumeration resources = classLoader.getResources(TEMPLATES_FILE_PATH); + while (resources.hasMoreElements()) { + URL resourceUrl = resources.nextElement(); + classNames.addAll(readClassNamesFromFile(resourceUrl)); + } + } catch (IOException e) { + logger.warn("Failed to read template files: " + e.getMessage()); + } + return classNames; + } + + /** + * Reads class names from a single template file. + * + * @param resourceUrl the URL of the resource file + * @return list of class names from the file + */ + private List readClassNamesFromFile(URL resourceUrl) { + List classNames = new ArrayList<>(); + + try (InputStream is = resourceUrl.openStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(is))) { + + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + // Skip empty lines and comments + if (!line.isEmpty() && !line.startsWith("#")) { + classNames.add(line); + } + } + } catch (IOException e) { + logger.warn("Failed to read template file " + resourceUrl + ": " + e.getMessage()); + } + + return classNames; + } + + /** + * Loads a class and builds TemplateMetadata from its @ChangeTemplate annotation. + * + *

Uses two strategies: + *

    + *
  1. Reflective class loading via {@code Class.forName()}
  2. + *
  3. Annotation processing mirror API via {@code Elements.getTypeElement()}, + * which can resolve classes on the compilation classpath even when the + * annotation processor's classloader cannot load them
  4. + *
+ * + * @param className the fully qualified class name + * @return the template metadata, or null if the class cannot be loaded or lacks annotation + */ + private TemplateMetadata loadAndBuildMetadata(String className) { + // Strategy 1: Try reflective class loading + TemplateMetadata result = loadViaReflection(className); + if (result != null) { + return result; + } + + // Strategy 2: Try annotation processing mirror API + result = loadViaMirrorApi(className); + if (result != null) { + return result; + } + + logger.warn("Template class not found: " + className); + return null; + } + + private TemplateMetadata loadViaReflection(String className) { + try { + Class clazz = Class.forName(className, false, classLoader); + ChangeTemplate annotation = clazz.getAnnotation(ChangeTemplate.class); + + if (annotation == null) { + logger.warn("Class " + className + " in templates file is missing @ChangeTemplate annotation"); + return null; + } + + logger.verbose(" Found template via reflection: " + annotation.id() + " (" + className + ")"); + return new TemplateMetadata(annotation.id(), annotation.multiStep(), className, true); + } catch (ClassNotFoundException e) { + return null; + } catch (Exception e) { + logger.verbose("Failed to load template class via reflection " + className + ": " + e.getMessage()); + return null; + } + } + + private TemplateMetadata loadViaMirrorApi(String className) { + if (processingEnv == null) { + return null; + } + try { + TypeElement typeElement = processingEnv.getElementUtils().getTypeElement(className); + if (typeElement == null) { + return null; + } + + ChangeTemplate annotation = typeElement.getAnnotation(ChangeTemplate.class); + if (annotation == null) { + logger.warn("Class " + className + " in templates file is missing @ChangeTemplate annotation"); + return null; + } + + logger.verbose(" Found template via mirror API: " + annotation.id() + " (" + className + ")"); + return new TemplateMetadata(annotation.id(), annotation.multiStep(), className, true); + } catch (Exception e) { + logger.verbose("Failed to load template class via mirror API " + className + ": " + e.getMessage()); + return null; + } + } +} diff --git a/core/flamingock-processor/src/main/java/io/flamingock/core/processor/template/TemplateDiscoveryOrchestrator.java b/core/flamingock-processor/src/main/java/io/flamingock/core/processor/template/TemplateDiscoveryOrchestrator.java new file mode 100644 index 000000000..322cf8b27 --- /dev/null +++ b/core/flamingock-processor/src/main/java/io/flamingock/core/processor/template/TemplateDiscoveryOrchestrator.java @@ -0,0 +1,149 @@ +/* + * 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.core.processor.template; + +import io.flamingock.internal.common.core.template.TemplateMetadata; +import io.flamingock.internal.common.core.util.LoggerPreProcessor; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Orchestrates template discovery from multiple sources. + * + *

This orchestrator: + *

    + *
  • Collects templates from all registered {@link ChangeTemplateDiscoverer} implementations
  • + *
  • Validates that template IDs are unique across all sources
  • + *
  • Warns about annotation-discovered templates not registered in any file
  • + *
+ * + *

The orchestrator follows the Open/Closed Principle - new discoverers can be + * added without modifying this class. + */ +public class TemplateDiscoveryOrchestrator { + + private final List discoverers; + private final LoggerPreProcessor logger; + + /** + * Creates a new TemplateDiscoveryOrchestrator. + * + * @param discoverers list of discoverers to use + * @param logger the logger for diagnostic messages + */ + public TemplateDiscoveryOrchestrator( + List discoverers, + LoggerPreProcessor logger) { + this.discoverers = discoverers; + this.logger = logger; + } + + /** + * Discovers all templates from all configured sources. + * + * @return the discovery result containing templates and any validation errors + */ + public DiscoveryResult discoverAllTemplates() { + logger.info("Discovering change templates"); + + // 1. Discover from all sources + Map templatesByFqcn = new HashMap<>(); + Map templatesById = new HashMap<>(); + List errors = new ArrayList<>(); + + for (ChangeTemplateDiscoverer discoverer : discoverers) { + Collection discovered = discoverer.discover(); + + for (TemplateMetadata template : discovered) { + // Check for duplicate class names (same template discovered twice) + String fqcn = template.getFullyQualifiedClassName(); + if (templatesByFqcn.containsKey(fqcn)) { + // Same class discovered by multiple discoverers - merge fileRegistered flag + if (template.isFileRegistered()) { + templatesByFqcn.get(fqcn).setFileRegistered(true); + } + continue; + } + + // Check for duplicate IDs (different classes with same ID) + String id = template.getId(); + if (templatesById.containsKey(id)) { + TemplateMetadata existing = templatesById.get(id); + errors.add("Duplicate template ID '" + id + "' found in classes: " + + existing.getFullyQualifiedClassName() + " and " + fqcn); + continue; + } + + templatesByFqcn.put(fqcn, template); + templatesById.put(id, template); + } + } + + // 2. Warn about annotation-discovered templates not registered in any file + warnAboutUnregisteredTemplates(templatesByFqcn.values()); + + // 4. Return result + List templates = new ArrayList<>(templatesById.values()); + logger.info("Discovered " + templates.size() + " templates total"); + + return new DiscoveryResult(templates, errors); + } + + /** + * Warns about templates discovered via annotation but not registered in any file. + * + * @param templates all discovered templates + */ + private void warnAboutUnregisteredTemplates(Collection templates) { + for (TemplateMetadata template : templates) { + if (!template.isFileRegistered()) { + logger.warn("Template '" + template.getId() + "' (class: " + + template.getFullyQualifiedClassName() + ") was discovered via annotation " + + "but is not listed in any META-INF/flamingock/templates file. " + + "If you externalize this template to a separate library, you must add it to that file."); + } + } + } + + /** + * Result of template discovery containing templates and any validation errors. + */ + public static class DiscoveryResult { + private final List templates; + private final List errors; + + public DiscoveryResult(List templates, List errors) { + this.templates = templates; + this.errors = errors; + } + + public List getTemplates() { + return templates; + } + + public List getErrors() { + return errors; + } + + public boolean hasErrors() { + return !errors.isEmpty(); + } + } +} diff --git a/core/flamingock-processor/src/main/java/io/flamingock/core/processor/util/AnnotationFinder.java b/core/flamingock-processor/src/main/java/io/flamingock/core/processor/util/AnnotationFinder.java index 7deee1439..d61123c7d 100644 --- a/core/flamingock-processor/src/main/java/io/flamingock/core/processor/util/AnnotationFinder.java +++ b/core/flamingock-processor/src/main/java/io/flamingock/core/processor/util/AnnotationFinder.java @@ -17,6 +17,10 @@ import io.flamingock.api.annotations.EnableFlamingock; import io.flamingock.api.annotations.FlamingockCliBuilder; +import io.flamingock.core.processor.template.AnnotationTemplateDiscoverer; +import io.flamingock.core.processor.template.ChangeTemplateDiscoverer; +import io.flamingock.core.processor.template.FileTemplateDiscoverer; +import io.flamingock.core.processor.template.TemplateDiscoveryOrchestrator; import io.flamingock.internal.common.core.discover.ChangeDiscoverer; import io.flamingock.internal.common.core.metadata.BuilderProviderInfo; import io.flamingock.internal.common.core.preview.CodePreviewChange; @@ -216,4 +220,42 @@ private void validateReturnType(ExecutableElement method) { // If we can't find the builder type (shouldn't happen), we skip validation } + /** + * Discovers all templates via annotation processing and file-based discovery. + * + * @return the discovery result containing templates and any validation errors + */ + public TemplateDiscoveryOrchestrator.DiscoveryResult discoverTemplates() { + logger.info("Searching for @ChangeTemplate annotations and template files"); + + // Get class loader - try multiple sources + ClassLoader classLoader = getClassLoader(); + + // Create discoverers + AnnotationTemplateDiscoverer annotationDiscoverer = new AnnotationTemplateDiscoverer(roundEnv, logger); + FileTemplateDiscoverer fileDiscoverer = new FileTemplateDiscoverer(classLoader, logger, processingEnv); + + // Create orchestrator with all discoverers + List discoverers = Arrays.asList(annotationDiscoverer, fileDiscoverer); + TemplateDiscoveryOrchestrator orchestrator = new TemplateDiscoveryOrchestrator(discoverers, logger); + + return orchestrator.discoverAllTemplates(); + } + + /** + * Gets a suitable ClassLoader for loading template classes. + * + * @return the class loader to use + */ + private ClassLoader getClassLoader() { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + if (cl == null) { + cl = getClass().getClassLoader(); + } + if (cl == null) { + cl = ClassLoader.getSystemClassLoader(); + } + return cl; + } + } \ No newline at end of file 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 60c477696..75b993e9c 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 @@ -127,7 +127,7 @@ private AbstractLoadedTask getTemplateLoadedChange(String profiles) { } - @ChangeTemplate + @ChangeTemplate(id = "test-template-simulate") public static abstract class TemplateSimulate extends AbstractChangeTemplate { public TemplateSimulate() { super(); diff --git a/utils/test-util/src/main/java/io/flamingock/core/kit/audit/AuditEntryTestFactory.java b/utils/test-util/src/main/java/io/flamingock/core/kit/audit/AuditEntryTestFactory.java index b79ac87a2..508796c30 100644 --- a/utils/test-util/src/main/java/io/flamingock/core/kit/audit/AuditEntryTestFactory.java +++ b/utils/test-util/src/main/java/io/flamingock/core/kit/audit/AuditEntryTestFactory.java @@ -80,7 +80,7 @@ public static AuditEntry createTestAuditEntry(String changeId, AuditEntry.Status "test-stage", // stageId changeId, // taskId "test-author", // author - LocalDateTime.now(), // timestamp + LocalDateTime.now().minusSeconds(1), // timestamp (earlier to simulate previous execution) status, // state AuditEntry.ChangeType.STANDARD_CODE, // type "TestChangeClass", // className @@ -110,7 +110,7 @@ public static AuditEntry createTestAuditEntry(String changeId, AuditEntry.Status "test-stage", // stageId changeId, // taskId "test-author", // author - LocalDateTime.now(), // timestamp + LocalDateTime.now().minusSeconds(1), // timestamp (earlier to simulate previous execution) status, // state AuditEntry.ChangeType.STANDARD_CODE, // type "TestChangeClass", // className @@ -219,7 +219,7 @@ public static AuditEntry createTestAuditEntry(String changeId, AuditEntry.Status "test-stage", // stageId changeId, // taskId "test-author", // author - LocalDateTime.now(), // timestamp + LocalDateTime.now().minusSeconds(1), // timestamp (earlier to simulate previous execution) status, // state AuditEntry.ChangeType.STANDARD_CODE, // type "TestChangeClass", // className @@ -248,7 +248,7 @@ public static AuditEntry createTestAuditEntry(String changeId, AuditEntry.Status "test-stage", // stageId changeId, // taskId "test-author", // author - LocalDateTime.now(), // timestamp + LocalDateTime.now().minusSeconds(1), // timestamp (earlier to simulate previous execution) status, // state AuditEntry.ChangeType.STANDARD_CODE, // type "TestChangeClass", // className @@ -282,7 +282,7 @@ public static AuditEntry createTestAuditEntryWithRecoveryStrategy(String changeI "test-stage", // stageId changeId, // taskId "test-author", // author - LocalDateTime.now(), // timestamp + LocalDateTime.now().minusSeconds(1), // timestamp (earlier to simulate previous execution) status, // state AuditEntry.ChangeType.STANDARD_CODE, // type "TestChangeClass", // className