Skip to content

Commit f1baf06

Browse files
authored
refactor: template resolution by annotation name and validation at build time (#842)
1 parent a05d92e commit f1baf06

17 files changed

Lines changed: 524 additions & 330 deletions

File tree

core/flamingock-core-api/src/main/java/io/flamingock/api/annotations/ChangeTemplate.java

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,21 @@
2828
* <p>All template classes must extend {@link AbstractChangeTemplate} and be annotated with
2929
* this annotation to specify whether they process single or multiple steps.
3030
*
31-
* <p><b>Simple templates</b> (default, {@code steppable = false}):
31+
* <p>The {@code id} field is mandatory and must match the {@code template:} field in YAML
32+
* pipeline definitions. This decouples template identity from Java class naming.
33+
*
34+
* <p><b>Simple templates</b> (default, {@code multiStep = false}):
3235
* <pre>
3336
* id: create-users-table
34-
* template: SqlTemplate
37+
* template: sql-template
3538
* apply: "CREATE TABLE users (id INT PRIMARY KEY)"
3639
* rollback: "DROP TABLE users"
3740
* </pre>
3841
*
39-
* <p><b>Steppable templates</b> ({@code steppable = true}) process multiple operations:
42+
* <p><b>Steppable templates</b> ({@code multiStep = true}) process multiple operations:
4043
* <pre>
4144
* id: setup-orders
42-
* template: MongoTemplate
45+
* template: mongo-template
4346
* steps:
4447
* - apply: { type: createCollection, collection: orders }
4548
* rollback: { type: dropCollection, collection: orders }
@@ -59,6 +62,13 @@
5962
@Target(ElementType.TYPE)
6063
public @interface ChangeTemplate {
6164

65+
/**
66+
* Unique identifier for this template. The YAML {@code template:} field must match this ID.
67+
*
68+
* @return the template identifier
69+
*/
70+
String name();
71+
6272
/**
6373
* When {@code true}, the template expects a {@code steps} array in YAML.
6474
* When {@code false} (default), it expects {@code apply} and optional {@code rollback} at root.

core/flamingock-core-api/src/main/java/io/flamingock/api/annotations/EnableFlamingock.java

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -128,26 +128,4 @@
128128
*/
129129
boolean strictStageMapping() default true;
130130

131-
/**
132-
* If true, the annotation processor validates that all template-based changes
133-
* have YAML structure matching their template type (Simple vs Steppable).
134-
* <p>
135-
* <strong>SimpleTemplate</strong> validation:
136-
* <ul>
137-
* <li>MUST have {@code apply} field</li>
138-
* <li>MAY have {@code rollback} field</li>
139-
* <li>MUST NOT have {@code steps} field</li>
140-
* </ul>
141-
* <p>
142-
* <strong>SteppableTemplate</strong> validation:
143-
* <ul>
144-
* <li>MUST have {@code steps} field</li>
145-
* <li>MUST NOT have {@code apply} or {@code rollback} fields at root level</li>
146-
* <li>Each step MUST have {@code apply} field</li>
147-
* </ul>
148-
* <p>
149-
* When validation fails and this flag is {@code true} (default), a RuntimeException
150-
* is thrown at compilation time. When {@code false}, only a warning is emitted.
151-
*/
152-
boolean strictTemplateValidation() default true;
153131
}

core/flamingock-core-api/src/test/java/io/flamingock/api/template/AbstractChangeTemplateReflectiveClassesTest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public static class AnotherAdditionalClass {
5252
}
5353

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

@@ -67,7 +67,7 @@ public void apply() {
6767
}
6868

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

@@ -82,7 +82,7 @@ public void apply() {
8282
}
8383

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

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright 2026 Flamingock (https://www.flamingock.io)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.flamingock.internal.common.core.template;
17+
18+
import io.flamingock.api.template.ChangeTemplate;
19+
20+
/**
21+
* Wraps a template class together with its pre-resolved metadata from the {@code @ChangeTemplate} annotation.
22+
* <p>
23+
* Created at registration time in {@link ChangeTemplateManager}, this ensures:
24+
* <ul>
25+
* <li>The {@code @ChangeTemplate} annotation is validated once at registration (fail-fast if missing)</li>
26+
* <li>The {@code multiStep} flag is resolved once and exposed via this wrapper</li>
27+
* <li>Consumers never need to read annotations directly</li>
28+
* </ul>
29+
*/
30+
public class ChangeTemplateDefinition {
31+
32+
private final String id;
33+
private final Class<? extends ChangeTemplate<?, ?, ?>> templateClass;
34+
private final boolean multiStep;
35+
36+
public ChangeTemplateDefinition(
37+
String id,
38+
Class<? extends ChangeTemplate<?, ?, ?>> templateClass,
39+
boolean multiStep) {
40+
this.id = id;
41+
this.templateClass = templateClass;
42+
this.multiStep = multiStep;
43+
}
44+
45+
public String getId() {
46+
return id;
47+
}
48+
49+
public Class<? extends ChangeTemplate<?, ?, ?>> getTemplateClass() {
50+
return templateClass;
51+
}
52+
53+
public boolean isMultiStep() {
54+
return multiStep;
55+
}
56+
}

core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/template/ChangeTemplateFactory.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public interface ChangeTemplateFactory {
4747
/**
4848
* Returns a collection of {@link ChangeTemplate} instances provided by this factory.
4949
* <p>
50-
* This method is called by {@link ChangeTemplateManager#getTemplates()} to discover templates
50+
* This method is called by {@link ChangeTemplateManager#getRawTemplates()} to discover templates
5151
* in a federated manner. It is invoked in two contexts:
5252
* <ul>
5353
* <li>During GraalVM build-time processing to register template classes for reflection</li>

core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/template/ChangeTemplateManager.java

Lines changed: 55 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package io.flamingock.internal.common.core.template;
1717

1818
import io.flamingock.api.template.ChangeTemplate;
19+
import io.flamingock.internal.common.core.error.FlamingockException;
1920
import org.jetbrains.annotations.TestOnly;
2021
import io.flamingock.internal.util.log.FlamingockLoggerFactory;
2122
import org.slf4j.Logger;
@@ -33,7 +34,7 @@
3334
* <p>
3435
* This class serves two primary purposes in different contexts:
3536
* <ol>
36-
* <li><strong>GraalVM Build-time Context</strong> - The {@link #getTemplates()} method is called by
37+
* <li><strong>GraalVM Build-time Context</strong> - The {@link #getRawTemplates()} method is called by
3738
* the GraalVM RegistrationFeature to discover all available templates. For each template,
3839
* the feature registers both the template class itself and all classes returned by
3940
* {@link ChangeTemplate#getReflectiveClasses()} for reflection in native images.</li>
@@ -58,7 +59,7 @@ public final class ChangeTemplateManager {
5859

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

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

6364
/**
6465
* Private constructor to prevent instantiation of this utility class.
@@ -71,7 +72,7 @@ private ChangeTemplateManager() {
7172
* Loads and registers all available templates from the classpath into the internal registry.
7273
* <p>
7374
* This method is intended to be called once during Flamingock runtime initialization.
74-
* It discovers all templates via {@link #getTemplates()} and registers them in the internal
75+
* It discovers all templates via {@link #getRawTemplates()} and registers them in the internal
7576
* registry, indexed by their simple class name.
7677
* <p>
7778
* This method is not thread-safe and should be called from a single thread during application
@@ -80,14 +81,36 @@ private ChangeTemplateManager() {
8081
@SuppressWarnings("unchecked")
8182
public static void loadTemplates() {
8283
logger.debug("Registering templates");
83-
getTemplates().forEach(template -> {
84+
getRawTemplates().forEach(template -> {
8485
Class<? extends ChangeTemplate<?, ?, ?>> templateClass = (Class<? extends ChangeTemplate<?, ?, ?>>) template.getClass();
85-
templates.put(templateClass.getSimpleName(), templateClass);
86-
logger.debug("registered template: {}", templateClass.getSimpleName());
86+
ChangeTemplateDefinition definition = buildDefinition(templateClass);
87+
templates.put(definition.getId(), definition);
88+
logger.debug("registered template: {}", definition.getId());
8789
});
8890

8991
}
9092

93+
/**
94+
* Retrieves a template definition by name from the internal registry.
95+
* <p>
96+
* This method is used during runtime to look up template definitions by their simple name.
97+
* It returns an {@link Optional} that will be empty if no template with the specified
98+
* name has been registered.
99+
* <p>
100+
* This method is thread-safe after initialization (after {@link #loadTemplates()} has been called).
101+
*
102+
* @param templateName The simple class name of the template to retrieve
103+
* @return An Optional containing the template definition if found, or empty if not found
104+
*/
105+
public static Optional<ChangeTemplateDefinition> getTemplate(String templateName) {
106+
return Optional.ofNullable(templates.get(templateName));
107+
}
108+
109+
public static ChangeTemplateDefinition getTemplateOrFail(String templateName) {
110+
return Optional.ofNullable(templates.get(templateName))
111+
.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)));
112+
}
113+
91114
/**
92115
* Discovers and returns all available templates from the classpath.
93116
* <p>
@@ -110,7 +133,7 @@ public static void loadTemplates() {
110133
*
111134
* @return A collection of all discovered template instances
112135
*/
113-
public static Collection<ChangeTemplate<?, ?, ?>> getTemplates() {
136+
public static Collection<ChangeTemplate<?, ?, ?>> getRawTemplates() {
114137
logger.debug("Retrieving ChangeTemplates");
115138

116139
//Loads the ChangeTemplates directly registered with SPI
@@ -134,28 +157,39 @@ public static void loadTemplates() {
134157
* <p>
135158
* This method is intended for use in test environments only to register mock or test templates.
136159
* It directly modifies the internal template registry and is not thread-safe.
160+
* The template is registered under its {@code @ChangeTemplate} annotation's {@code id}.
137161
*
138-
* @param templateName The name to register the template under (typically the simple class name)
139162
* @param templateClass The template class to register
140163
*/
141164
@TestOnly
142-
public static void addTemplate(String templateName, Class<? extends ChangeTemplate<?, ?, ?>> templateClass) {
143-
templates.put(templateName, templateClass);
165+
public static void addTemplate(Class<? extends ChangeTemplate<?, ?, ?>> templateClass) {
166+
ChangeTemplateDefinition definition = buildDefinition(templateClass);
167+
templates.put(definition.getId(), definition);
144168
}
145169

170+
146171
/**
147-
* Retrieves a template class by name from the internal registry.
148-
* <p>
149-
* This method is used during runtime to look up template classes by their simple name.
150-
* It returns an {@link Optional} that will be empty if no template with the specified
151-
* name has been registered.
152-
* <p>
153-
* This method is thread-safe after initialization (after {@link #loadTemplates()} has been called).
172+
* Validates the {@code @ChangeTemplate} annotation on the given class and builds a
173+
* {@link ChangeTemplateDefinition} with pre-resolved metadata.
154174
*
155-
* @param templateName The simple class name of the template to retrieve
156-
* @return An Optional containing the template class if found, or empty if not found
175+
* @param templateClass the template class to validate and wrap
176+
* @return a new ChangeTemplateDefinition
177+
* @throws FlamingockException if the class is missing the {@code @ChangeTemplate} annotation
157178
*/
158-
public static Optional<Class<? extends ChangeTemplate<?, ?, ?>>> getTemplate(String templateName) {
159-
return Optional.ofNullable(templates.get(templateName));
179+
private static ChangeTemplateDefinition buildDefinition(Class<? extends ChangeTemplate<?, ?, ?>> templateClass) {
180+
io.flamingock.api.annotations.ChangeTemplate annotation =
181+
templateClass.getAnnotation(io.flamingock.api.annotations.ChangeTemplate.class);
182+
if (annotation == null) {
183+
throw new FlamingockException(String.format(
184+
"Template class '%s' is missing required @ChangeTemplate annotation",
185+
templateClass.getSimpleName()));
186+
}
187+
String id = annotation.name();
188+
if (id == null || id.trim().isEmpty()) {
189+
throw new FlamingockException(String.format(
190+
"Template class '%s' has a blank @ChangeTemplate id. The id must be a non-empty string",
191+
templateClass.getSimpleName()));
192+
}
193+
return new ChangeTemplateDefinition(id, templateClass, annotation.multiStep());
160194
}
161195
}

0 commit comments

Comments
 (0)