diff --git a/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilder.java b/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilder.java index 488bcb10..0a9d5b16 100644 --- a/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilder.java +++ b/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilder.java @@ -63,7 +63,15 @@ *
  • Integration: generateWithInterface (default: true) * * - *

    Use {@link Template} to create reusable configuration presets. + *

    This annotation is itself a built-in {@link Template}: it is meta-annotated with + * {@code @SimpleBuilder.Template(options = @Options())}. When placed on a class or record, the + * processor treats it like any other template annotation. The optional {@link #options()} attribute + * on a concrete {@code @SimpleBuilder} usage overrides the template defaults. + * + *

    Use {@link Template} to create reusable configuration presets for project- or layer-specific + * conventions. A custom template annotation is an annotation type that is itself meta-annotated + * with {@code @SimpleBuilder.Template(options = @SimpleBuilder.Options(...))} and then applied to + * classes and records. * *

    This annotation is {@link Inherited}: a subclass of an annotated type is treated as if it also * carried {@code @SimpleBuilder} for the purpose of triggering builder generation, unless it is @@ -89,6 +97,7 @@ @Target(ElementType.TYPE) @Retention(RetentionPolicy.CLASS) @Inherited +@SimpleBuilder.Template public @interface SimpleBuilder { /** @@ -782,10 +791,11 @@ @Inherited @interface Template { /** - * The options to apply when this template is used. + * The options to apply when this template is used. Defaults to an empty {@link Options} + * instance, so templates inherit the built-in defaults unless options are explicitly set. * * @return the builder configuration options */ - Options options(); + Options options() default @Options(); } } diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 0579421b..01a0ca35 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -80,7 +80,7 @@ public class PersonDto { ## Template Annotations -`@SimpleBuilder.Template` is a **meta-annotation**: it is placed on a custom annotation declaration (`@interface`), not directly on a class or record. Use it to create reusable configuration presets that can be applied to many classes with a single custom annotation. For one-off builder generation, use `@SimpleBuilder` directly on the class. +`@SimpleBuilder.Template` is a **meta-annotation**: it is placed on a custom annotation declaration (`@interface`), not directly on a class or record. Use it to create reusable configuration presets that can be applied to many classes with a single custom annotation. `@SimpleBuilder` itself is the built-in template: it is meta-annotated with `@SimpleBuilder.Template` and can be used directly on a class for one-off builder generation, or you can define your own custom template annotations. | Need | Use | |------|-----| @@ -1154,9 +1154,9 @@ Or in compiler options: ### Template Annotations Not Working 1. **Check @SimpleBuilder.Template**: Ensure template annotation has `@SimpleBuilder.Template` -2. **Verify options parameter**: Template must specify `options = @SimpleBuilder.Options(...)` +2. **Verify options parameter**: Template can specify `options = @SimpleBuilder.Options(...)` or rely on the built-in defaults (omitting it is equivalent to an empty `@Options()`) 3. **Retention and Target**: Add `@Retention(RetentionPolicy.CLASS)` and `@Target(ElementType.TYPE)` -4. **Don't combine**: Don't use `@SimpleBuilder` when using a template annotation +4. **Combining with @SimpleBuilder**: `@SimpleBuilder` is itself a built-in template. If both `@SimpleBuilder` and a custom template are present on the same class, `@SimpleBuilder` takes precedence in that scope. To use a custom template, place only the custom annotation on the class. 5. **Subclasses not getting a builder**: Add `@Inherited` to the custom template annotation so it propagates to unannotated subclasses (see [Template Annotations](#template-annotations) above). Without `@Inherited`, only the exact type carrying the annotation gets a builder. 6. **Subclass builder has wrong options**: Ensure the custom template annotation is `@Inherited` and that the parent annotation declares the desired `@SimpleBuilder.Options`. Inherited options are applied to subclass builders; a subclass's own annotation overrides the inherited options. diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java index 5efaf2bd..d89d43e1 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java @@ -44,7 +44,6 @@ import javax.lang.model.element.Element; import javax.lang.model.element.TypeElement; import org.javahelpers.simple.builders.core.annotations.Ignore4BuilderGeneration; -import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; import org.javahelpers.simple.builders.core.annotations.SimpleBuilder.Template; import org.javahelpers.simple.builders.processor.analysis.JavaLangAnalyser; import org.javahelpers.simple.builders.processor.classgen.roaster.RoasterCodeGenerator; @@ -135,20 +134,14 @@ public boolean process(Set annotations, RoundEnvironment BuilderConfigurationReader reader = context.getConfigurationReader(); - // Find all elements to process: - // 1. Elements annotated with @SimpleBuilder - // 2. Elements annotated with custom annotations that have @SimpleBuilder.Template - // Configuration is resolved per-element to handle priority correctly when both exist + // Find all elements to process: any element annotated with an annotation that is + // meta-annotated with @SimpleBuilder.Template. This includes @SimpleBuilder itself, which is + // a built-in template. Configuration is resolved per-element to handle priority correctly. Set elementsToProcess = new HashSet<>(); - // Find all @SimpleBuilder annotations - TypeElement simpleBuilderAnnotation = - context.getTypeElement(SimpleBuilder.class.getCanonicalName()); - if (simpleBuilderAnnotation != null) { - elementsToProcess.addAll(roundEnv.getElementsAnnotatedWith(simpleBuilderAnnotation)); - } - - // Find all Annotations with @SimpleBuilder.Template + // Find all annotations meta-annotated with @SimpleBuilder.Template (this includes + // @SimpleBuilder itself, which is now a built-in template). Each such annotation triggers + // builder generation for the elements it is applied to. List annotationsWithTemplate = extractingAnnotationsWithTemplate(annotations); for (TypeElement annotation : annotationsWithTemplate) { elementsToProcess.addAll(roundEnv.getElementsAnnotatedWith(annotation)); @@ -279,15 +272,8 @@ private static List extractingAnnotationsWithTemplate( } private static boolean shouldSkipAnnotation(TypeElement annotation) { - // Only process real annotation specifications - if (annotation.getKind() != javax.lang.model.element.ElementKind.ANNOTATION_TYPE) { - return true; - } - // Skip @SimpleBuilder annotation because we only want to find annotations with - // @SimpleBuilder.Template - return annotation - .getQualifiedName() - .toString() - .equals(org.javahelpers.simple.builders.core.annotations.SimpleBuilder.class.getName()); + // Only process real annotation specifications. @SimpleBuilder is now a built-in template, so + // it is processed through the same path as custom template annotations. + return annotation.getKind() != javax.lang.model.element.ElementKind.ANNOTATION_TYPE; } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderConfigurationReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderConfigurationReader.java index 1711d768..6abb5259 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderConfigurationReader.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderConfigurationReader.java @@ -33,8 +33,8 @@ import javax.lang.model.element.AnnotationValue; import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; import javax.lang.model.util.Elements; -import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; import org.javahelpers.simple.builders.core.enums.AccessModifier; import org.javahelpers.simple.builders.core.enums.OptionState; import org.javahelpers.simple.builders.processor.exceptions.BuilderException; @@ -43,23 +43,26 @@ /** * Reads builder configuration from annotated elements. * - *

    This class analyzes {@link SimpleBuilder.Options} and custom template annotations (annotations - * meta-annotated with {@link SimpleBuilder.Template}) on an element. + *

    Both {@code @SimpleBuilder} and custom annotations are treated as template annotations: an + * annotation triggers builder generation when it is itself meta-annotated with + * {@code @SimpleBuilder.Template}. {@code @SimpleBuilder} is the built-in template; its optional + * {@code options()} attribute overrides the defaults declared on its own + * {@code @SimpleBuilder.Template} meta-annotation. Custom template annotations carry their + * configuration on the {@code @SimpleBuilder.Template} meta-annotation. * *

    Priority order (highest to lowest): * *

      - *
    1. Directly declared {@code @SimpleBuilder(options = ...)} inline options - *
    2. Custom template annotations directly declared on the element - *
    3. Inherited {@code @SimpleBuilder(options = ...)} inline options - *
    4. Inherited custom template annotations + *
    5. Directly declared template annotations on the element (with {@code @SimpleBuilder} taking + * precedence over other direct templates in the same scope) + *
    6. Inherited template annotations *
    7. Global compiler arguments *
    8. Built-in defaults *
    * *

    Custom template annotations are annotations that are themselves meta-annotated with - * {@code @SimpleBuilder.Template}; they are not placed directly on the class. Within each - * inheritance scope, {@code @SimpleBuilder} options take precedence over template options. + * {@code @SimpleBuilder.Template}; they are placed directly on the class or record. Direct + * annotations always override inherited annotations. */ public class BuilderConfigurationReader { private static final String SIMPLE_BUILDER_ANNOTATION = @@ -97,34 +100,124 @@ public BuilderConfiguration getGlobalConfiguration() { } /** - * Reads builder configuration from {@code @SimpleBuilder(options = ...)} inline options. + * Resolves the complete builder configuration for an element by chaining all configuration + * sources in priority order. * - *

    Directly declared options take precedence over inherited options from superclasses. + *

    Priority chain (highest to lowest): * - *

    Returns null if the element has no {@code @SimpleBuilder} annotation. + *

      + *
    1. Direct template annotations on the element + *
    2. Inherited template annotations + *
    3. Global compiler arguments + *
    4. Built-in defaults + *
    * - * @param element the annotated element to analyze - * @return configuration from the inline options, or null if not present + *

    When both {@code @SimpleBuilder} and another direct template annotation are present on the + * same element, {@code @SimpleBuilder} takes precedence within that scope. A subclass's own + * annotation always overrides inherited annotations. + * + * @param element the annotated element to resolve configuration for + * @return the fully resolved configuration with all sources merged */ - public BuilderConfiguration readFromInlineOptions(Element element) { - BuilderConfiguration direct = readFromInlineOptions(element, AnnotationScope.DIRECT); - if (direct != null) { - return direct; + public BuilderConfiguration resolveConfiguration(Element element) throws BuilderException { + String elementName = element.getSimpleName().toString(); + logger.debugStartOperation("Resolving configuration for element: %s", elementName); + + BuilderConfiguration inheritedConfig = readFromScope(element, AnnotationScope.INHERITED); + BuilderConfiguration directConfig = readFromScope(element, AnnotationScope.DIRECT); + + BuilderConfiguration result = + BuilderConfiguration.DEFAULT + .merge(globalConfiguration) + .merge(inheritedConfig) + .merge(directConfig); + + // Validate access modifiers and warn about problematic configurations + validateAccessModifiers(element, result); + + logger.debugEndOperation("Resulting configuration resolved: %s", result.toString()); + return result; + } + + /** + * Reads the highest-priority template configuration for the element in the requested scope. + * + *

    If {@code @SimpleBuilder} is present in the scope, its effective configuration (built-in + * template defaults overridden by any inline {@code options()}) is returned. Otherwise the first + * custom template annotation found in the scope is used. + */ + private BuilderConfiguration readFromScope(Element element, AnnotationScope scope) { + List mirrors = getAnnotationMirrors(element, scope); + BuilderConfiguration simpleBuilderConfig = null; + BuilderConfiguration customTemplateConfig = null; + + for (AnnotationMirror mirror : mirrors) { + if (isSimpleBuilderAnnotation(mirror)) { + simpleBuilderConfig = extractSimpleBuilderConfiguration(mirror); + } else if (customTemplateConfig == null) { + BuilderConfiguration templateConfig = extractCustomTemplateConfiguration(mirror); + if (templateConfig != null) { + logger.debug( + "Annotation based Configuration for scope %s: %s", scope, templateConfig.toString()); + customTemplateConfig = templateConfig; + } + } } - return readFromInlineOptions(element, AnnotationScope.INHERITED); + + if (simpleBuilderConfig != null) { + logger.debug("Built-in template @SimpleBuilder found in %s scope", scope); + return simpleBuilderConfig; + } + return customTemplateConfig; + } + + /** + * Extracts the effective configuration for a concrete {@code @SimpleBuilder} usage. The built-in + * template defaults defined on the {@code @SimpleBuilder} annotation type are merged with the + * inline {@code options()} from the usage, so inline options override the template defaults. + */ + private BuilderConfiguration extractSimpleBuilderConfiguration( + AnnotationMirror simpleBuilderMirror) { + TypeElement simpleBuilderType = + (TypeElement) simpleBuilderMirror.getAnnotationType().asElement(); + BuilderConfiguration templateDefaults = extractTemplateConfigurationFromType(simpleBuilderType); + BuilderConfiguration inlineOptions = extractOptionsFromAnnotationMirror(simpleBuilderMirror); + return mergeNullable(templateDefaults, inlineOptions); + } + + /** + * Extracts the configuration for a custom template annotation usage. The configuration is read + * from the {@code @SimpleBuilder.Template} meta-annotation on the custom annotation type. + */ + private BuilderConfiguration extractCustomTemplateConfiguration( + AnnotationMirror annotationMirror) { + TypeElement annotationType = (TypeElement) annotationMirror.getAnnotationType().asElement(); + return extractTemplateConfigurationFromType(annotationType); } - private BuilderConfiguration readFromInlineOptions(Element element, AnnotationScope scope) { - AnnotationMirror simpleBuilderMirror = - extractAnnotationMirror(element, SIMPLE_BUILDER_ANNOTATION, scope); - return extractOptionsFromAnnotationMirror(simpleBuilderMirror); + /** + * Extracts the template configuration declared on an annotation type by reading the {@code + * options} attribute of its {@code @SimpleBuilder.Template} meta-annotation. + * + * @return the configuration, or {@code null} if the type is not a template annotation + */ + private BuilderConfiguration extractTemplateConfigurationFromType(TypeElement annotationType) { + AnnotationMirror templateMetaMirror = findTemplateMetaMirror(annotationType); + if (templateMetaMirror == null) { + return null; + } + return extractOptionsFromAnnotationMirror(templateMetaMirror); } - private AnnotationMirror extractAnnotationMirror( - Element element, String annotationName, AnnotationScope scope) { - for (AnnotationMirror mirror : getAnnotationMirrors(element, scope)) { - if (mirror.getAnnotationType().toString().equals(annotationName)) { - return mirror; + /** + * Finds the {@code @SimpleBuilder.Template} meta-annotation on an annotation type. + * + * @return the template meta-annotation mirror, or {@code null} if not present + */ + private AnnotationMirror findTemplateMetaMirror(TypeElement annotationType) { + for (AnnotationMirror metaMirror : annotationType.getAnnotationMirrors()) { + if (isTemplateAnnotation(metaMirror)) { + return metaMirror; } } return null; @@ -132,10 +225,11 @@ private AnnotationMirror extractAnnotationMirror( /** * Extracts configuration from the 'options' attribute of an annotation mirror. Used for - * inline @SimpleBuilder(options = ...) where reflection doesn't work. + * {@code @SimpleBuilder(options = ...)} and for {@code @SimpleBuilder.Template(options = ...)}. * * @param annotationMirror the annotation mirror (either @SimpleBuilder or @Template) - * @return the configuration extracted from the options attribute + * @return the configuration extracted from the options attribute, or {@code null} if no options + * are set */ private BuilderConfiguration extractOptionsFromAnnotationMirror( AnnotationMirror annotationMirror) { @@ -143,10 +237,11 @@ private BuilderConfiguration extractOptionsFromAnnotationMirror( return null; } - // Find the 'options' attribute + // Find the 'options' attribute, including default values. This is important for @SimpleBuilder, + // whose options() attribute defaults to @Options() even when it is not explicitly specified. AnnotationMirror optionsMirror = null; Map elementValues = - annotationMirror.getElementValues(); + elementUtils.getElementValuesWithDefaults(annotationMirror); for (Map.Entry entry : elementValues.entrySet()) { @@ -168,6 +263,18 @@ private BuilderConfiguration extractOptionsFromAnnotationMirror( return parseOptionsFromMirror(optionsMirror); } + /** Merges two nullable configurations, preferring the override when both are present. */ + private BuilderConfiguration mergeNullable( + BuilderConfiguration base, BuilderConfiguration override) { + if (base == null) { + return override; + } + if (override == null) { + return base; + } + return base.merge(override); + } + /** * Parses SimpleBuilder.Options from AnnotationMirror. Only contains explicitly set values (not * defaults). @@ -243,48 +350,6 @@ private String extractEnumName(Object value) { : enumString; } - /** - * Reads builder configuration from a custom template annotation on the element. - * - *

    Directly declared template annotations take precedence over inherited template annotations. - * If a {@code @SimpleBuilder} annotation is present in the same scope, template annotations in - * that scope are ignored. - * - *

    Returns null if no template annotation is found or if {@code @SimpleBuilder} is present. - * - * @param element the annotated element to analyze - * @return configuration from the template annotation, or null if not present - */ - public BuilderConfiguration readFromTemplate(Element element) { - BuilderConfiguration direct = readFromTemplate(element, AnnotationScope.DIRECT); - if (direct != null) { - return direct; - } - return readFromTemplate(element, AnnotationScope.INHERITED); - } - - private BuilderConfiguration readFromTemplate(Element element, AnnotationScope scope) { - List mirrors = getAnnotationMirrors(element, scope); - - // If @SimpleBuilder is present in this scope, ignore template annotations in the same scope - if (containsSimpleBuilder(mirrors)) { - logger.debug( - "Template annotations ignored because @SimpleBuilder is present in %s scope", scope); - return null; - } - - // Check all annotations in this scope to find one annotated with @SimpleBuilder.Template - for (AnnotationMirror mirror : mirrors) { - BuilderConfiguration templateConfig = checkForTemplateAnnotation(mirror, element); - if (templateConfig != null) { - logger.debug("Annotation based Configuration: %s", templateConfig.toString()); - return templateConfig; - } - } - - return null; - } - private enum AnnotationScope { DIRECT, INHERITED @@ -310,17 +375,8 @@ private List getAnnotationMirrors( return inheritedMirrors; } - private boolean containsSimpleBuilder(List mirrors) { - for (AnnotationMirror mirror : mirrors) { - if (isSimpleBuilderAnnotation(mirror)) { - return true; - } - } - return false; - } - /** - * Checks if an annotation mirror represents @SimpleBuilder. + * Checks whether an annotation mirror represents @SimpleBuilder. * * @param mirror the annotation mirror to check * @return true if this is @SimpleBuilder @@ -330,29 +386,6 @@ private boolean isSimpleBuilderAnnotation(AnnotationMirror mirror) { return typeName.equals(SIMPLE_BUILDER_ANNOTATION); } - /** - * Checks if an annotation is a template annotation and extracts its configuration. - * - * @param mirror the annotation mirror to check - * @param element the element being processed (for logging) - * @return the configuration if this is a template annotation, null otherwise - */ - private BuilderConfiguration checkForTemplateAnnotation( - AnnotationMirror mirror, Element element) { - Element annotationElement = mirror.getAnnotationType().asElement(); - - // Check using AnnotationMirror for template annotations - for (AnnotationMirror metaMirror : annotationElement.getAnnotationMirrors()) { - if (isTemplateAnnotation(metaMirror)) { - logger.debug( - "Found template annotation '%s' on '%s'", - annotationElement.getSimpleName(), element.getSimpleName()); - return extractOptionsFromTemplateMirror(metaMirror); - } - } - return null; - } - /** * Checks if an annotation mirror represents @SimpleBuilder.Template. * @@ -366,77 +399,6 @@ private boolean isTemplateAnnotation(AnnotationMirror metaMirror) { || metaAnnotationName.equals(SIMPLE_BUILDER_TEMPLATE_ANNOTATION_ALT); } - /** - * Extracts configuration from @SimpleBuilder.Template(options = ...) using AnnotationMirror. - * Fallback for same-round compiled templates where reflection doesn't work. - */ - private BuilderConfiguration extractOptionsFromTemplateMirror(AnnotationMirror templateMirror) { - Map templateValues = - elementUtils.getElementValuesWithDefaults(templateMirror); - - for (Map.Entry entry : - templateValues.entrySet()) { - if (entry.getKey().getSimpleName().toString().equals("options")) { - Object value = entry.getValue().getValue(); - if (value instanceof AnnotationMirror optionsMirror) { - return parseOptionsFromMirror(optionsMirror); - } - } - } - return null; - } - - /** - * Resolves the complete builder configuration for an element by chaining all configuration - * sources in priority order. - * - *

    Priority chain (highest to lowest): - * - *

      - *
    1. Directly declared {@code @SimpleBuilder(options = ...)} inline options - *
    2. Custom template annotations directly declared on the element - *
    3. Inherited {@code @SimpleBuilder(options = ...)} inline options - *
    4. Inherited custom template annotations - *
    5. Global compiler arguments - *
    6. Built-in defaults - *
    - * - *

    Custom template annotations are annotations that are themselves meta-annotated with - * {@code @SimpleBuilder.Template} and are placed directly on the class; the - * {@code @SimpleBuilder.Template} meta-annotation is not placed on the class itself. Within each - * inheritance scope, {@code @SimpleBuilder} options take precedence over template options. Direct - * annotations always override inherited annotations. - * - * @param element the annotated element to resolve configuration for - * @return the fully resolved configuration with all sources merged - */ - public BuilderConfiguration resolveConfiguration(Element element) throws BuilderException { - String elementName = element.getSimpleName().toString(); - logger.debugStartOperation("Resolving configuration for element: %s", elementName); - - BuilderConfiguration inheritedTemplateConfig = - readFromTemplate(element, AnnotationScope.INHERITED); - BuilderConfiguration inheritedInlineConfig = - readFromInlineOptions(element, AnnotationScope.INHERITED); - BuilderConfiguration directTemplateConfig = readFromTemplate(element, AnnotationScope.DIRECT); - BuilderConfiguration directInlineConfig = - readFromInlineOptions(element, AnnotationScope.DIRECT); - - BuilderConfiguration result = - BuilderConfiguration.DEFAULT - .merge(globalConfiguration) - .merge(inheritedTemplateConfig) - .merge(inheritedInlineConfig) - .merge(directTemplateConfig) - .merge(directInlineConfig); - - // Validate access modifiers and warn about problematic configurations - validateAccessModifiers(element, result); - - logger.debugEndOperation("Resulting configuration resolved: %s", result.toString()); - return result; - } - /** * Validates access modifier configurations and throws exception for invalid settings. * diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderAnnotationInheritanceTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderAnnotationInheritanceTest.java index 33e4ef44..5210b161 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderAnnotationInheritanceTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderAnnotationInheritanceTest.java @@ -592,4 +592,82 @@ public class ChildDto extends ParentDto { assertNotContaining(childBuilder, "Supplier<"); assertNotContaining(childBuilder, "Consumer<"); } + + /** + * A direct {@code @SimpleBuilder} on a subclass must override an inherited custom template + * annotation. This proves that {@code @SimpleBuilder} is handled through the same template + * discovery path as custom templates and wins within the same scope by precedence. + */ + @Test + void directSimpleBuilderOverridesInheritedCustomTemplateOptions() { + JavaFileObject parentTemplate = + ProcessorTestUtils.forSource( + """ + package test; + + import java.lang.annotation.ElementType; + import java.lang.annotation.Inherited; + import java.lang.annotation.Retention; + import java.lang.annotation.RetentionPolicy; + import java.lang.annotation.Target; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + import org.javahelpers.simple.builders.core.enums.OptionState; + + @SimpleBuilder.Template(options = @SimpleBuilder.Options( + generateFieldSupplier = OptionState.DISABLED, + builderSuffix = "ParentBuilder" + )) + @Inherited + @Retention(RetentionPolicy.CLASS) + @Target(ElementType.TYPE) + public @interface ParentTemplate {} + """); + + JavaFileObject parentSource = + ProcessorTestUtils.forSource( + """ + package test; + + @ParentTemplate + public class ParentDto { + private String name; + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + } + """); + + JavaFileObject childSource = + ProcessorTestUtils.forSource( + """ + package test; + + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + + @SimpleBuilder(options = @SimpleBuilder.Options( + builderSuffix = "ChildBuilder" + )) + public class ChildDto extends ParentDto { + private int age; + + public int getAge() { return age; } + public void setAge(int age) { this.age = age; } + } + """); + + Compilation compilation = compile(parentTemplate, parentSource, childSource); + + assertThat(compilation).succeededWithoutWarnings(); + + String parentBuilder = loadGeneratedSource(compilation, "ParentDtoParentBuilder"); + assertContaining(parentBuilder, "class ParentDtoParentBuilder"); + assertContaining(parentBuilder, "public ParentDtoParentBuilder name(String name)"); + assertNotContaining(parentBuilder, "Supplier<"); + + String childBuilder = loadGeneratedSource(compilation, "ChildDtoChildBuilder"); + assertContaining(childBuilder, "class ChildDtoChildBuilder"); + assertContaining(childBuilder, "public ChildDtoChildBuilder name(String name)"); + assertContaining(childBuilder, "public ChildDtoChildBuilder age(int age)"); + assertNotContaining(childBuilder, "Supplier<"); + } } diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderConfigurationReaderTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderConfigurationReaderTest.java index 80ddcc7c..18993690 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderConfigurationReaderTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderConfigurationReaderTest.java @@ -34,13 +34,16 @@ /** * Integration tests for {@link - * org.javahelpers.simple.builders.processor.util.BuilderConfigurationReader}. + * org.javahelpers.simple.builders.processor.processing.BuilderConfigurationReader}. * - *

    Verifies configuration reading from various sources through end-to-end compilation: + *

    Verifies configuration reading from various sources through end-to-end compilation. Both + * {@code @SimpleBuilder} (which is itself a built-in {@code @SimpleBuilder.Template}) and custom + * template annotations are discovered and resolved through the same path. * *

    @@ -53,8 +56,8 @@ class BuilderConfigurationReaderTest { /** * Test: Builder respects configuration from @SimpleBuilder.Options annotation. * - *

    Verifies BuilderConfigurationReader.readFromOptions() correctly reads and applies all - * options. + *

    Verifies the built-in {@code @SimpleBuilder} template is resolved and its inline {@code + * options()} override the template defaults. */ @Test void readFromOptions_WithOptionsAnnotation_AppliesAllOptions() { @@ -134,10 +137,10 @@ public class PersonDto { } /** - * Test: Builder respects configuration from template annotation. + * Test: Builder respects configuration from a custom template annotation. * - *

    Verifies BuilderConfigurationReader.readFromTemplate() correctly detects and applies - * template configuration. + *

    Verifies a custom annotation meta-annotated with {@code @SimpleBuilder.Template} is resolved + * through the same path as the built-in {@code @SimpleBuilder}. */ @Test void readFromTemplate_WithTemplateAnnotation_AppliesTemplateConfiguration() { @@ -244,10 +247,11 @@ class PersonDto { } /** - * Test: Options annotation overrides template annotation (proper priority). + * Test: Inline @SimpleBuilder options override a custom template annotation. * - *

    Verifies BuilderConfigurationReader.resolveConfiguration() applies correct priority: Options - * > Template > Compiler args > Defaults + *

    Verifies resolveConfiguration() applies correct priority when both the built-in + * {@code @SimpleBuilder} and a custom template are present: direct {@code @SimpleBuilder} options + * win over the custom template, which in turn wins over compiler arguments and defaults. */ @Test void resolveConfiguration_OptionsOverridesTemplate_AppliesPriorityCorrectly() { diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java index 4a2daf22..568301ab 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java @@ -98,7 +98,7 @@ void shouldLogDebugMessagesWhenVerboseModeEnabled() { "[DEBUG] simple-builders: Processing round started. Found 1 annotated elements.", "[DEBUG] Processing element: VerboseTest", "[DEBUG] ├─ Resolving configuration for element: VerboseTest", - "[DEBUG] │ ├─ Template annotations ignored because @SimpleBuilder is present in DIRECT scope", + "[DEBUG] │ ├─ Built-in template @SimpleBuilder found in DIRECT scope", "[DEBUG] │ └─ Resulting configuration resolved: BuilderConfiguration[", "[DEBUG] ├─ Extracting builder definition from: test.VerboseTest", "[DEBUG] │ ├─ Builder will be generated as: test.VerboseTestBuilder",