Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,21 @@
* <p>All template classes must extend {@link AbstractChangeTemplate} and be annotated with
* this annotation to specify whether they process single or multiple steps.
*
* <p><b>Simple templates</b> (default, {@code steppable = false}):
* <p>The {@code id} field is mandatory and must match the {@code template:} field in YAML
* pipeline definitions. This decouples template identity from Java class naming.
*
* <p><b>Simple templates</b> (default, {@code multiStep = false}):
* <pre>
* id: create-users-table
* template: SqlTemplate
* template: sql-template
* apply: "CREATE TABLE users (id INT PRIMARY KEY)"
* rollback: "DROP TABLE users"
* </pre>
*
* <p><b>Steppable templates</b> ({@code steppable = true}) process multiple operations:
* <p><b>Steppable templates</b> ({@code multiStep = true}) process multiple operations:
* <pre>
* id: setup-orders
* template: MongoTemplate
* template: mongo-template
* steps:
* - apply: { type: createCollection, collection: orders }
* rollback: { type: dropCollection, collection: orders }
Expand All @@ -59,6 +62,13 @@
@Target(ElementType.TYPE)
public @interface ChangeTemplate {

/**
* Unique identifier for this template. The YAML {@code template:} field must match this ID.
*
* @return the template identifier
*/
String name();

/**
* 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,26 +128,4 @@
*/
boolean strictStageMapping() default true;

/**
* If true, the annotation processor validates that all template-based changes
* have YAML structure matching their template type (Simple vs Steppable).
* <p>
* <strong>SimpleTemplate</strong> validation:
* <ul>
* <li>MUST have {@code apply} field</li>
* <li>MAY have {@code rollback} field</li>
* <li>MUST NOT have {@code steps} field</li>
* </ul>
* <p>
* <strong>SteppableTemplate</strong> validation:
* <ul>
* <li>MUST have {@code steps} field</li>
* <li>MUST NOT have {@code apply} or {@code rollback} fields at root level</li>
* <li>Each step MUST have {@code apply} field</li>
* </ul>
* <p>
* When validation fails and this flag is {@code true} (default), a RuntimeException
* is thrown at compilation time. When {@code false}, only a warning is emitted.
*/
boolean strictTemplateValidation() default true;
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public static class AnotherAdditionalClass {
}

// Test template with custom generic types
@ChangeTemplate
@ChangeTemplate(name = "test-template-with-custom-types")
public static class TestTemplateWithCustomTypes
extends AbstractChangeTemplate<TestConfig, TestApplyPayload, TestRollbackPayload> {

Expand All @@ -67,7 +67,7 @@ public void apply() {
}

// Test template with additional reflective classes
@ChangeTemplate
@ChangeTemplate(name = "test-template-with-additional-classes")
public static class TestTemplateWithAdditionalClasses
extends AbstractChangeTemplate<TestConfig, TestApplyPayload, TestRollbackPayload> {

Expand All @@ -82,7 +82,7 @@ public void apply() {
}

// Test template with Void configuration
@ChangeTemplate
@ChangeTemplate(name = "test-template-with-void-config")
public static class TestTemplateWithVoidConfig
extends AbstractChangeTemplate<Void, String, String> {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* 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.template.ChangeTemplate;

/**
* Wraps a template class together with its pre-resolved metadata from the {@code @ChangeTemplate} annotation.
* <p>
* Created at registration time in {@link ChangeTemplateManager}, this ensures:
* <ul>
* <li>The {@code @ChangeTemplate} annotation is validated once at registration (fail-fast if missing)</li>
* <li>The {@code multiStep} flag is resolved once and exposed via this wrapper</li>
* <li>Consumers never need to read annotations directly</li>
* </ul>
*/
public class ChangeTemplateDefinition {

private final String id;
private final Class<? extends ChangeTemplate<?, ?, ?>> templateClass;
private final boolean multiStep;

public ChangeTemplateDefinition(
String id,
Class<? extends ChangeTemplate<?, ?, ?>> templateClass,
boolean multiStep) {
this.id = id;
this.templateClass = templateClass;
this.multiStep = multiStep;
}

public String getId() {
return id;
}

public Class<? extends ChangeTemplate<?, ?, ?>> getTemplateClass() {
return templateClass;
}

public boolean isMultiStep() {
return multiStep;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public interface ChangeTemplateFactory {
/**
* Returns a collection of {@link ChangeTemplate} instances provided by this factory.
* <p>
* This method is called by {@link ChangeTemplateManager#getTemplates()} to discover templates
* This method is called by {@link ChangeTemplateManager#getRawTemplates()} to discover templates
* in a federated manner. It is invoked in two contexts:
* <ul>
* <li>During GraalVM build-time processing to register template classes for reflection</li>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package io.flamingock.internal.common.core.template;

import io.flamingock.api.template.ChangeTemplate;
import io.flamingock.internal.common.core.error.FlamingockException;
import org.jetbrains.annotations.TestOnly;
import io.flamingock.internal.util.log.FlamingockLoggerFactory;
import org.slf4j.Logger;
Expand All @@ -33,7 +34,7 @@
* <p>
* This class serves two primary purposes in different contexts:
* <ol>
* <li><strong>GraalVM Build-time Context</strong> - The {@link #getTemplates()} method is called by
* <li><strong>GraalVM Build-time Context</strong> - The {@link #getRawTemplates()} 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.</li>
Expand All @@ -58,7 +59,7 @@ public final class ChangeTemplateManager {

private static final Logger logger = FlamingockLoggerFactory.getLogger("TemplateManager");

private static final Map<String, Class<? extends ChangeTemplate<?, ?, ?>>> templates = new HashMap<>();
private static final Map<String, ChangeTemplateDefinition> templates = new HashMap<>();

/**
* Private constructor to prevent instantiation of this utility class.
Expand All @@ -71,7 +72,7 @@ private ChangeTemplateManager() {
* Loads and registers all available templates from the classpath into the internal registry.
* <p>
* 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
* It discovers all templates via {@link #getRawTemplates()} and registers them in the internal
* registry, indexed by their simple class name.
* <p>
* This method is not thread-safe and should be called from a single thread during application
Expand All @@ -80,14 +81,36 @@ private ChangeTemplateManager() {
@SuppressWarnings("unchecked")
public static void loadTemplates() {
logger.debug("Registering templates");
getTemplates().forEach(template -> {
getRawTemplates().forEach(template -> {
Class<? extends ChangeTemplate<?, ?, ?>> templateClass = (Class<? extends ChangeTemplate<?, ?, ?>>) template.getClass();
templates.put(templateClass.getSimpleName(), templateClass);
logger.debug("registered template: {}", templateClass.getSimpleName());
ChangeTemplateDefinition definition = buildDefinition(templateClass);
templates.put(definition.getId(), definition);
logger.debug("registered template: {}", definition.getId());
});

}

/**
* Retrieves a template definition by name from the internal registry.
* <p>
* This method is used during runtime to look up template definitions by their simple name.
* It returns an {@link Optional} that will be empty if no template with the specified
* name has been registered.
* <p>
* This method is thread-safe after initialization (after {@link #loadTemplates()} has been called).
*
* @param templateName The simple class name of the template to retrieve
* @return An Optional containing the template definition if found, or empty if not found
*/
public static Optional<ChangeTemplateDefinition> getTemplate(String templateName) {
return Optional.ofNullable(templates.get(templateName));
}

public static ChangeTemplateDefinition getTemplateOrFail(String templateName) {
return Optional.ofNullable(templates.get(templateName))
.orElseThrow(()-> new FlamingockException(String.format("Template[%s] not found. This is probably because template's name is wrong or template's library not imported", templateName)));
}

/**
* Discovers and returns all available templates from the classpath.
* <p>
Expand All @@ -110,7 +133,7 @@ public static void loadTemplates() {
*
* @return A collection of all discovered template instances
*/
public static Collection<ChangeTemplate<?, ?, ?>> getTemplates() {
public static Collection<ChangeTemplate<?, ?, ?>> getRawTemplates() {
logger.debug("Retrieving ChangeTemplates");

//Loads the ChangeTemplates directly registered with SPI
Expand All @@ -134,28 +157,39 @@ public static void loadTemplates() {
* <p>
* 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.
* The template is registered under its {@code @ChangeTemplate} annotation's {@code id}.
*
* @param templateName The name to register the template under (typically the simple class name)
* @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(Class<? extends ChangeTemplate<?, ?, ?>> templateClass) {
ChangeTemplateDefinition definition = buildDefinition(templateClass);
templates.put(definition.getId(), definition);
}


/**
* Retrieves a template class by name from the internal registry.
* <p>
* 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.
* <p>
* This method is thread-safe after initialization (after {@link #loadTemplates()} has been called).
* Validates the {@code @ChangeTemplate} annotation on the given class and builds a
* {@link ChangeTemplateDefinition} with pre-resolved metadata.
*
* @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
* @param templateClass the template class to validate and wrap
* @return a new ChangeTemplateDefinition
* @throws FlamingockException if the class is missing the {@code @ChangeTemplate} annotation
*/
public static Optional<Class<? extends ChangeTemplate<?, ?, ?>>> getTemplate(String templateName) {
return Optional.ofNullable(templates.get(templateName));
private static ChangeTemplateDefinition buildDefinition(Class<? extends ChangeTemplate<?, ?, ?>> templateClass) {
io.flamingock.api.annotations.ChangeTemplate annotation =
templateClass.getAnnotation(io.flamingock.api.annotations.ChangeTemplate.class);
if (annotation == null) {
throw new FlamingockException(String.format(
"Template class '%s' is missing required @ChangeTemplate annotation",
templateClass.getSimpleName()));
}
String id = annotation.name();
if (id == null || id.trim().isEmpty()) {
throw new FlamingockException(String.format(
"Template class '%s' has a blank @ChangeTemplate id. The id must be a non-empty string",
templateClass.getSimpleName()));
}
return new ChangeTemplateDefinition(id, templateClass, annotation.multiStep());
}
}
Loading
Loading