From a723561bf362a593e9420251a476b10610151e2d Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sat, 1 Nov 2025 17:36:19 +0100 Subject: [PATCH 01/63] Extending SimpleBuilder annotation to support options and to have AccessModifiers in Annotations --- .../core/annotations/SimpleBuilder.java | 295 +++++++++++++++++- .../builders/core/enums/AccessModifier.java | 60 ++++ 2 files changed, 353 insertions(+), 2 deletions(-) create mode 100644 core/src/main/java/org/javahelpers/simple/builders/core/enums/AccessModifier.java 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 6a112cdb..781c581e 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 @@ -25,11 +25,302 @@ package org.javahelpers.simple.builders.core.annotations; 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.enums.AccessModifier; -/** Annotation to be placed on all classes, for which a builder should be generated. */ +/** + * Annotation to mark classes for builder generation. + * + *

Triggers generation of a fluent builder class with support for various patterns and helper + * methods. Can be used standalone or combined with {@link Options} for fine-grained control. + * + *

Available configuration options: + * + *

+ * + *

Use {@link Template} to create reusable configuration presets. + * + * @see Options + * @see Template + */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.CLASS) -public @interface SimpleBuilder {} +public @interface SimpleBuilder { + + /** + * Configuration options for builder generation. + * + *

Allows fine-grained control over what gets generated in the builder class. Can be used with + * {@link SimpleBuilder} or as part of {@link Template}. + * + *

All options have sensible defaults and can be overridden via compiler options using {@code + * -A} flag. + */ + @Retention(RetentionPolicy.CLASS) + @Target(ElementType.TYPE) + @interface Options { + // === Generation Options === + /** + * Generate a supplier method by which the user of this builder could define a function, which + * supplies the value for this field.
+ * The generated method has the parameter-type {@code Supplier} with T being the type of the + * field. Default: true Compiler option: -Asimple.builders.generateFieldSupplier + */ + boolean generateFieldSupplier() default true; + + /** + * Generate a provider method with parameter-type {@code Provider} with T being the type of + * the field.
+ * This is only done for complex field types, so that users could use setter to change the + * properties of that parameter. Default: true Compiler option: + * -Asimple.builders.generateFieldProvider + */ + boolean generateFieldProvider() default true; + + /** + * Generate a builder provider method with parameter-type {@code Provider>} with T + * being the type of the field
+ * This is only done for complex field types, which have a recognized builder so that users + * could use the chained builder methods to set the value of this complex field.
+ * Default: true Compiler option: -Asimple.builders.generateBuilderProvider + */ + boolean generateBuilderProvider() default true; + + /** + * Generate conditional logic method (conditional)
+ * Default: true Compiler option: -Asimple.builders.generateConditionalHelper + */ + boolean generateConditionalHelper() default true; + + // === Access Control === + /** + * Access level for generated builder class. + * + *

Default: {@link AccessModifier#PUBLIC PUBLIC} + * + *

Compiler option: -Asimple.builders.builderAccess (values: PUBLIC, PROTECTED, + * PACKAGE_PRIVATE, PRIVATE) + */ + AccessModifier builderAccess() default AccessModifier.PUBLIC; + + /** + * Access level for generated builder methods. + * + *

Default: {@link AccessModifier#PUBLIC PUBLIC} + * + *

Compiler option: -Asimple.builders.methodAccess (values: PUBLIC, PROTECTED, + * PACKAGE_PRIVATE, PRIVATE) + */ + AccessModifier methodAccess() default AccessModifier.PUBLIC; + + // === Collection Options === + /** + * Generate helper methods with VarArgs for Lists and Sets.
+ * Default: true Compiler option: -Asimple.builders.generateVarArgsHelpers + */ + boolean generateVarArgsHelpers() default true; + + /** + * Generate helper methods with a ArrayListBuilder supplier for lists instead of simple + * supplier, which would not allow to use in a chanined way:
+ * Example with ArrayListBuilder:
+ * + *

{@code
+     * MannschaftDto mannschaft = MannschaftDtoBuilder()
+     *     .create()
+     *     .mitglieder(mb -> mb.add("Max").add("Moritz"))
+     *     .build();
+     * }
+ * + * Instead of
+ * + *
{@code
+     * MannschaftDto mannschaft = MannschaftDtoBuilder()
+     *     .create()
+     *     .mitglieder(mitglieder -> {
+     *         mitglieder.add("Max");
+     *         mitglieder.add("Moritz");
+     *     })
+     *     .build();
+     * }
+ * + * Default: true Compiler option: -Asimple.builders.usingArrayListBuilder + */ + boolean usingArrayListBuilder() default true; + + /** + * Generate helper methods with a ArrayListBuilderWithElementBuilders supplier for lists of + * complex objects instead of simple supplier, which would not allow to use in a chanined way: + *
+ * Example with ArrayListBuilderWithElementBuilders:
+ * + *
{@code
+     * MannschaftDto mannschaft = MannschaftDtoBuilder()
+     *     .create()
+     *     .mitglieder(mb -> mb
+     *         .add(pb -> pb.name("Max").alter(20))
+     *         .add(pb -> pb.name("Moritz").alter(22)))
+     *     .build();
+     * }
+ * + * Instead of
+ * + *
{@code
+     * MannschaftDto mannschaft = MannschaftDtoBuilder()
+     *     .create()
+     *     .mitglieder(mitglieder -> {
+     *         mitglieder.add(new PersonDto("Max", 20));
+     *         mitglieder.add(new PersonDto("Moritz", 22));
+     *     })
+     *     .build();
+     * }
+ * + * Default: true Compiler option: -Asimple.builders.generateWithInterface + */ + boolean usingArrayListBuilderWithElementBuilders() default true; + + /** + * Generate helper methods with a ArrayListBuilder supplier for lists instead of simple + * supplier, which would not allow to use in a chanined way:
+ * Example with ArrayListBuilder:
+ * + *
{@code
+     * MannschaftDto mannschaft = MannschaftDtoBuilder()
+     *     .create()
+     *     .mitglieder(mb -> mb.add("Max").add("Moritz"))
+     *     .build();
+     * }
+ * + * Instead of
+ * + *
{@code
+     * MannschaftDto mannschaft = MannschaftDtoBuilder()
+     *     .create()
+     *     .mitglieder(mitglieder -> {
+     *         mitglieder.add("Max");
+     *         mitglieder.add("Moritz");
+     *     })
+     *     .build();
+     * }
+ * + * Default: true Compiler option: -Asimple.builders.usingHashSetBuilder + */ + boolean usingHashSetBuilder() default true; + + /** + * Generate helper methods with a HashSetBuilderWithElementBuilders supplier for lists of + * complex objects instead of simple supplier, which would not allow to use in a chanined way: + *
+ * Example with HashSetBuilderWithElementBuilders:
+ * + *
{@code
+     * MannschaftDto mannschaft = MannschaftDtoBuilder()
+     *     .create()
+     *     .mitglieder(mb -> mb
+     *         .add(pb -> pb.name("Max").alter(20))
+     *         .add(pb -> pb.name("Moritz").alter(22)))
+     *     .build();
+     * }
+ * + * Instead of
+ * + *
{@code
+     * MannschaftDto mannschaft = MannschaftDtoBuilder()
+     *     .create()
+     *     .mitglieder(mitglieder -> {
+     *         mitglieder.add(new PersonDto("Max", 20));
+     *         mitglieder.add(new PersonDto("Moritz", 22));
+     *     })
+     *     .build();
+     * }
+ * + * Default: true Compiler option: -Asimple.builders.generateWithInterface + */ + boolean usingHashSetBuilderWithElementBuilders() default true; + + /** + * Generate helper methods with a HashMapBuilder supplier for maps instead of simple supplier, + * which would not allow to use in a chanined way:
+ * Example with HashMapBuilder:
+ * + *
{@code
+     * MannschaftDto mannschaft = MannschaftDtoBuilder()
+     *     .create()
+     *     .mitglieder(mb -> mb.put(20, "Max").put(22, "Moritz"))
+     *     .build();
+     * }
+ * + * Instead of
+ * + *
{@code
+     * MannschaftDto mannschaft = MannschaftDtoBuilder()
+     *     .create()
+     *     .mitglieder(mitglieder -> {
+     *         mitglieder.put(20, "Max");
+     *         mitglieder.put(22, "Moritz");
+     *     })
+     *     .build();
+     * }
+ * + * Default: true Compiler option: -Asimple.builders.usingHashMapBuilder + */ + boolean usingHashMapBuilder() default true; + + /** + * Generate With interface for integrating builder into DTOs.
+ * Default: true Compiler option: -Asimple.builders.generateWithInterface + */ + boolean generateWithInterface() default true; + } + + /** + * Meta-annotation for creating custom SimpleBuilder annotation templates. + * + *

This allows you to create custom annotations that pre-configure SimpleBuilder options. The + * custom annotation itself will be treated as @SimpleBuilder by the processor and will + * automatically apply the configured options. + * + *

Example: + * + *

{@code
+   * @SimpleBuilder.Template(options = @SimpleBuilder.Options(
+   *     generateSupplier = true,
+   *     generateProvider = true,
+   *     generateToString = true
+   * ))
+   * @Retention(RetentionPolicy.CLASS)
+   * @Target(ElementType.TYPE)
+   * public @interface FullFeaturedBuilder {
+   * }
+   *
+   * // Usage - just use the template annotation, no @SimpleBuilder needed
+   * @FullFeaturedBuilder
+   * public class PersonDto {
+   *     private String name;
+   * }
+   * }
+ */ + @Retention(RetentionPolicy.CLASS) + @Target(ElementType.ANNOTATION_TYPE) + @Inherited + @interface Template { + /** + * The options to apply when this template is used. + * + * @return the builder configuration options + */ + Options options(); + } +} diff --git a/core/src/main/java/org/javahelpers/simple/builders/core/enums/AccessModifier.java b/core/src/main/java/org/javahelpers/simple/builders/core/enums/AccessModifier.java new file mode 100644 index 00000000..789d04fc --- /dev/null +++ b/core/src/main/java/org/javahelpers/simple/builders/core/enums/AccessModifier.java @@ -0,0 +1,60 @@ +/* + * MIT License + * + * Copyright (c) 2025 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.core.enums; + +/** + * Enum representing Java access modifiers for generated builder classes and methods. + * + *

This enum is used to control the visibility of generated builders and their methods through + * the {@link org.javahelpers.simple.builders.core.annotations.SimpleBuilder.Options} annotation. + */ +public enum AccessModifier { + /** Public access - accessible from anywhere */ + PUBLIC("public"), + + /** Protected access - accessible within the same package and subclasses */ + PROTECTED("protected"), + + /** Package-private access (default) - accessible only within the same package */ + PACKAGE_PRIVATE(""), + + /** Private access - accessible only within the same class */ + PRIVATE("private"); + + private final String javaKeyword; + + AccessModifier(String javaKeyword) { + this.javaKeyword = javaKeyword; + } + + /** + * Get the Java keyword for this access modifier. + * + * @return the Java keyword (e.g., "public", "private"), or empty string for package-private + */ + public String getJavaKeyword() { + return javaKeyword; + } +} From 06670976327d609e04531dab1a929f288cf163f9 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sat, 1 Nov 2025 17:51:37 +0100 Subject: [PATCH 02/63] Switching simple.builders.* to simplebuilders.* compiler arguments --- .../core/annotations/SimpleBuilder.java | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) 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 781c581e..6dda0de0 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 @@ -76,7 +76,7 @@ * Generate a supplier method by which the user of this builder could define a function, which * supplies the value for this field.
* The generated method has the parameter-type {@code Supplier} with T being the type of the - * field. Default: true Compiler option: -Asimple.builders.generateFieldSupplier + * field. Default: true Compiler option: -Asimplebuilder.generateFieldSupplier */ boolean generateFieldSupplier() default true; @@ -85,7 +85,7 @@ * the field.
* This is only done for complex field types, so that users could use setter to change the * properties of that parameter. Default: true Compiler option: - * -Asimple.builders.generateFieldProvider + * -Asimplebuilder.generateFieldProvider */ boolean generateFieldProvider() default true; @@ -94,13 +94,13 @@ * being the type of the field
* This is only done for complex field types, which have a recognized builder so that users * could use the chained builder methods to set the value of this complex field.
- * Default: true Compiler option: -Asimple.builders.generateBuilderProvider + * Default: true Compiler option: -Asimplebuilder.generateBuilderProvider */ boolean generateBuilderProvider() default true; /** * Generate conditional logic method (conditional)
- * Default: true Compiler option: -Asimple.builders.generateConditionalHelper + * Default: true Compiler option: -Asimplebuilder.generateConditionalHelper */ boolean generateConditionalHelper() default true; @@ -110,7 +110,7 @@ * *

Default: {@link AccessModifier#PUBLIC PUBLIC} * - *

Compiler option: -Asimple.builders.builderAccess (values: PUBLIC, PROTECTED, + *

Compiler option: -Asimplebuilder.builderAccess (values: PUBLIC, PROTECTED, * PACKAGE_PRIVATE, PRIVATE) */ AccessModifier builderAccess() default AccessModifier.PUBLIC; @@ -120,7 +120,7 @@ * *

Default: {@link AccessModifier#PUBLIC PUBLIC} * - *

Compiler option: -Asimple.builders.methodAccess (values: PUBLIC, PROTECTED, + *

Compiler option: -Asimplebuilder.methodAccess (values: PUBLIC, PROTECTED, * PACKAGE_PRIVATE, PRIVATE) */ AccessModifier methodAccess() default AccessModifier.PUBLIC; @@ -128,7 +128,7 @@ // === Collection Options === /** * Generate helper methods with VarArgs for Lists and Sets.
- * Default: true Compiler option: -Asimple.builders.generateVarArgsHelpers + * Default: true Compiler option: -Asimplebuilder.generateVarArgsHelpers */ boolean generateVarArgsHelpers() default true; @@ -156,7 +156,7 @@ * .build(); * } * - * Default: true Compiler option: -Asimple.builders.usingArrayListBuilder + * Default: true Compiler option: -Asimplebuilder.usingArrayListBuilder */ boolean usingArrayListBuilder() default true; @@ -187,7 +187,7 @@ * .build(); * } * - * Default: true Compiler option: -Asimple.builders.generateWithInterface + * Default: true Compiler option: -Asimplebuilder.usingArrayListBuilderWithElementBuilders */ boolean usingArrayListBuilderWithElementBuilders() default true; @@ -215,7 +215,7 @@ * .build(); * } * - * Default: true Compiler option: -Asimple.builders.usingHashSetBuilder + * Default: true Compiler option: -Asimplebuilder.usingHashSetBuilder */ boolean usingHashSetBuilder() default true; @@ -246,7 +246,7 @@ * .build(); * } * - * Default: true Compiler option: -Asimple.builders.generateWithInterface + * Default: true Compiler option: -Asimplebuilder.usingHashSetBuilderWithElementBuilders */ boolean usingHashSetBuilderWithElementBuilders() default true; @@ -274,13 +274,13 @@ * .build(); * } * - * Default: true Compiler option: -Asimple.builders.usingHashMapBuilder + * Default: true Compiler option: -Asimplebuilder.usingHashMapBuilder */ boolean usingHashMapBuilder() default true; /** * Generate With interface for integrating builder into DTOs.
- * Default: true Compiler option: -Asimple.builders.generateWithInterface + * Default: true Compiler option: -Asimplebuilder.generateWithInterface */ boolean generateWithInterface() default true; } From ca19a284dc3655ed0c704d4c29d54e3981d0a9f3 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sat, 1 Nov 2025 17:52:59 +0100 Subject: [PATCH 03/63] Adding documentation for this feature --- README.md | 15 ++ docs/CONFIGURATION.md | 521 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 536 insertions(+) create mode 100644 docs/CONFIGURATION.md diff --git a/README.md b/README.md index 89897e93..1865c628 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ - [Conditional Builder Logic](#conditional-builder-logic) - [Collections and Nested Objects](#collections-and-nested-objects) - [With Interface Pattern](#with-interface-pattern) + - [Builder Configuration](#builder-configuration) - [Contributing](#contributing) - [License](#license) - [Acknowledgements](#acknowledgements) @@ -257,6 +258,20 @@ Person youngerPerson = person.with(p -> p.age(29)); The `With` interface provides type-safe setter methods that mirror the builder's API, making it easy to create object variations without manually copying all fields. +## Builder Configuration + +Simple Builders provides extensive configuration options to customize the generated builder code. You can control: + +- Field setter generation (Supplier, Provider, Builder patterns) +- Conditional logic helpers +- Access modifiers for builders and methods +- Collection helper methods +- Integration features + +Configuration can be applied per-class using `@SimpleBuilder.Options` annotation or project-wide using compiler options. + +📖 **For complete documentation, examples, and all available options, see the [Configuration Guide](docs/CONFIGURATION.md).** + ## Contributing Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for: diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md new file mode 100644 index 00000000..d6d2db8f --- /dev/null +++ b/docs/CONFIGURATION.md @@ -0,0 +1,521 @@ +# Configuration + +Simple-builders supports fine-grained configuration through the `@SimpleBuilder.Options` annotation and compiler options. + +## Table of Contents + +- [Overview](#overview) +- [Annotation Configuration](#annotation-configuration) +- [Template Annotations](#template-annotations) +- [Compiler Options](#compiler-options) + - [Maven Configuration](#maven-configuration) + - [Gradle Configuration](#gradle-configuration) + - [IntelliJ IDEA Configuration](#intellij-idea-configuration) +- [Configuration Options](#configuration-options) + - [Field Setter Generation](#field-setter-generation) + - [Conditional Logic](#conditional-logic) + - [Access Control](#access-control) + - [Collection Helpers](#collection-helpers) + - [Integration](#integration) +- [Examples](#examples) + - [Minimal Builder](#minimal-builder) + - [Internal API Builder](#internal-api-builder) + - [Collection-Heavy Builder](#collection-heavy-builder) + - [Minimal Builder Template](#minimal-builder-template) + - [Project-Wide Defaults](#project-wide-defaults) +- [Priority Rules](#priority-rules) + - [Example: Priority in Action](#example-priority-in-action) +- [AccessModifier Enum](#accessmodifier-enum) +- [Troubleshooting](#troubleshooting) + - [Compiler Options Not Working](#compiler-options-not-working) + - [Annotation Values Not Applied](#annotation-values-not-applied) + - [Access Level Issues](#access-level-issues) + - [Template Annotations Not Working](#template-annotations-not-working) +- [Best Practices](#best-practices) +- [Reference](#reference) + - [All Compiler Options](#all-compiler-options) + - [Complete Options Example](#complete-options-example) + +## Overview + +Configuration follows a priority system: +1. **Annotation values** - Highest priority +2. **Compiler options** - Medium priority +3. **Default values** - Lowest priority + +This allows you to set project-wide defaults while still being able to override them per-class when needed. + +## Annotation Configuration + +Configure individual builders using `@SimpleBuilder` with `@SimpleBuilder.Options`: + +```java +@SimpleBuilder +@SimpleBuilder.Options( + generateFieldSupplier = true, + generateFieldProvider = true, + generateBuilderProvider = true, + generateConditionalHelper = true, + builderAccess = AccessModifier.PUBLIC, + methodAccess = AccessModifier.PUBLIC, + generateVarArgsHelpers = true, + usingArrayListBuilder = true, + usingHashMapBuilder = true, + generateWithInterface = true +) +public class PersonDto { + private String name; + private int age; + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + + public int getAge() { return age; } + public void setAge(int age) { this.age = age; } +} +``` + +## Template Annotations + +Create reusable configuration presets with custom template annotations: + +```java +@SimpleBuilder.Template(options = @SimpleBuilder.Options( + generateFieldSupplier = false, + generateFieldProvider = false, + generateBuilderProvider = false, + generateConditionalHelper = false, + generateVarArgsHelpers = false, + usingArrayListBuilder = false, + usingArrayListBuilderWithElementBuilders = false, + usingHashSetBuilder = false, + usingHashSetBuilderWithElementBuilders = false, + usingHashMapBuilder = false, + generateWithInterface = false +)) +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.TYPE) +public @interface MinimalBuilder {} +``` + +Then use your template: + +```java +@MinimalBuilder // No need for @SimpleBuilder - template includes it! +public class PersonDto { + private String name; +} +``` + +## Compiler Options + +Set project-wide defaults via compiler options. These apply to all builders unless overridden by annotations. + +### Maven Configuration + +```xml + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 17 + 17 + + + io.github.java-helpers + simple-builders-processor + ${simple-builders.version} + + + + -Asimplebuilder.generateFieldSupplier=true + -Asimplebuilder.generateFieldProvider=true + -Asimplebuilder.builderAccess=PUBLIC + -Asimplebuilder.usingArrayListBuilder=true + + + +``` + +### Gradle Configuration + +```gradle +dependencies { + annotationProcessor "io.github.java-helpers:simple-builders-processor:${simpleBuildersVersion}" +} + +compileJava { + options.compilerArgs += [ + "-Asimplebuilder.generateFieldSupplier=true", + "-Asimplebuilder.generateFieldProvider=true", + "-Asimplebuilder.builderAccess=PUBLIC" + ] +} +``` + +### IntelliJ IDEA Configuration + +1. Go to **Settings → Build, Execution, Deployment → Compiler → Java Compiler** +2. Add to **Additional command line parameters**: + ``` + -Asimplebuilder.generateFieldSupplier=true -Asimplebuilder.builderAccess=PUBLIC + ``` + +## Configuration Options + +### Field Setter Generation + +| Option | Type | Default | Compiler Option | Description | +|--------|------|---------|------------------|-------------| +| `generateFieldSupplier` | boolean | `true` | `-Asimplebuilder.generateFieldSupplier` | Generate setter methods accepting `Supplier` for field values | +| `generateFieldProvider` | boolean | `true` | `-Asimplebuilder.generateFieldProvider` | Generate setter methods accepting `Provider` for complex field types | +| `generateBuilderProvider` | boolean | `true` | `-Asimplebuilder.generateBuilderProvider` | Generate setter methods accepting `Provider>` for buildable types | + +### Conditional Logic + +| Option | Type | Default | Compiler Option | Description | +|--------|------|---------|------------------|-------------| +| `generateConditionalHelper` | boolean | `true` | `-Asimplebuilder.generateConditionalHelper` | Generate conditional/when methods for fluent conditional logic | + +### Access Control + +| Option | Type | Default | Values | Compiler Option | Description | +|--------|------|---------|--------|------------------|-------------| +| `builderAccess` | AccessModifier | `PUBLIC` | `PUBLIC`, `PROTECTED`, `PACKAGE_PRIVATE`, `PRIVATE` | `-Asimplebuilder.builderAccess` | Visibility level for generated builder class | +| `methodAccess` | AccessModifier | `PUBLIC` | `PUBLIC`, `PROTECTED`, `PACKAGE_PRIVATE`, `PRIVATE` | `-Asimplebuilder.methodAccess` | Visibility level for generated builder methods | + +### Collection Helpers + +| Option | Type | Default | Compiler Option | Description | +|--------|------|---------|------------------|-------------| +| `generateVarArgsHelpers` | boolean | `true` | `-Asimplebuilder.generateVarArgsHelpers` | Generate varargs methods for Lists and Sets | +| `usingArrayListBuilder` | boolean | `true` | `-Asimplebuilder.usingArrayListBuilder` | Use chaining ArrayListBuilder for Lists | +| `usingArrayListBuilderWithElementBuilders` | boolean | `true` | `-Asimplebuilder.usingArrayListBuilderWithElementBuilders` | Use ArrayListBuilderWithElementBuilders for Lists of complex objects | +| `usingHashSetBuilder` | boolean | `true` | `-Asimplebuilder.usingHashSetBuilder` | Use chaining HashSetBuilder for Sets | +| `usingHashSetBuilderWithElementBuilders` | boolean | `true` | `-Asimplebuilder.usingHashSetBuilderWithElementBuilders` | Use HashSetBuilderWithElementBuilders for Sets of complex objects | +| `usingHashMapBuilder` | boolean | `true` | `-Asimplebuilder.usingHashMapBuilder` | Use chaining HashMapBuilder for Maps | + +### Integration + +| Option | Type | Default | Compiler Option | Description | +|--------|------|---------|------------------|-------------| +| `generateWithInterface` | boolean | `true` | `-Asimplebuilder.generateWithInterface` | Generate With interface for DTO integration | + +## Examples + +### Minimal Builder + +Generate only essential builder methods: + +```java +@SimpleBuilder +@SimpleBuilder.Options( + generateFieldSupplier = false, + generateFieldProvider = false, + generateBuilderProvider = false, + generateConditionalHelper = false, + generateVarArgsHelpers = false, + usingArrayListBuilder = false, + usingHashMapBuilder = false, + generateWithInterface = false +) +public class MinimalDto { + private String name; + + public String getName() { return name; } + public void setName(String name) { this.name = name; } +} +``` + +**Generated**: Only basic builder methods (`create()`, field setters, `build()`) + +### Internal API Builder + +Create builders for internal use only: + +```java +@SimpleBuilder +@SimpleBuilder.Options( + builderAccess = AccessModifier.PACKAGE_PRIVATE, + methodAccess = AccessModifier.PACKAGE_PRIVATE +) +public class InternalConfig { + private String secretKey; + + public String getSecretKey() { return secretKey; } + public void setSecretKey(String secretKey) { this.secretKey = secretKey; } +} +``` + +**Generated**: Package-private builder and methods, only accessible within the same package + +### Collection-Heavy Builder + +Optimize for collection manipulation: + +```java +@SimpleBuilder +@SimpleBuilder.Options( + generateVarArgsHelpers = true, + usingArrayListBuilder = true, + usingArrayListBuilderWithElementBuilders = true, + usingHashSetBuilder = true, + usingHashMapBuilder = true +) +public class TeamDto { + private List memberNames; + private Set members; + private Map memberMap; +} +``` + +**Generated**: Chained collection builders for fluent collection manipulation + +Example usage: +```java +TeamDto team = TeamDtoBuilder.create() + .memberNames(list -> list.add("Alice").add("Bob")) + .members(set -> set + .add(person -> person.name("Alice").age(30)) + .add(person -> person.name("Bob").age(25))) + .memberMap(map -> map.put("Alice", alice).put("Bob", bob)) + .build(); +``` + +### Minimal Builder Template + +Create a reusable template for lightweight builders: + +```java +@SimpleBuilder.Template(options = @SimpleBuilder.Options( + generateFieldSupplier = false, + generateFieldProvider = false, + generateBuilderProvider = false, + generateConditionalHelper = false, + generateVarArgsHelpers = false, + usingArrayListBuilder = false, + usingArrayListBuilderWithElementBuilders = false, + usingHashSetBuilder = false, + usingHashSetBuilderWithElementBuilders = false, + usingHashMapBuilder = false, + generateWithInterface = false +)) +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.TYPE) +public @interface MinimalBuilder {} +``` + +Use everywhere: +```java +@MinimalBuilder +public class CustomerDto { + private String name; + private List orders; +} +``` + +### Project-Wide Defaults + +Set sensible defaults for your entire project: + +```xml + + + + -Asimplebuilder.generateFieldSupplier=true + -Asimplebuilder.generateFieldProvider=true + -Asimplebuilder.generateBuilderProvider=true + + + -Asimplebuilder.builderAccess=PACKAGE_PRIVATE + + + -Asimplebuilder.usingArrayListBuilder=true + -Asimplebuilder.usingHashMapBuilder=true + +``` + +Override per-class when needed: + +```java +@SimpleBuilder +@SimpleBuilder.Options(builderAccess = AccessModifier.PUBLIC) // Override: make this one public +public class PublicApiDto { + private String data; +} + +@SimpleBuilder // Uses project defaults: package-private +public class InternalDto { + private String data; +} +``` + +## Priority Rules + +Configuration resolution follows these priority rules: + +1. **Annotation values** (highest priority) + - Values in `@SimpleBuilder.Options(...)` always win +2. **Compiler options** (medium priority) + - Used when no annotation value is specified +3. **Default values** (lowest priority) + - Used when neither annotation nor compiler option is specified + +### Example: Priority in Action + +```java +// Compiler option: -Asimple.builders.generateFieldSupplier=false +// Global default: true + +@SimpleBuilder +@SimpleBuilder.Options(generateFieldSupplier = true) // Annotation wins! +public class Person { + private String name; +} + +@SimpleBuilder // Uses compiler option (false) +public class Company { + private String name; +} + +// No compiler option set +@SimpleBuilder // Uses global default (true) +public class Product { + private String name; +} +``` + +## AccessModifier Enum + +The `AccessModifier` enum provides type-safe access control: + +```java +public enum AccessModifier { + PUBLIC, // Accessible from anywhere + PROTECTED, // Accessible within same package and subclasses + PACKAGE_PRIVATE, // Accessible only within same package (default Java visibility) + PRIVATE // Accessible only within same class +} +``` + +Use in annotations: +```java +@SimpleBuilder.Options( + builderAccess = AccessModifier.PACKAGE_PRIVATE, + methodAccess = AccessModifier.PUBLIC +) +``` + +Or in compiler options: +``` +-Asimplebuilder.builderAccess=PACKAGE_PRIVATE +``` + +## Troubleshooting + +### Compiler Options Not Working + +1. **Check option names**: Ensure you're using the full option name (e.g., `-Asimplebuilder.generateFieldSupplier`) +2. **Verify processor is running**: Ensure annotation processor is configured correctly +3. **Check IDE configuration**: Some IDEs need special configuration for compiler options +4. **Clean and rebuild**: Run `mvn clean compile` to ensure fresh build + +### Annotation Values Not Applied + +1. **Verify annotation import**: Import `org.javahelpers.simple.builders.core.annotations.SimpleBuilder` +2. **Check annotation placement**: Use `@SimpleBuilder` on the class, `@SimpleBuilder.Options` on the same class +3. **Verify compilation**: Recompile after changing annotations +4. **Check for syntax errors**: Ensure AccessModifier enum values are correct + +### Access Level Issues + +1. **Package-private builders**: Ensure DTO and builder are in the same package +2. **Private builders**: May cause issues with reflection-based frameworks +3. **Protected builders**: Only accessible to subclasses +4. **AccessModifier import**: Import `org.javahelpers.simple.builders.core.enums.AccessModifier` + +### Template Annotations Not Working + +1. **Check @SimpleBuilder.Template**: Ensure template annotation has `@SimpleBuilder.Template` +2. **Verify options parameter**: Template must specify `options = @SimpleBuilder.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 + +## Best Practices + +1. **Use templates for common patterns**: Define reusable templates for your project +2. **Set project-wide defaults**: Configure sensible defaults via compiler options +3. **Override sparingly**: Only override when truly necessary +4. **Document templates**: Add JavaDoc to custom template annotations +5. **Use type-safe enums**: Prefer `AccessModifier` enum over string values +6. **Test configurations**: Verify generated code meets expectations +7. **Consider team preferences**: Choose configurations that work for everyone + +## Reference + +### All Compiler Options + +``` +# Field Setter Generation +-Asimplebuilder.generateFieldSupplier=true|false +-Asimplebuilder.generateFieldProvider=true|false +-Asimplebuilder.generateBuilderProvider=true|false + +# Conditional Logic +-Asimplebuilder.generateConditionalHelper=true|false + +# Access Control +-Asimplebuilder.builderAccess=PUBLIC|PROTECTED|PACKAGE_PRIVATE|PRIVATE +-Asimplebuilder.methodAccess=PUBLIC|PROTECTED|PACKAGE_PRIVATE|PRIVATE + +# Collection Helpers +-Asimplebuilder.generateVarArgsHelpers=true|false +-Asimplebuilder.usingArrayListBuilder=true|false +-Asimplebuilder.usingArrayListBuilderWithElementBuilders=true|false +-Asimplebuilder.usingHashSetBuilder=true|false +-Asimplebuilder.usingHashSetBuilderWithElementBuilders=true|false +-Asimplebuilder.usingHashMapBuilder=true|false + +# Integration +-Asimplebuilder.generateWithInterface=true|false +``` + +### Complete Options Example + +```java +@SimpleBuilder +@SimpleBuilder.Options( + // Field Setter Generation + generateFieldSupplier = true, + generateFieldProvider = true, + generateBuilderProvider = true, + + // Conditional Logic + generateConditionalHelper = true, + + // Access Control + builderAccess = AccessModifier.PUBLIC, + methodAccess = AccessModifier.PUBLIC, + + // Collection Helpers + generateVarArgsHelpers = true, + usingArrayListBuilder = true, + usingArrayListBuilderWithElementBuilders = true, + usingHashSetBuilder = true, + usingHashSetBuilderWithElementBuilders = true, + usingHashMapBuilder = true, + + // Integration + generateWithInterface = true +) +public class ExampleDto { + private String name; +} +``` + +--- + +**Last Updated**: 2025-11-01 +**Version**: 0.2.0 +**Related**: [README.md](../README.md) From d9972cdf7dd48ccd7059d7356f26cd1906d194c7 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sat, 1 Nov 2025 18:00:10 +0100 Subject: [PATCH 04/63] Adding a record which contains all configurations, default values are read from Annotation-Options --- .../processor/dtos/BuilderConfiguration.java | 275 ++++++++++++++++++ .../util/AnnotationDefaultReader.java | 119 ++++++++ 2 files changed, 394 insertions(+) create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/util/AnnotationDefaultReader.java diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java new file mode 100644 index 00000000..ecf4670e --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java @@ -0,0 +1,275 @@ +/* + * MIT License + * + * Copyright (c) 2025 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.processor.dtos; + +import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; +import org.javahelpers.simple.builders.processor.util.AnnotationDefaultReader; + +/** + * Configuration for builder generation. Combines annotation values with compiler options. Priority: + * Annotation > Compiler Options > Defaults + * + *

Properties and defaults are read from {@link SimpleBuilder.Options} annotation defaults via + * reflection. + * + * @param generateFieldSupplier Generate field supplier methods + * @param generateFieldProvider Generate field provider methods + * @param generateBuilderProvider Generate builder provider methods + * @param generateConditionalHelper Generate conditional logic methods + * @param builderAccess Access level for builder class + * @param methodAccess Access level for builder methods + * @param generateVarArgsHelpers Generate varargs helper methods + * @param usingArrayListBuilder Use ArrayListBuilder for lists + * @param usingArrayListBuilderWithElementBuilders Use ArrayListBuilderWithElementBuilders + * @param usingHashSetBuilder Use HashSetBuilder for sets + * @param usingHashSetBuilderWithElementBuilders Use HashSetBuilderWithElementBuilders + * @param usingHashMapBuilder Use HashMapBuilder for maps + * @param generateWithInterface Generate With interface + * @param hasAnnotationOverride Whether annotation was used (vs just compiler options) + */ +public record BuilderConfiguration( + boolean generateFieldSupplier, + boolean generateFieldProvider, + boolean generateBuilderProvider, + boolean generateConditionalHelper, + String builderAccess, + String methodAccess, + boolean generateVarArgsHelpers, + boolean usingArrayListBuilder, + boolean usingArrayListBuilderWithElementBuilders, + boolean usingHashSetBuilder, + boolean usingHashSetBuilderWithElementBuilders, + boolean usingHashMapBuilder, + boolean generateWithInterface, + boolean hasAnnotationOverride) { + + // === Convenience accessors with 'is' prefix for boolean properties === + public boolean isGenerateSupplier() { + return generateFieldSupplier; + } + + public boolean isGenerateProvider() { + return generateFieldProvider; + } + + public boolean isGenerateBuilderProvider() { + return generateBuilderProvider; + } + + public boolean isGenerateConditionalLogic() { + return generateConditionalHelper; + } + + public boolean isGenerateWithInterface() { + return generateWithInterface; + } + + public boolean isGenerateVarArgsHelpers() { + return generateVarArgsHelpers; + } + + public boolean isUsingArrayListBuilder() { + return usingArrayListBuilder; + } + + public boolean isUsingArrayListBuilderWithElementBuilders() { + return usingArrayListBuilderWithElementBuilders; + } + + public boolean isUsingHashSetBuilder() { + return usingHashSetBuilder; + } + + public boolean isUsingHashSetBuilderWithElementBuilders() { + return usingHashSetBuilderWithElementBuilders; + } + + public boolean isUsingHashMapBuilder() { + return usingHashMapBuilder; + } + + // === String accessors === + public String getBuilderAccess() { + return builderAccess; + } + + public String getMethodAccess() { + return methodAccess; + } + + // === Builder Pattern === + // Default values are read from SimpleBuilder.Options annotation via reflection + public static class Builder { + // === Field Setter Generation (defaults from SimpleBuilder.Options) === + private boolean generateFieldSupplier = + AnnotationDefaultReader.getBooleanDefault( + SimpleBuilder.Options.class, "generateFieldSupplier", true); + private boolean generateFieldProvider = + AnnotationDefaultReader.getBooleanDefault( + SimpleBuilder.Options.class, "generateFieldProvider", true); + private boolean generateBuilderProvider = + AnnotationDefaultReader.getBooleanDefault( + SimpleBuilder.Options.class, "generateBuilderProvider", true); + + // === Conditional Logic === + private boolean generateConditionalHelper = + AnnotationDefaultReader.getBooleanDefault( + SimpleBuilder.Options.class, "generateConditionalHelper", true); + + // === Access Control === + private String builderAccess = + AnnotationDefaultReader.getEnumDefaultAsString( + SimpleBuilder.Options.class, "builderAccess", "PUBLIC"); + private String methodAccess = + AnnotationDefaultReader.getEnumDefaultAsString( + SimpleBuilder.Options.class, "methodAccess", "PUBLIC"); + + // === Collection Options === + private boolean generateVarArgsHelpers = + AnnotationDefaultReader.getBooleanDefault( + SimpleBuilder.Options.class, "generateVarArgsHelpers", true); + private boolean usingArrayListBuilder = + AnnotationDefaultReader.getBooleanDefault( + SimpleBuilder.Options.class, "usingArrayListBuilder", true); + private boolean usingArrayListBuilderWithElementBuilders = + AnnotationDefaultReader.getBooleanDefault( + SimpleBuilder.Options.class, "usingArrayListBuilderWithElementBuilders", true); + private boolean usingHashSetBuilder = + AnnotationDefaultReader.getBooleanDefault( + SimpleBuilder.Options.class, "usingHashSetBuilder", true); + private boolean usingHashSetBuilderWithElementBuilders = + AnnotationDefaultReader.getBooleanDefault( + SimpleBuilder.Options.class, "usingHashSetBuilderWithElementBuilders", true); + private boolean usingHashMapBuilder = + AnnotationDefaultReader.getBooleanDefault( + SimpleBuilder.Options.class, "usingHashMapBuilder", true); + + // === Integration === + private boolean generateWithInterface = + AnnotationDefaultReader.getBooleanDefault( + SimpleBuilder.Options.class, "generateWithInterface", true); + + // === Source Information === + private boolean hasAnnotationOverride = false; + + // === Setters === + public Builder generateSupplier(boolean value) { + this.generateFieldSupplier = value; + return this; + } + + public Builder generateProvider(boolean value) { + this.generateFieldProvider = value; + return this; + } + + public Builder generateBuilderProvider(boolean value) { + this.generateBuilderProvider = value; + return this; + } + + public Builder generateConditionalLogic(boolean value) { + this.generateConditionalHelper = value; + return this; + } + + public Builder generateWithInterface(boolean value) { + this.generateWithInterface = value; + return this; + } + + public Builder generateVarArgsHelpers(boolean value) { + this.generateVarArgsHelpers = value; + return this; + } + + public Builder usingUtilBuilderForGenerate(boolean value) { + this.usingArrayListBuilder = value; + return this; + } + + public Builder usingArrayListBuilder(boolean value) { + this.usingArrayListBuilder = value; + return this; + } + + public Builder usingArrayListBuilderWithElementBuilders(boolean value) { + this.usingArrayListBuilderWithElementBuilders = value; + return this; + } + + public Builder usingHashSetBuilder(boolean value) { + this.usingHashSetBuilder = value; + return this; + } + + public Builder usingHashSetBuilderWithElementBuilders(boolean value) { + this.usingHashSetBuilderWithElementBuilders = value; + return this; + } + + public Builder usingHashMapBuilder(boolean value) { + this.usingHashMapBuilder = value; + return this; + } + + public Builder builderAccess(String value) { + this.builderAccess = value; + return this; + } + + public Builder methodAccess(String value) { + this.methodAccess = value; + return this; + } + + public Builder hasAnnotationOverride(boolean value) { + this.hasAnnotationOverride = value; + return this; + } + + public BuilderConfiguration build() { + return new BuilderConfiguration( + generateFieldSupplier, + generateFieldProvider, + generateBuilderProvider, + generateConditionalHelper, + builderAccess, + methodAccess, + generateVarArgsHelpers, + usingArrayListBuilder, + usingArrayListBuilderWithElementBuilders, + usingHashSetBuilder, + usingHashSetBuilderWithElementBuilders, + usingHashMapBuilder, + generateWithInterface, + hasAnnotationOverride); + } + } + + public static Builder builder() { + return new Builder(); + } +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/AnnotationDefaultReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/AnnotationDefaultReader.java new file mode 100644 index 00000000..26d9e5f4 --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/AnnotationDefaultReader.java @@ -0,0 +1,119 @@ +/* + * MIT License + * + * Copyright (c) 2025 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.processor.util; + +import java.lang.reflect.Method; + +/** + * Utility class for reading default values from annotations via reflection. + * + *

This class provides methods to extract default values from annotation methods, which is useful + * for maintaining a single source of truth for configuration defaults. + */ +public final class AnnotationDefaultReader { + + private AnnotationDefaultReader() { + // Utility class - prevent instantiation + } + + /** + * Read a boolean default value from an annotation method. + * + * @param annotationClass The annotation class containing the method + * @param methodName The annotation method name + * @param fallback Fallback value if reflection fails + * @return The default value from the annotation, or fallback if not found + */ + public static boolean getBooleanDefault( + Class annotationClass, String methodName, boolean fallback) { + try { + Method method = annotationClass.getMethod(methodName); + Object defaultValue = method.getDefaultValue(); + return defaultValue != null ? (Boolean) defaultValue : fallback; + } catch (Exception e) { + // Fallback to provided value if reflection fails + return fallback; + } + } + + /** + * Read an enum default value from an annotation method and convert to String. + * + * @param annotationClass The annotation class containing the method + * @param methodName The annotation method name + * @param fallback Fallback value if reflection fails + * @return The enum name as String, or fallback if not found + */ + public static String getEnumDefaultAsString( + Class annotationClass, String methodName, String fallback) { + try { + Method method = annotationClass.getMethod(methodName); + Object defaultValue = method.getDefaultValue(); + return defaultValue != null ? ((Enum) defaultValue).name() : fallback; + } catch (Exception e) { + // Fallback to provided value if reflection fails + return fallback; + } + } + + /** + * Read a String default value from an annotation method. + * + * @param annotationClass The annotation class containing the method + * @param methodName The annotation method name + * @param fallback Fallback value if reflection fails + * @return The default value from the annotation, or fallback if not found + */ + public static String getStringDefault( + Class annotationClass, String methodName, String fallback) { + try { + Method method = annotationClass.getMethod(methodName); + Object defaultValue = method.getDefaultValue(); + return defaultValue != null ? (String) defaultValue : fallback; + } catch (Exception e) { + // Fallback to provided value if reflection fails + return fallback; + } + } + + /** + * Read an integer default value from an annotation method. + * + * @param annotationClass The annotation class containing the method + * @param methodName The annotation method name + * @param fallback Fallback value if reflection fails + * @return The default value from the annotation, or fallback if not found + */ + public static int getIntDefault(Class annotationClass, String methodName, int fallback) { + try { + Method method = annotationClass.getMethod(methodName); + Object defaultValue = method.getDefaultValue(); + return defaultValue != null ? (Integer) defaultValue : fallback; + } catch (Exception e) { + // Fallback to provided value if reflection fails + return fallback; + } + } +} From 6ecd2f53a1d9cca2f0c797a0d89e20dbb13d606c Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sat, 1 Nov 2025 18:01:26 +0100 Subject: [PATCH 05/63] Adding BuilderConfiguration to ProcessingContext --- .../processor/util/ProcessingContext.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ProcessingContext.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ProcessingContext.java index 4e8c835e..3766c172 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ProcessingContext.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ProcessingContext.java @@ -31,6 +31,7 @@ import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Elements; import javax.lang.model.util.Types; +import org.javahelpers.simple.builders.processor.dtos.BuilderConfiguration; /** * Context object that wraps Elements, Types, and logging utilities from annotation processing, @@ -42,6 +43,7 @@ public final class ProcessingContext { private final Elements elementUtils; private final Types typeUtils; private final ProcessingLogger logger; + private BuilderConfiguration configuration; /** * Creates a new processing context. @@ -56,6 +58,24 @@ public ProcessingContext(Elements elementUtils, Types typeUtils, ProcessingLogge this.logger = logger; } + /** + * Get the builder configuration for the current element being processed. + * + * @return the builder configuration + */ + public BuilderConfiguration getConfiguration() { + return configuration; + } + + /** + * Set the builder configuration for the current element being processed. + * + * @param configuration the builder configuration + */ + public void setConfiguration(BuilderConfiguration configuration) { + this.configuration = configuration; + } + /** * Get a type element by its fully qualified class name. * From 4a49f19121828f9203e4110078d934abc78a7269 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sat, 1 Nov 2025 18:36:27 +0100 Subject: [PATCH 06/63] Refactoring Annotation options and BuilderConfiguration --- .../core/annotations/SimpleBuilder.java | 45 ++-- .../builders/core/enums/AccessModifier.java | 4 + .../builders/core/enums/OptionState.java | 62 +++++ .../processor/dtos/BuilderConfiguration.java | 233 +++++++++++------- 4 files changed, 236 insertions(+), 108 deletions(-) create mode 100644 core/src/main/java/org/javahelpers/simple/builders/core/enums/OptionState.java 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 6dda0de0..ae24a2b3 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 @@ -30,6 +30,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.javahelpers.simple.builders.core.enums.AccessModifier; +import org.javahelpers.simple.builders.core.enums.OptionState; /** * Annotation to mark classes for builder generation. @@ -76,33 +77,33 @@ * Generate a supplier method by which the user of this builder could define a function, which * supplies the value for this field.
* The generated method has the parameter-type {@code Supplier} with T being the type of the - * field. Default: true Compiler option: -Asimplebuilder.generateFieldSupplier + * field. Default: ENABLED Compiler option: -Asimplebuilder.generateFieldSupplier */ - boolean generateFieldSupplier() default true; + OptionState generateFieldSupplier() default OptionState.DEFAULT; /** * Generate a provider method with parameter-type {@code Provider} with T being the type of * the field.
* This is only done for complex field types, so that users could use setter to change the - * properties of that parameter. Default: true Compiler option: + * properties of that parameter. Default: ENABLED Compiler option: * -Asimplebuilder.generateFieldProvider */ - boolean generateFieldProvider() default true; + OptionState generateFieldProvider() default OptionState.DEFAULT; /** * Generate a builder provider method with parameter-type {@code Provider>} with T * being the type of the field
* This is only done for complex field types, which have a recognized builder so that users * could use the chained builder methods to set the value of this complex field.
- * Default: true Compiler option: -Asimplebuilder.generateBuilderProvider + * Default: ENABLED Compiler option: -Asimplebuilder.generateBuilderProvider */ - boolean generateBuilderProvider() default true; + OptionState generateBuilderProvider() default OptionState.DEFAULT; /** * Generate conditional logic method (conditional)
- * Default: true Compiler option: -Asimplebuilder.generateConditionalHelper + * Default: ENABLED Compiler option: -Asimplebuilder.generateConditionalHelper */ - boolean generateConditionalHelper() default true; + OptionState generateConditionalHelper() default OptionState.DEFAULT; // === Access Control === /** @@ -128,9 +129,9 @@ // === Collection Options === /** * Generate helper methods with VarArgs for Lists and Sets.
- * Default: true Compiler option: -Asimplebuilder.generateVarArgsHelpers + * Default: ENABLED Compiler option: -Asimplebuilder.generateVarArgsHelpers */ - boolean generateVarArgsHelpers() default true; + OptionState generateVarArgsHelpers() default OptionState.DEFAULT; /** * Generate helper methods with a ArrayListBuilder supplier for lists instead of simple @@ -156,9 +157,9 @@ * .build(); * } * - * Default: true Compiler option: -Asimplebuilder.usingArrayListBuilder + * Default: ENABLED Compiler option: -Asimplebuilder.usingArrayListBuilder */ - boolean usingArrayListBuilder() default true; + OptionState usingArrayListBuilder() default OptionState.DEFAULT; /** * Generate helper methods with a ArrayListBuilderWithElementBuilders supplier for lists of @@ -187,9 +188,9 @@ * .build(); * } * - * Default: true Compiler option: -Asimplebuilder.usingArrayListBuilderWithElementBuilders + * Default: ENABLED Compiler option: -Asimplebuilder.usingArrayListBuilderWithElementBuilders */ - boolean usingArrayListBuilderWithElementBuilders() default true; + OptionState usingArrayListBuilderWithElementBuilders() default OptionState.DEFAULT; /** * Generate helper methods with a ArrayListBuilder supplier for lists instead of simple @@ -215,9 +216,9 @@ * .build(); * } * - * Default: true Compiler option: -Asimplebuilder.usingHashSetBuilder + * Default: ENABLED Compiler option: -Asimplebuilder.usingHashSetBuilder */ - boolean usingHashSetBuilder() default true; + OptionState usingHashSetBuilder() default OptionState.DEFAULT; /** * Generate helper methods with a HashSetBuilderWithElementBuilders supplier for lists of @@ -246,9 +247,9 @@ * .build(); * } * - * Default: true Compiler option: -Asimplebuilder.usingHashSetBuilderWithElementBuilders + * Default: ENABLED Compiler option: -Asimplebuilder.usingHashSetBuilderWithElementBuilders */ - boolean usingHashSetBuilderWithElementBuilders() default true; + OptionState usingHashSetBuilderWithElementBuilders() default OptionState.DEFAULT; /** * Generate helper methods with a HashMapBuilder supplier for maps instead of simple supplier, @@ -274,15 +275,15 @@ * .build(); * } * - * Default: true Compiler option: -Asimplebuilder.usingHashMapBuilder + * Default: ENABLED Compiler option: -Asimplebuilder.usingHashMapBuilder */ - boolean usingHashMapBuilder() default true; + OptionState usingHashMapBuilder() default OptionState.DEFAULT; /** * Generate With interface for integrating builder into DTOs.
- * Default: true Compiler option: -Asimplebuilder.generateWithInterface + * Default: ENABLED Compiler option: -Asimplebuilder.generateWithInterface */ - boolean generateWithInterface() default true; + OptionState generateWithInterface() default OptionState.DEFAULT; } /** diff --git a/core/src/main/java/org/javahelpers/simple/builders/core/enums/AccessModifier.java b/core/src/main/java/org/javahelpers/simple/builders/core/enums/AccessModifier.java index 789d04fc..309c0471 100644 --- a/core/src/main/java/org/javahelpers/simple/builders/core/enums/AccessModifier.java +++ b/core/src/main/java/org/javahelpers/simple/builders/core/enums/AccessModifier.java @@ -31,6 +31,10 @@ * the {@link org.javahelpers.simple.builders.core.annotations.SimpleBuilder.Options} annotation. */ public enum AccessModifier { + + /** Default access */ + DEFAULT("public"), + /** Public access - accessible from anywhere */ PUBLIC("public"), diff --git a/core/src/main/java/org/javahelpers/simple/builders/core/enums/OptionState.java b/core/src/main/java/org/javahelpers/simple/builders/core/enums/OptionState.java new file mode 100644 index 00000000..8a7383fa --- /dev/null +++ b/core/src/main/java/org/javahelpers/simple/builders/core/enums/OptionState.java @@ -0,0 +1,62 @@ +/* + * MIT License + * + * Copyright (c) 2025 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.core.enums; + +/** + * Represents the three-state configuration for builder options. + * + *

This enum allows distinguishing between: + * + *

+ * + *

Priority resolution: Annotation (ENABLED/DISABLED) > Compiler Options > Built-in Defaults + * + *

Example: + * + *

{@code
+ * // Global config via compiler option: -Asimplebuilder.generateFieldSupplier=false
+ *
+ * // Per-class override to enable:
+ * @SimpleBuilder.Options(generateFieldSupplier = OptionState.ENABLED)
+ * public class MyDto { }
+ * }
+ */ +public enum OptionState { + /** + * Use the inherited value from compiler options or built-in defaults. This is the default state + * when no explicit override is specified. + */ + DEFAULT, + + /** Explicitly enable this option, overriding any global configuration or defaults. */ + ENABLED, + + /** Explicitly disable this option, overriding any global configuration or defaults. */ + DISABLED +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java index ecf4670e..db112b8b 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java @@ -24,8 +24,10 @@ package org.javahelpers.simple.builders.processor.dtos; -import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; -import org.javahelpers.simple.builders.processor.util.AnnotationDefaultReader; +import org.javahelpers.simple.builders.core.enums.AccessModifier; +import org.javahelpers.simple.builders.core.enums.OptionState; +import static org.javahelpers.simple.builders.core.enums.OptionState.*; +import static org.javahelpers.simple.builders.core.enums.AccessModifier.*; /** * Configuration for builder generation. Combines annotation values with compiler options. Priority: @@ -50,206 +52,265 @@ * @param hasAnnotationOverride Whether annotation was used (vs just compiler options) */ public record BuilderConfiguration( - boolean generateFieldSupplier, - boolean generateFieldProvider, - boolean generateBuilderProvider, - boolean generateConditionalHelper, - String builderAccess, - String methodAccess, - boolean generateVarArgsHelpers, - boolean usingArrayListBuilder, - boolean usingArrayListBuilderWithElementBuilders, - boolean usingHashSetBuilder, - boolean usingHashSetBuilderWithElementBuilders, - boolean usingHashMapBuilder, - boolean generateWithInterface, - boolean hasAnnotationOverride) { + OptionState generateFieldSupplier, + OptionState generateFieldProvider, + OptionState generateBuilderProvider, + OptionState generateConditionalHelper, + AccessModifier builderAccess, + AccessModifier methodAccess, + OptionState generateVarArgsHelpers, + OptionState usingArrayListBuilder, + OptionState usingArrayListBuilderWithElementBuilders, + OptionState usingHashSetBuilder, + OptionState usingHashSetBuilderWithElementBuilders, + OptionState usingHashMapBuilder, + OptionState generateWithInterface, + OptionState hasAnnotationOverride) { + + public static final BuilderConfiguration DEFAULT = + builder() + .generateSupplier(ENABLED) + .generateProvider(ENABLED) + .generateBuilderProvider(ENABLED) + .generateConditionalLogic(ENABLED) + .builderAccess(PUBLIC) + .methodAccess(PUBLIC) + .generateVarArgsHelpers(ENABLED) + .usingArrayListBuilder(ENABLED) + .usingArrayListBuilderWithElementBuilders(ENABLED) + .usingHashSetBuilder(ENABLED) + .usingHashSetBuilderWithElementBuilders(ENABLED) + .usingHashMapBuilder(ENABLED) + .generateWithInterface(ENABLED) + .hasAnnotationOverride(ENABLED) + .build(); // === Convenience accessors with 'is' prefix for boolean properties === public boolean isGenerateSupplier() { - return generateFieldSupplier; + return generateFieldSupplier == ENABLED; } public boolean isGenerateProvider() { - return generateFieldProvider; + return generateFieldProvider == ENABLED; } public boolean isGenerateBuilderProvider() { - return generateBuilderProvider; + return generateBuilderProvider == ENABLED; } public boolean isGenerateConditionalLogic() { - return generateConditionalHelper; + return generateConditionalHelper == ENABLED; } public boolean isGenerateWithInterface() { - return generateWithInterface; + return generateWithInterface == ENABLED; } public boolean isGenerateVarArgsHelpers() { - return generateVarArgsHelpers; + return generateVarArgsHelpers == ENABLED; } public boolean isUsingArrayListBuilder() { - return usingArrayListBuilder; + return usingArrayListBuilder == ENABLED; } public boolean isUsingArrayListBuilderWithElementBuilders() { - return usingArrayListBuilderWithElementBuilders; + return usingArrayListBuilderWithElementBuilders == ENABLED; } public boolean isUsingHashSetBuilder() { - return usingHashSetBuilder; + return usingHashSetBuilder == ENABLED; } public boolean isUsingHashSetBuilderWithElementBuilders() { - return usingHashSetBuilderWithElementBuilders; + return usingHashSetBuilderWithElementBuilders == ENABLED; } public boolean isUsingHashMapBuilder() { - return usingHashMapBuilder; + return usingHashMapBuilder == ENABLED; } // === String accessors === - public String getBuilderAccess() { + public AccessModifier getBuilderAccess() { return builderAccess; } - public String getMethodAccess() { + public AccessModifier getMethodAccess() { return methodAccess; } // === Builder Pattern === - // Default values are read from SimpleBuilder.Options annotation via reflection + // All defaults are DEFAULT to allow proper three-state resolution public static class Builder { - // === Field Setter Generation (defaults from SimpleBuilder.Options) === - private boolean generateFieldSupplier = - AnnotationDefaultReader.getBooleanDefault( - SimpleBuilder.Options.class, "generateFieldSupplier", true); - private boolean generateFieldProvider = - AnnotationDefaultReader.getBooleanDefault( - SimpleBuilder.Options.class, "generateFieldProvider", true); - private boolean generateBuilderProvider = - AnnotationDefaultReader.getBooleanDefault( - SimpleBuilder.Options.class, "generateBuilderProvider", true); + // === Field Setter Generation === + private OptionState generateFieldSupplier = OptionState.DEFAULT; + private OptionState generateFieldProvider = OptionState.DEFAULT; + private OptionState generateBuilderProvider = OptionState.DEFAULT; // === Conditional Logic === - private boolean generateConditionalHelper = - AnnotationDefaultReader.getBooleanDefault( - SimpleBuilder.Options.class, "generateConditionalHelper", true); + private OptionState generateConditionalHelper = OptionState.DEFAULT; // === Access Control === - private String builderAccess = - AnnotationDefaultReader.getEnumDefaultAsString( - SimpleBuilder.Options.class, "builderAccess", "PUBLIC"); - private String methodAccess = - AnnotationDefaultReader.getEnumDefaultAsString( - SimpleBuilder.Options.class, "methodAccess", "PUBLIC"); + private AccessModifier builderAccess = AccessModifier.DEFAULT; + private AccessModifier methodAccess = AccessModifier.DEFAULT; // === Collection Options === - private boolean generateVarArgsHelpers = - AnnotationDefaultReader.getBooleanDefault( - SimpleBuilder.Options.class, "generateVarArgsHelpers", true); - private boolean usingArrayListBuilder = - AnnotationDefaultReader.getBooleanDefault( - SimpleBuilder.Options.class, "usingArrayListBuilder", true); - private boolean usingArrayListBuilderWithElementBuilders = - AnnotationDefaultReader.getBooleanDefault( - SimpleBuilder.Options.class, "usingArrayListBuilderWithElementBuilders", true); - private boolean usingHashSetBuilder = - AnnotationDefaultReader.getBooleanDefault( - SimpleBuilder.Options.class, "usingHashSetBuilder", true); - private boolean usingHashSetBuilderWithElementBuilders = - AnnotationDefaultReader.getBooleanDefault( - SimpleBuilder.Options.class, "usingHashSetBuilderWithElementBuilders", true); - private boolean usingHashMapBuilder = - AnnotationDefaultReader.getBooleanDefault( - SimpleBuilder.Options.class, "usingHashMapBuilder", true); + private OptionState generateVarArgsHelpers = OptionState.DEFAULT; + private OptionState usingArrayListBuilder = OptionState.DEFAULT; + private OptionState usingArrayListBuilderWithElementBuilders = OptionState.DEFAULT; + private OptionState usingHashSetBuilder = OptionState.DEFAULT; + private OptionState usingHashSetBuilderWithElementBuilders = OptionState.DEFAULT; + private OptionState usingHashMapBuilder = OptionState.DEFAULT; // === Integration === - private boolean generateWithInterface = - AnnotationDefaultReader.getBooleanDefault( - SimpleBuilder.Options.class, "generateWithInterface", true); + private OptionState generateWithInterface = OptionState.DEFAULT; // === Source Information === - private boolean hasAnnotationOverride = false; + private OptionState hasAnnotationOverride = OptionState.DEFAULT; // === Setters === - public Builder generateSupplier(boolean value) { + public Builder generateSupplier(OptionState value) { this.generateFieldSupplier = value; return this; } - public Builder generateProvider(boolean value) { + public Builder generateSupplier(boolean value) { + this.generateFieldSupplier = value ? ENABLED : DISABLED; + return this; + } + + public Builder generateProvider(OptionState value) { this.generateFieldProvider = value; return this; } - public Builder generateBuilderProvider(boolean value) { + public Builder generateProvider(boolean value) { + this.generateFieldProvider = value ? ENABLED : DISABLED; + return this; + } + + public Builder generateBuilderProvider(OptionState value) { this.generateBuilderProvider = value; return this; } - public Builder generateConditionalLogic(boolean value) { + public Builder generateBuilderProvider(boolean value) { + this.generateBuilderProvider = value ? ENABLED : DISABLED; + return this; + } + + public Builder generateConditionalLogic(OptionState value) { this.generateConditionalHelper = value; return this; } - public Builder generateWithInterface(boolean value) { + public Builder generateConditionalLogic(boolean value) { + this.generateConditionalHelper = value ? ENABLED : DISABLED; + return this; + } + + public Builder generateWithInterface(OptionState value) { this.generateWithInterface = value; return this; } - public Builder generateVarArgsHelpers(boolean value) { + public Builder generateWithInterface(boolean value) { + this.generateWithInterface = value ? ENABLED : DISABLED; + return this; + } + + public Builder generateVarArgsHelpers(OptionState value) { this.generateVarArgsHelpers = value; return this; } - public Builder usingUtilBuilderForGenerate(boolean value) { + public Builder generateVarArgsHelpers(boolean value) { + this.generateVarArgsHelpers = value ? ENABLED : DISABLED; + return this; + } + + public Builder usingArrayListBuilder(OptionState value) { this.usingArrayListBuilder = value; return this; } public Builder usingArrayListBuilder(boolean value) { - this.usingArrayListBuilder = value; + this.usingArrayListBuilder = value ? ENABLED : DISABLED; return this; } - public Builder usingArrayListBuilderWithElementBuilders(boolean value) { + public Builder usingArrayListBuilderWithElementBuilders(OptionState value) { this.usingArrayListBuilderWithElementBuilders = value; return this; } - public Builder usingHashSetBuilder(boolean value) { + public Builder usingArrayListBuilderWithElementBuilders(boolean value) { + this.usingArrayListBuilderWithElementBuilders = + value ? ENABLED : DISABLED; + return this; + } + + public Builder usingHashSetBuilder(OptionState value) { this.usingHashSetBuilder = value; return this; } - public Builder usingHashSetBuilderWithElementBuilders(boolean value) { + public Builder usingHashSetBuilder(boolean value) { + this.usingHashSetBuilder = value ? ENABLED : DISABLED; + return this; + } + + public Builder usingHashSetBuilderWithElementBuilders(OptionState value) { this.usingHashSetBuilderWithElementBuilders = value; return this; } - public Builder usingHashMapBuilder(boolean value) { + public Builder usingHashSetBuilderWithElementBuilders(boolean value) { + this.usingHashSetBuilderWithElementBuilders = + value ? ENABLED : DISABLED; + return this; + } + + public Builder usingHashMapBuilder(OptionState value) { this.usingHashMapBuilder = value; return this; } - public Builder builderAccess(String value) { + public Builder usingHashMapBuilder(boolean value) { + this.usingHashMapBuilder = value ? ENABLED : DISABLED; + return this; + } + + public Builder builderAccess(AccessModifier value) { this.builderAccess = value; return this; } - public Builder methodAccess(String value) { + public Builder builderAccess(String value) { + this.builderAccess = AccessModifier.valueOf(value.toUpperCase()); + return this; + } + + public Builder methodAccess(AccessModifier value) { this.methodAccess = value; return this; } - public Builder hasAnnotationOverride(boolean value) { + public Builder methodAccess(String value) { + this.methodAccess = AccessModifier.valueOf(value.toUpperCase()); + return this; + } + + public Builder hasAnnotationOverride(OptionState value) { this.hasAnnotationOverride = value; return this; } + public Builder hasAnnotationOverride(boolean value) { + this.hasAnnotationOverride = value ? ENABLED : DISABLED; + return this; + } + public BuilderConfiguration build() { return new BuilderConfiguration( generateFieldSupplier, From 167233161a3994bd2b151b1352d04ad287115bf5 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sat, 1 Nov 2025 20:29:52 +0100 Subject: [PATCH 07/63] Putting all supported compiler-arguments into CompilerArgumentsEnum and adding an entry in README.md --- README.md | 27 ++++ .../enums/CompilerArgumentsEnum.java | 140 ++++++++++++++++++ 2 files changed, 167 insertions(+) create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java diff --git a/README.md b/README.md index 1865c628..89a642e1 100644 --- a/README.md +++ b/README.md @@ -270,6 +270,33 @@ Simple Builders provides extensive configuration options to customize the genera Configuration can be applied per-class using `@SimpleBuilder.Options` annotation or project-wide using compiler options. +### Compiler Arguments + +All configuration options are available as compiler arguments using the `-A` flag. For example: + +```bash +javac -Asimplebuilder.verbose=true \ + -Asimplebuilder.generateFieldSupplier=false \ + YourClass.java +``` + +Or in Maven: + +```xml + + org.apache.maven.plugins + maven-compiler-plugin + + + -Asimplebuilder.verbose=true + -Asimplebuilder.generateFieldSupplier=false + + + +``` + +📋 **For a complete list of all available compiler arguments, see [`CompilerArgumentsEnum`](processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java).** + 📖 **For complete documentation, examples, and all available options, see the [Configuration Guide](docs/CONFIGURATION.md).** ## Contributing diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java new file mode 100644 index 00000000..12292aef --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java @@ -0,0 +1,140 @@ +/* + * MIT License + * + * Copyright (c) 2025 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.processor.enums; + +/** + * Enumeration of all builder configuration compiler arguments. + * + *

This enum provides a single source of truth for all option names used in: + * + *

+ * + *

Each enum constant provides the option name and the full compiler argument. + */ +public enum CompilerArgumentsEnum { + // === Field Setter Generation === + /** Option for field supplier generation. */ + GENERATE_FIELD_SUPPLIER("generateFieldSupplier"), + + /** Option for field provider generation. */ + GENERATE_FIELD_PROVIDER("generateFieldProvider"), + + /** Option for builder provider generation. */ + GENERATE_BUILDER_PROVIDER("generateBuilderProvider"), + + // === Conditional Logic === + /** Option for conditional helper generation. */ + GENERATE_CONDITIONAL_HELPER("generateConditionalHelper"), + + // === Access Control === + /** Option for builder access level. */ + BUILDER_ACCESS("builderAccess"), + + /** Option for method access level. */ + METHOD_ACCESS("methodAccess"), + + // === Collection Options === + /** Option for varargs helper generation. */ + GENERATE_VAR_ARGS_HELPERS("generateVarArgsHelpers"), + + /** Option for ArrayList builder usage. */ + USING_ARRAY_LIST_BUILDER("usingArrayListBuilder"), + + /** Option for ArrayList builder with element builders usage. */ + USING_ARRAY_LIST_BUILDER_WITH_ELEMENT_BUILDERS("usingArrayListBuilderWithElementBuilders"), + + /** Option for HashSet builder usage. */ + USING_HASH_SET_BUILDER("usingHashSetBuilder"), + + /** Option for HashSet builder with element builders usage. */ + USING_HASH_SET_BUILDER_WITH_ELEMENT_BUILDERS("usingHashSetBuilderWithElementBuilders"), + + /** Option for HashMap builder usage. */ + USING_HASH_MAP_BUILDER("usingHashMapBuilder"), + + // === Integration === + /** Option for With interface generation. */ + GENERATE_WITH_INTERFACE("generateWithInterface"), + + // === Logging === + /** Option for verbose logging output. */ + VERBOSE("verbose"); + + /** Compiler option prefix for all simple-builders options. */ + private static final String OPTION_PREFIX = "simplebuilder."; + + /** The option name (used in annotation methods). */ + private final String optionName; + + /** + * Constructs a CompilerArgumentsEnum constant. + * + * @param optionName The option name + */ + CompilerArgumentsEnum(String optionName) { + this.optionName = optionName; + } + + /** + * Gets the option name for use in annotation methods. + * + *

Example: {@code "generateFieldSupplier"} + * + * @return The option name + */ + public String getOptionName() { + return optionName; + } + + /** + * Gets the full compiler argument including the package prefix. + * + *

Example: {@code "simplebuilder.generateFieldSupplier"} + * + * @return The full compiler argument + */ + public String getCompilerArgument() { + return OPTION_PREFIX + optionName; + } + + /** + * Finds a CompilerArgumentsEnum by its compiler argument. + * + * @param compilerArgument The compiler argument to search for + * @return The matching CompilerArgumentsEnum, or null if not found + */ + public static CompilerArgumentsEnum fromCompilerArgument(String compilerArgument) { + for (CompilerArgumentsEnum option : values()) { + if (option.getCompilerArgument().equals(compilerArgument)) { + return option; + } + } + return null; + } +} From 32642f2a7d4a950581f0adb439936baa3c25a56d Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sat, 1 Nov 2025 20:41:48 +0100 Subject: [PATCH 08/63] Fixing codeformat --- .../builders/core/annotations/SimpleBuilder.java | 4 ++-- .../processor/dtos/BuilderConfiguration.java | 11 +++++------ .../builders/processor/util/ProcessingLogger.java | 12 ++++++++---- 3 files changed, 15 insertions(+), 12 deletions(-) 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 ae24a2b3..bc200bfe 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 @@ -121,8 +121,8 @@ * *

Default: {@link AccessModifier#PUBLIC PUBLIC} * - *

Compiler option: -Asimplebuilder.methodAccess (values: PUBLIC, PROTECTED, - * PACKAGE_PRIVATE, PRIVATE) + *

Compiler option: -Asimplebuilder.methodAccess (values: PUBLIC, PROTECTED, PACKAGE_PRIVATE, + * PRIVATE) */ AccessModifier methodAccess() default AccessModifier.PUBLIC; diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java index db112b8b..00629910 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java @@ -24,10 +24,11 @@ package org.javahelpers.simple.builders.processor.dtos; +import static org.javahelpers.simple.builders.core.enums.AccessModifier.*; +import static org.javahelpers.simple.builders.core.enums.OptionState.*; + import org.javahelpers.simple.builders.core.enums.AccessModifier; import org.javahelpers.simple.builders.core.enums.OptionState; -import static org.javahelpers.simple.builders.core.enums.OptionState.*; -import static org.javahelpers.simple.builders.core.enums.AccessModifier.*; /** * Configuration for builder generation. Combines annotation values with compiler options. Priority: @@ -245,8 +246,7 @@ public Builder usingArrayListBuilderWithElementBuilders(OptionState value) { } public Builder usingArrayListBuilderWithElementBuilders(boolean value) { - this.usingArrayListBuilderWithElementBuilders = - value ? ENABLED : DISABLED; + this.usingArrayListBuilderWithElementBuilders = value ? ENABLED : DISABLED; return this; } @@ -266,8 +266,7 @@ public Builder usingHashSetBuilderWithElementBuilders(OptionState value) { } public Builder usingHashSetBuilderWithElementBuilders(boolean value) { - this.usingHashSetBuilderWithElementBuilders = - value ? ENABLED : DISABLED; + this.usingHashSetBuilderWithElementBuilders = value ? ENABLED : DISABLED; return this; } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ProcessingLogger.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ProcessingLogger.java index cf4f18bd..9d1fc307 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ProcessingLogger.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ProcessingLogger.java @@ -28,6 +28,7 @@ import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.Element; import javax.tools.Diagnostic; +import org.javahelpers.simple.builders.processor.enums.CompilerArgumentsEnum; /** * Logger for all messages during annotation processing. Providing util-functions for posting @@ -44,13 +45,14 @@ public class ProcessingLogger { /** * Constructs a new ProcessingLogger with the specified ProcessingEnvironment. The Messager is * used to report errors, warnings, and other notices during annotation processing. Debug logging - * is enabled by setting the compiler argument: -Averbose=true + * is enabled by setting the compiler argument: -Averbose=true or -Asimplebuilder.verbose=true * * @param processingEnv the processing environment providing messager and options */ public ProcessingLogger(ProcessingEnvironment processingEnv) { this.messager = processingEnv.getMessager(); - this.debugEnabled = "true".equalsIgnoreCase(processingEnv.getOptions().get("verbose")); + CompilerArgumentsReader reader = new CompilerArgumentsReader(processingEnv); + this.debugEnabled = reader.readBooleanValue(CompilerArgumentsEnum.VERBOSE); } /** @@ -85,7 +87,8 @@ public void info(String format, Object... args) { /** * Posts a debug message with OTHER level. Used for detailed tracing of the builder generation - * process. Only visible when enabled via -Averbose=true compiler argument. + * process. Only visible when enabled via -Averbose=true or -Asimplebuilder.verbose=true compiler + * argument. * * @param message the debug message to be posted */ @@ -97,7 +100,8 @@ public void debug(String message) { } /** - * Posts a debug message with a formatted string. Only visible when enabled via -Averbose=true. + * Posts a debug message with a formatted string. Only visible when enabled via -Averbose=true or + * -Asimplebuilder.verbose=true. * * @param format the format string * @param args arguments referenced by the format specifiers in the format string From bde54d5aab255757e5539e9d3a66a32d129dc1f8 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sat, 1 Nov 2025 20:42:21 +0100 Subject: [PATCH 09/63] Adding dynamic way of defining supported compiler-arguments --- .../builders/processor/BuilderProcessor.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) 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 523e36e4..a3b4fa8e 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 @@ -27,17 +27,18 @@ import static org.javahelpers.simple.builders.processor.util.BuilderDefinitionCreator.extractFromElement; import com.google.auto.service.AutoService; +import java.util.HashSet; import java.util.Set; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.Processor; import javax.annotation.processing.RoundEnvironment; import javax.annotation.processing.SupportedAnnotationTypes; -import javax.annotation.processing.SupportedOptions; import javax.lang.model.SourceVersion; import javax.lang.model.element.Element; import javax.lang.model.element.TypeElement; import org.javahelpers.simple.builders.processor.dtos.BuilderDefinitionDto; +import org.javahelpers.simple.builders.processor.enums.CompilerArgumentsEnum; import org.javahelpers.simple.builders.processor.exceptions.BuilderException; import org.javahelpers.simple.builders.processor.util.JavaCodeGenerator; import org.javahelpers.simple.builders.processor.util.ProcessingContext; @@ -50,7 +51,6 @@ */ @AutoService(Processor.class) @SupportedAnnotationTypes("org.javahelpers.simple.builders.core.annotations.SimpleBuilder") -@SupportedOptions("verbose") public class BuilderProcessor extends AbstractProcessor { private ProcessingContext context; private JavaCodeGenerator codeGenerator; @@ -117,6 +117,16 @@ public boolean process(Set annotations, RoundEnvironment return true; } + @Override + public Set getSupportedOptions() { + Set options = new HashSet<>(); + for (CompilerArgumentsEnum arg : CompilerArgumentsEnum.values()) { + options.add(arg.getOptionName()); // e.g., "verbose" + options.add(arg.getCompilerArgument()); // e.g., "simplebuilder.verbose" + } + return options; + } + @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); From 0bc117d428212229ac970ecde7d7015e9669410c Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sat, 1 Nov 2025 20:48:46 +0100 Subject: [PATCH 10/63] Moving markdown files into docs folder --- README.md | 2 +- CONTRIBUTING.md => docs/CONTRIBUTING.md | 0 DEBUG_LOGGING.md => docs/DEBUG_LOGGING.md | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename CONTRIBUTING.md => docs/CONTRIBUTING.md (100%) rename DEBUG_LOGGING.md => docs/DEBUG_LOGGING.md (100%) diff --git a/README.md b/README.md index 89a642e1..eb471670 100644 --- a/README.md +++ b/README.md @@ -301,7 +301,7 @@ Or in Maven: ## Contributing -Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for: +Contributions are welcome! Please see [CONTRIBUTING.md](docs/CONTRIBUTING.md) for: - Development setup and project structure - Building and testing strategies (important for annotation processor modules) diff --git a/CONTRIBUTING.md b/docs/CONTRIBUTING.md similarity index 100% rename from CONTRIBUTING.md rename to docs/CONTRIBUTING.md diff --git a/DEBUG_LOGGING.md b/docs/DEBUG_LOGGING.md similarity index 100% rename from DEBUG_LOGGING.md rename to docs/DEBUG_LOGGING.md From 358403cfe9eecdbf96c3f4cf6fafdc0752d29576 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sat, 1 Nov 2025 20:49:31 +0100 Subject: [PATCH 11/63] Renaming OptionState.DEFAULT to OptionState.UNSET --- .../core/annotations/SimpleBuilder.java | 22 ++++++++--------- .../builders/core/enums/OptionState.java | 6 ++--- .../processor/dtos/BuilderConfiguration.java | 24 +++++++++---------- 3 files changed, 26 insertions(+), 26 deletions(-) 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 bc200bfe..354adee8 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 @@ -79,7 +79,7 @@ * The generated method has the parameter-type {@code Supplier} with T being the type of the * field. Default: ENABLED Compiler option: -Asimplebuilder.generateFieldSupplier */ - OptionState generateFieldSupplier() default OptionState.DEFAULT; + OptionState generateFieldSupplier() default OptionState.UNSET; /** * Generate a provider method with parameter-type {@code Provider} with T being the type of @@ -88,7 +88,7 @@ * properties of that parameter. Default: ENABLED Compiler option: * -Asimplebuilder.generateFieldProvider */ - OptionState generateFieldProvider() default OptionState.DEFAULT; + OptionState generateFieldProvider() default OptionState.UNSET; /** * Generate a builder provider method with parameter-type {@code Provider>} with T @@ -97,13 +97,13 @@ * could use the chained builder methods to set the value of this complex field.
* Default: ENABLED Compiler option: -Asimplebuilder.generateBuilderProvider */ - OptionState generateBuilderProvider() default OptionState.DEFAULT; + OptionState generateBuilderProvider() default OptionState.UNSET; /** * Generate conditional logic method (conditional)
* Default: ENABLED Compiler option: -Asimplebuilder.generateConditionalHelper */ - OptionState generateConditionalHelper() default OptionState.DEFAULT; + OptionState generateConditionalHelper() default OptionState.UNSET; // === Access Control === /** @@ -131,7 +131,7 @@ * Generate helper methods with VarArgs for Lists and Sets.
* Default: ENABLED Compiler option: -Asimplebuilder.generateVarArgsHelpers */ - OptionState generateVarArgsHelpers() default OptionState.DEFAULT; + OptionState generateVarArgsHelpers() default OptionState.UNSET; /** * Generate helper methods with a ArrayListBuilder supplier for lists instead of simple @@ -159,7 +159,7 @@ * * Default: ENABLED Compiler option: -Asimplebuilder.usingArrayListBuilder */ - OptionState usingArrayListBuilder() default OptionState.DEFAULT; + OptionState usingArrayListBuilder() default OptionState.UNSET; /** * Generate helper methods with a ArrayListBuilderWithElementBuilders supplier for lists of @@ -190,7 +190,7 @@ * * Default: ENABLED Compiler option: -Asimplebuilder.usingArrayListBuilderWithElementBuilders */ - OptionState usingArrayListBuilderWithElementBuilders() default OptionState.DEFAULT; + OptionState usingArrayListBuilderWithElementBuilders() default OptionState.UNSET; /** * Generate helper methods with a ArrayListBuilder supplier for lists instead of simple @@ -218,7 +218,7 @@ * * Default: ENABLED Compiler option: -Asimplebuilder.usingHashSetBuilder */ - OptionState usingHashSetBuilder() default OptionState.DEFAULT; + OptionState usingHashSetBuilder() default OptionState.UNSET; /** * Generate helper methods with a HashSetBuilderWithElementBuilders supplier for lists of @@ -249,7 +249,7 @@ * * Default: ENABLED Compiler option: -Asimplebuilder.usingHashSetBuilderWithElementBuilders */ - OptionState usingHashSetBuilderWithElementBuilders() default OptionState.DEFAULT; + OptionState usingHashSetBuilderWithElementBuilders() default OptionState.UNSET; /** * Generate helper methods with a HashMapBuilder supplier for maps instead of simple supplier, @@ -277,13 +277,13 @@ * * Default: ENABLED Compiler option: -Asimplebuilder.usingHashMapBuilder */ - OptionState usingHashMapBuilder() default OptionState.DEFAULT; + OptionState usingHashMapBuilder() default OptionState.UNSET; /** * Generate With interface for integrating builder into DTOs.
* Default: ENABLED Compiler option: -Asimplebuilder.generateWithInterface */ - OptionState generateWithInterface() default OptionState.DEFAULT; + OptionState generateWithInterface() default OptionState.UNSET; } /** diff --git a/core/src/main/java/org/javahelpers/simple/builders/core/enums/OptionState.java b/core/src/main/java/org/javahelpers/simple/builders/core/enums/OptionState.java index 8a7383fa..cb955d20 100644 --- a/core/src/main/java/org/javahelpers/simple/builders/core/enums/OptionState.java +++ b/core/src/main/java/org/javahelpers/simple/builders/core/enums/OptionState.java @@ -49,10 +49,10 @@ */ public enum OptionState { /** - * Use the inherited value from compiler options or built-in defaults. This is the default state - * when no explicit override is specified. + * Use inherited value from compiler options or built-in defaults. This is the default state when + * the option is not explicitly configured at the annotation level. */ - DEFAULT, + UNSET, /** Explicitly enable this option, overriding any global configuration or defaults. */ ENABLED, diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java index 00629910..ab4c8a10 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java @@ -144,30 +144,30 @@ public AccessModifier getMethodAccess() { // All defaults are DEFAULT to allow proper three-state resolution public static class Builder { // === Field Setter Generation === - private OptionState generateFieldSupplier = OptionState.DEFAULT; - private OptionState generateFieldProvider = OptionState.DEFAULT; - private OptionState generateBuilderProvider = OptionState.DEFAULT; + private OptionState generateFieldSupplier = OptionState.UNSET; + private OptionState generateFieldProvider = OptionState.UNSET; + private OptionState generateBuilderProvider = OptionState.UNSET; // === Conditional Logic === - private OptionState generateConditionalHelper = OptionState.DEFAULT; + private OptionState generateConditionalHelper = OptionState.UNSET; // === Access Control === private AccessModifier builderAccess = AccessModifier.DEFAULT; private AccessModifier methodAccess = AccessModifier.DEFAULT; // === Collection Options === - private OptionState generateVarArgsHelpers = OptionState.DEFAULT; - private OptionState usingArrayListBuilder = OptionState.DEFAULT; - private OptionState usingArrayListBuilderWithElementBuilders = OptionState.DEFAULT; - private OptionState usingHashSetBuilder = OptionState.DEFAULT; - private OptionState usingHashSetBuilderWithElementBuilders = OptionState.DEFAULT; - private OptionState usingHashMapBuilder = OptionState.DEFAULT; + private OptionState generateVarArgsHelpers = OptionState.UNSET; + private OptionState usingArrayListBuilder = OptionState.UNSET; + private OptionState usingArrayListBuilderWithElementBuilders = OptionState.UNSET; + private OptionState usingHashSetBuilder = OptionState.UNSET; + private OptionState usingHashSetBuilderWithElementBuilders = OptionState.UNSET; + private OptionState usingHashMapBuilder = OptionState.UNSET; // === Integration === - private OptionState generateWithInterface = OptionState.DEFAULT; + private OptionState generateWithInterface = OptionState.UNSET; // === Source Information === - private OptionState hasAnnotationOverride = OptionState.DEFAULT; + private OptionState hasAnnotationOverride = OptionState.UNSET; // === Setters === public Builder generateSupplier(OptionState value) { From ed6ac3fec27dedfe7fa0eabcedb19ff876dafe98 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sat, 1 Nov 2025 20:50:48 +0100 Subject: [PATCH 12/63] Adding reader for compilerArguments --- .../builders/processor/BuilderProcessor.java | 11 +- .../processor/dtos/BuilderConfiguration.java | 54 ++++++ .../util/CompilerArgumentsReader.java | 168 ++++++++++++++++++ .../processor/util/ProcessingContext.java | 30 +++- 4 files changed, 261 insertions(+), 2 deletions(-) create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java 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 a3b4fa8e..1d119428 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 @@ -37,9 +37,11 @@ import javax.lang.model.SourceVersion; import javax.lang.model.element.Element; import javax.lang.model.element.TypeElement; +import org.javahelpers.simple.builders.processor.dtos.BuilderConfiguration; import org.javahelpers.simple.builders.processor.dtos.BuilderDefinitionDto; import org.javahelpers.simple.builders.processor.enums.CompilerArgumentsEnum; import org.javahelpers.simple.builders.processor.exceptions.BuilderException; +import org.javahelpers.simple.builders.processor.util.CompilerArgumentsReader; import org.javahelpers.simple.builders.processor.util.JavaCodeGenerator; import org.javahelpers.simple.builders.processor.util.ProcessingContext; import org.javahelpers.simple.builders.processor.util.ProcessingLogger; @@ -60,10 +62,17 @@ public class BuilderProcessor extends AbstractProcessor { public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); ProcessingLogger logger = new ProcessingLogger(processingEnv); + + // Read global configuration from compiler arguments + CompilerArgumentsReader reader = new CompilerArgumentsReader(processingEnv); + BuilderConfiguration globalConfig = reader.readBuilderConfiguration(); + this.context = new ProcessingContext( - processingEnv.getElementUtils(), processingEnv.getTypeUtils(), logger); + processingEnv.getElementUtils(), processingEnv.getTypeUtils(), logger, globalConfig); + context.debug("Loaded global configuration from compiler arguments: %s", globalConfig); this.codeGenerator = new JavaCodeGenerator(processingEnv.getFiler(), logger); + SourceVersion current = processingEnv.getSourceVersion(); this.supportedJdk = isAtLeastJava17(current); if (!this.supportedJdk) { diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java index ab4c8a10..14c0573e 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java @@ -27,6 +27,8 @@ import static org.javahelpers.simple.builders.core.enums.AccessModifier.*; import static org.javahelpers.simple.builders.core.enums.OptionState.*; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; import org.javahelpers.simple.builders.core.enums.AccessModifier; import org.javahelpers.simple.builders.core.enums.OptionState; @@ -140,6 +142,58 @@ public AccessModifier getMethodAccess() { return methodAccess; } + @Override + public String toString() { + ToStringBuilder builder = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE); + + if (generateFieldSupplier != UNSET) { + builder.append("generateFieldSupplier", generateFieldSupplier); + } + if (generateFieldProvider != UNSET) { + builder.append("generateFieldProvider", generateFieldProvider); + } + if (generateBuilderProvider != UNSET) { + builder.append("generateBuilderProvider", generateBuilderProvider); + } + if (generateConditionalHelper != UNSET) { + builder.append("generateConditionalHelper", generateConditionalHelper); + } + if (builderAccess != AccessModifier.DEFAULT) { + builder.append("builderAccess", builderAccess); + } + if (methodAccess != AccessModifier.DEFAULT) { + builder.append("methodAccess", methodAccess); + } + if (generateVarArgsHelpers != UNSET) { + builder.append("generateVarArgsHelpers", generateVarArgsHelpers); + } + if (usingArrayListBuilder != UNSET) { + builder.append("usingArrayListBuilder", usingArrayListBuilder); + } + if (usingArrayListBuilderWithElementBuilders != UNSET) { + builder.append( + "usingArrayListBuilderWithElementBuilders", usingArrayListBuilderWithElementBuilders); + } + if (usingHashSetBuilder != UNSET) { + builder.append("usingHashSetBuilder", usingHashSetBuilder); + } + if (usingHashSetBuilderWithElementBuilders != UNSET) { + builder.append( + "usingHashSetBuilderWithElementBuilders", usingHashSetBuilderWithElementBuilders); + } + if (usingHashMapBuilder != UNSET) { + builder.append("usingHashMapBuilder", usingHashMapBuilder); + } + if (generateWithInterface != UNSET) { + builder.append("generateWithInterface", generateWithInterface); + } + if (hasAnnotationOverride != UNSET) { + builder.append("hasAnnotationOverride", hasAnnotationOverride); + } + + return builder.toString(); + } + // === Builder Pattern === // All defaults are DEFAULT to allow proper three-state resolution public static class Builder { diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java new file mode 100644 index 00000000..5d411811 --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java @@ -0,0 +1,168 @@ +/* + * MIT License + * + * Copyright (c) 2025 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.processor.util; + +import javax.annotation.processing.ProcessingEnvironment; +import org.apache.commons.lang3.Strings; +import org.javahelpers.simple.builders.core.enums.AccessModifier; +import org.javahelpers.simple.builders.core.enums.OptionState; +import org.javahelpers.simple.builders.processor.dtos.BuilderConfiguration; +import org.javahelpers.simple.builders.processor.enums.CompilerArgumentsEnum; + +/** + * Utility class for reading compiler arguments from the annotation processing environment. + * + *

This class provides a centralized way to read compiler arguments using {@link + * CompilerArgumentsEnum} values, ensuring consistent handling of option names and values across the + * processor. + */ +public class CompilerArgumentsReader { + private final ProcessingEnvironment processingEnv; + + /** + * Constructs a new CompilerArgumentsReader. + * + * @param processingEnv the processing environment providing access to compiler options + */ + public CompilerArgumentsReader(ProcessingEnvironment processingEnv) { + this.processingEnv = processingEnv; + } + + /** + * Reads the value of a compiler argument. + * + *

The method looks up the compiler argument using both the full compiler argument name (with + * prefix) and the simple option name (without prefix) for backward compatibility. + * + * @param argument the compiler argument enum to read + * @return the value of the compiler argument, or null if not set + */ + public String readValue(CompilerArgumentsEnum argument) { + // Try with full compiler argument name first (e.g., "simplebuilder.verbose") + String value = processingEnv.getOptions().get(argument.getCompilerArgument()); + + // Fall back to simple option name for backward compatibility (e.g., "verbose") + if (value == null) { + value = processingEnv.getOptions().get(argument.getOptionName()); + } + + return value; + } + + /** + * Reads the value of a compiler argument as a boolean. + * + *

Returns true if the value equals "true" (case-insensitive), false otherwise. + * + * @param argument the compiler argument enum to read + * @return true if the value is "true" (case-insensitive), false otherwise + */ + public boolean readBooleanValue(CompilerArgumentsEnum argument) { + String value = readValue(argument); + return Strings.CI.equalsAny(value, "true", "enabled"); + } + + /** + * Reads the value of a compiler argument as an OptionState. + * + *

Returns ENABLED for "true" or "enabled", DISABLED for "false" or "disabled", and UNSET + * otherwise. + * + * @param argument the compiler argument enum to read + * @return the OptionState value + */ + public OptionState readOptionState(CompilerArgumentsEnum argument) { + String value = readValue(argument); + if (Strings.CI.equalsAny(value, "true", "enabled")) { + return OptionState.ENABLED; + } else if (Strings.CI.equalsAny(value, "false", "disabled")) { + return OptionState.DISABLED; + } + return OptionState.UNSET; + } + + /** + * Reads the value of a compiler argument as an AccessModifier. + * + *

Returns the corresponding AccessModifier enum value, or DEFAULT if not set or invalid. + * + * @param argument the compiler argument enum to read + * @return the AccessModifier value, or DEFAULT if not set + */ + public AccessModifier readAccessModifier(CompilerArgumentsEnum argument) { + String value = readValue(argument); + if (Strings.CI.equals(value, "public")) { + return AccessModifier.PUBLIC; + } else if (Strings.CI.equals(value, "private")) { + return AccessModifier.PRIVATE; + } else if (Strings.CI.equals(value, "package-private")) { + return AccessModifier.PACKAGE_PRIVATE; + } else if (Strings.CI.equals(value, "protected")) { + return AccessModifier.PROTECTED; + } else { + return AccessModifier.DEFAULT; + } + } + + /** + * Reads a complete BuilderConfiguration from compiler arguments. + * + *

This method reads all configuration options from compiler arguments like: + * + *

+ * + *

All values default to UNSET or DEFAULT if not specified in compiler arguments. The {@code + * hasAnnotationOverride} field is set to DISABLED since compiler arguments don't count as + * annotation overrides. + * + * @return a BuilderConfiguration with values read from compiler arguments + */ + public BuilderConfiguration readBuilderConfiguration() { + return BuilderConfiguration.builder() + .generateSupplier(readOptionState(CompilerArgumentsEnum.GENERATE_FIELD_SUPPLIER)) + .generateProvider(readOptionState(CompilerArgumentsEnum.GENERATE_FIELD_PROVIDER)) + .generateBuilderProvider(readOptionState(CompilerArgumentsEnum.GENERATE_BUILDER_PROVIDER)) + .generateConditionalLogic( + readOptionState(CompilerArgumentsEnum.GENERATE_CONDITIONAL_HELPER)) + .builderAccess(readAccessModifier(CompilerArgumentsEnum.BUILDER_ACCESS)) + .methodAccess(readAccessModifier(CompilerArgumentsEnum.METHOD_ACCESS)) + .generateVarArgsHelpers(readOptionState(CompilerArgumentsEnum.GENERATE_VAR_ARGS_HELPERS)) + .usingArrayListBuilder(readOptionState(CompilerArgumentsEnum.USING_ARRAY_LIST_BUILDER)) + .usingArrayListBuilderWithElementBuilders( + readOptionState(CompilerArgumentsEnum.USING_ARRAY_LIST_BUILDER_WITH_ELEMENT_BUILDERS)) + .usingHashSetBuilder(readOptionState(CompilerArgumentsEnum.USING_HASH_SET_BUILDER)) + .usingHashSetBuilderWithElementBuilders( + readOptionState(CompilerArgumentsEnum.USING_HASH_SET_BUILDER_WITH_ELEMENT_BUILDERS)) + .usingHashMapBuilder(readOptionState(CompilerArgumentsEnum.USING_HASH_MAP_BUILDER)) + .generateWithInterface(readOptionState(CompilerArgumentsEnum.GENERATE_WITH_INTERFACE)) + .hasAnnotationOverride( + OptionState.DISABLED) // Compiler arguments don't count as annotation override + .build(); + } +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ProcessingContext.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ProcessingContext.java index 3766c172..374f8c30 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ProcessingContext.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ProcessingContext.java @@ -43,6 +43,8 @@ public final class ProcessingContext { private final Elements elementUtils; private final Types typeUtils; private final ProcessingLogger logger; + private final BuilderConfiguration globalConfiguration; + private final BuilderConfigurationReader configurationReader; private BuilderConfiguration configuration; /** @@ -51,11 +53,37 @@ public final class ProcessingContext { * @param elementUtils utility for operating on program elements * @param typeUtils utility for operating on types * @param logger the logging utility for the annotation processor + * @param globalConfiguration the global builder configuration read from compiler arguments */ - public ProcessingContext(Elements elementUtils, Types typeUtils, ProcessingLogger logger) { + public ProcessingContext( + Elements elementUtils, + Types typeUtils, + ProcessingLogger logger, + BuilderConfiguration globalConfiguration) { this.elementUtils = elementUtils; this.typeUtils = typeUtils; this.logger = logger; + this.globalConfiguration = globalConfiguration; + this.configurationReader = new BuilderConfigurationReader(); + } + + /** + * Get the global builder configuration read from compiler arguments. This configuration applies + * to all builders unless overridden by annotation. + * + * @return the global builder configuration + */ + public BuilderConfiguration getGlobalConfiguration() { + return globalConfiguration; + } + + /** + * Get the configuration reader for reading configuration from annotations. + * + * @return the builder configuration reader + */ + public BuilderConfigurationReader getConfigurationReader() { + return configurationReader; } /** From 8a5997eb23693df887c95a986a0e03aab2afcef7 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sat, 1 Nov 2025 21:56:59 +0100 Subject: [PATCH 13/63] Adding functionality to read the options from Annotation and implementation of configuration-chain --- .../builders/processor/BuilderProcessor.java | 4 + .../processor/dtos/BuilderConfiguration.java | 73 +++++++ .../util/BuilderConfigurationReader.java | 180 ++++++++++++++++++ .../processor/util/ProcessingContext.java | 34 +--- 4 files changed, 266 insertions(+), 25 deletions(-) create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java 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 1d119428..d746dde2 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 @@ -142,6 +142,10 @@ public SourceVersion getSupportedSourceVersion() { } private void process(Element annotatedElement) throws BuilderException { + // Initialize configuration for this element (merges DEFAULT -> compiler args -> template -> + // options) + context.initConfiguration(annotatedElement); + BuilderDefinitionDto builderDef = extractFromElement(annotatedElement, context); codeGenerator.generateBuilder(builderDef); } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java index 14c0573e..7872757e 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java @@ -142,6 +142,79 @@ public AccessModifier getMethodAccess() { return methodAccess; } + /** + * Merges this configuration with another configuration. + * + *

The other configuration takes priority: if a field in the other configuration is not + * UNSET/DEFAULT, it will override the value from this configuration. If the other configuration + * is null, this configuration is returned unchanged. + * + * @param other the configuration to merge with this one (can be null) + * @return a new BuilderConfiguration with merged values + */ + public BuilderConfiguration merge(BuilderConfiguration other) { + if (other == null) { + return this; + } + + return BuilderConfiguration.builder() + .generateSupplier( + other.generateFieldSupplier != UNSET + ? other.generateFieldSupplier + : this.generateFieldSupplier) + .generateProvider( + other.generateFieldProvider != UNSET + ? other.generateFieldProvider + : this.generateFieldProvider) + .generateBuilderProvider( + other.generateBuilderProvider != UNSET + ? other.generateBuilderProvider + : this.generateBuilderProvider) + .generateConditionalLogic( + other.generateConditionalHelper != UNSET + ? other.generateConditionalHelper + : this.generateConditionalHelper) + .builderAccess( + other.builderAccess != AccessModifier.DEFAULT + ? other.builderAccess + : this.builderAccess) + .methodAccess( + other.methodAccess != AccessModifier.DEFAULT ? other.methodAccess : this.methodAccess) + .generateVarArgsHelpers( + other.generateVarArgsHelpers != UNSET + ? other.generateVarArgsHelpers + : this.generateVarArgsHelpers) + .usingArrayListBuilder( + other.usingArrayListBuilder != UNSET + ? other.usingArrayListBuilder + : this.usingArrayListBuilder) + .usingArrayListBuilderWithElementBuilders( + other.usingArrayListBuilderWithElementBuilders != UNSET + ? other.usingArrayListBuilderWithElementBuilders + : this.usingArrayListBuilderWithElementBuilders) + .usingHashSetBuilder( + other.usingHashSetBuilder != UNSET + ? other.usingHashSetBuilder + : this.usingHashSetBuilder) + .usingHashSetBuilderWithElementBuilders( + other.usingHashSetBuilderWithElementBuilders != UNSET + ? other.usingHashSetBuilderWithElementBuilders + : this.usingHashSetBuilderWithElementBuilders) + .usingHashMapBuilder( + other.usingHashMapBuilder != UNSET + ? other.usingHashMapBuilder + : this.usingHashMapBuilder) + .generateWithInterface( + other.generateWithInterface != UNSET + ? other.generateWithInterface + : this.generateWithInterface) + .hasAnnotationOverride( + other.hasAnnotationOverride != UNSET + ? other.hasAnnotationOverride + : this.hasAnnotationOverride) + .build(); + } + @Override public String toString() { ToStringBuilder builder = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java new file mode 100644 index 00000000..9bab19ed --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java @@ -0,0 +1,180 @@ +/* + * MIT License + * + * Copyright (c) 2025 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.processor.util; + +import static org.javahelpers.simple.builders.core.enums.OptionState.ENABLED; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; +import org.javahelpers.simple.builders.processor.dtos.BuilderConfiguration; + +/** + * Reads builder configuration from annotated elements. + * + *

This class analyzes {@link SimpleBuilder.Options} and {@link SimpleBuilder.Template} + * annotations on an element and extracts the raw configuration values without merging. + * + *

Priority order: + * + *

    + *
  1. {@code @SimpleBuilder.Options} on the element (highest priority) + *
  2. {@code @SimpleBuilder.Template} referenced by the element + *
  3. Global compiler arguments + *
  4. Built-in defaults (lowest priority) + *
+ */ +public class BuilderConfigurationReader { + private final BuilderConfiguration globalConfiguration; + + /** + * Creates a new BuilderConfigurationReader. + * + * @param globalConfiguration the global configuration from compiler arguments + */ + public BuilderConfigurationReader(BuilderConfiguration globalConfiguration) { + this.globalConfiguration = globalConfiguration; + } + + /** + * Reads builder configuration from an annotated element's {@code @SimpleBuilder.Options} + * annotation. + * + *

Returns empty Optional if the element has no {@code @SimpleBuilder.Options} annotation. + * + * @param element the annotated element to analyze + * @return Optional containing the configuration from the annotation, or empty if not present + */ + public BuilderConfiguration readFromOptions(Element element) { + SimpleBuilder.Options options = element.getAnnotation(SimpleBuilder.Options.class); + + if (options == null) { + return null; + } + + // Read raw values from annotation without merging + return BuilderConfiguration.builder() + .generateSupplier(options.generateFieldSupplier()) + .generateProvider(options.generateFieldProvider()) + .generateBuilderProvider(options.generateBuilderProvider()) + .generateConditionalLogic(options.generateConditionalHelper()) + .builderAccess(options.builderAccess()) + .methodAccess(options.methodAccess()) + .generateVarArgsHelpers(options.generateVarArgsHelpers()) + .usingArrayListBuilder(options.usingArrayListBuilder()) + .usingArrayListBuilderWithElementBuilders( + options.usingArrayListBuilderWithElementBuilders()) + .usingHashSetBuilder(options.usingHashSetBuilder()) + .usingHashSetBuilderWithElementBuilders(options.usingHashSetBuilderWithElementBuilders()) + .usingHashMapBuilder(options.usingHashMapBuilder()) + .generateWithInterface(options.generateWithInterface()) + .hasAnnotationOverride(ENABLED) + .build(); + } + + /** + * Reads builder configuration from a template annotation on the element. + * + *

Looks for any custom annotation on the element that is itself annotated with + * {@code @SimpleBuilder.Template}. For example, if the element has {@code @FullFeaturedBuilder}, + * and {@code @FullFeaturedBuilder} is annotated with {@code @SimpleBuilder.Template}, this method + * reads the configuration from that template. + * + *

Returns empty Optional if no template annotation is found. + * + * @param element the annotated element to analyze + * @return Optional containing the configuration from the template annotation, or empty if not + * present + */ + public BuilderConfiguration readFromTemplate(Element element) { + // Check all annotations on the element to find one annotated with @SimpleBuilder.Template + for (AnnotationMirror mirror : element.getAnnotationMirrors()) { + try { + // Get the annotation class + String annotationClassName = mirror.getAnnotationType().toString(); + Class annotationClass = Class.forName(annotationClassName); + + // Check if this annotation is annotated with @SimpleBuilder.Template + SimpleBuilder.Template template = + annotationClass.getAnnotation(SimpleBuilder.Template.class); + + if (template != null) { + // Found a template annotation, read its options + SimpleBuilder.Options options = template.options(); + + return BuilderConfiguration.builder() + .generateSupplier(options.generateFieldSupplier()) + .generateProvider(options.generateFieldProvider()) + .generateBuilderProvider(options.generateBuilderProvider()) + .generateConditionalLogic(options.generateConditionalHelper()) + .builderAccess(options.builderAccess()) + .methodAccess(options.methodAccess()) + .generateVarArgsHelpers(options.generateVarArgsHelpers()) + .usingArrayListBuilder(options.usingArrayListBuilder()) + .usingArrayListBuilderWithElementBuilders( + options.usingArrayListBuilderWithElementBuilders()) + .usingHashSetBuilder(options.usingHashSetBuilder()) + .usingHashSetBuilderWithElementBuilders( + options.usingHashSetBuilderWithElementBuilders()) + .usingHashMapBuilder(options.usingHashMapBuilder()) + .generateWithInterface(options.generateWithInterface()) + .hasAnnotationOverride(ENABLED) + .build(); + } + } catch (ClassNotFoundException e) { + // Annotation class not found, skip it + } + } + + return null; + } + + /** + * Resolves the complete builder configuration for an element by chaining all configuration + * sources in priority order. + * + *

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

    + *
  1. {@code @SimpleBuilder.Options} on the element + *
  2. {@code @SimpleBuilder.Template} on a meta-annotation + *
  3. Global compiler arguments + *
  4. Built-in defaults + *
+ * + * @param element the annotated element to resolve configuration for + * @return the fully resolved configuration with all sources merged + */ + public BuilderConfiguration resolveConfiguration(Element element) { + // Start with DEFAULT as the base + // Layer 2: Merge global configuration from compiler arguments + // Layer 3: Merge template configuration if present + // Layer 4: Merge options configuration if present (highest priority) + return BuilderConfiguration.DEFAULT + .merge(globalConfiguration) + .merge(readFromTemplate(element)) + .merge(readFromOptions(element)); + } +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ProcessingContext.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ProcessingContext.java index 374f8c30..70ce896d 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ProcessingContext.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ProcessingContext.java @@ -43,9 +43,8 @@ public final class ProcessingContext { private final Elements elementUtils; private final Types typeUtils; private final ProcessingLogger logger; - private final BuilderConfiguration globalConfiguration; private final BuilderConfigurationReader configurationReader; - private BuilderConfiguration configuration; + private BuilderConfiguration builderConfigurationForElement; /** * Creates a new processing context. @@ -63,27 +62,12 @@ public ProcessingContext( this.elementUtils = elementUtils; this.typeUtils = typeUtils; this.logger = logger; - this.globalConfiguration = globalConfiguration; - this.configurationReader = new BuilderConfigurationReader(); + this.configurationReader = new BuilderConfigurationReader(globalConfiguration); } - /** - * Get the global builder configuration read from compiler arguments. This configuration applies - * to all builders unless overridden by annotation. - * - * @return the global builder configuration - */ - public BuilderConfiguration getGlobalConfiguration() { - return globalConfiguration; - } - - /** - * Get the configuration reader for reading configuration from annotations. - * - * @return the builder configuration reader - */ - public BuilderConfigurationReader getConfigurationReader() { - return configurationReader; + public void initConfiguration(Element element) { + this.builderConfigurationForElement = configurationReader.resolveConfiguration(element); + logger.debug("Resolved builder configuration for element: {}", builderConfigurationForElement); } /** @@ -91,8 +75,8 @@ public BuilderConfigurationReader getConfigurationReader() { * * @return the builder configuration */ - public BuilderConfiguration getConfiguration() { - return configuration; + public BuilderConfiguration getBuilderConfigurationForElement() { + return builderConfigurationForElement; } /** @@ -100,8 +84,8 @@ public BuilderConfiguration getConfiguration() { * * @param configuration the builder configuration */ - public void setConfiguration(BuilderConfiguration configuration) { - this.configuration = configuration; + public void setBuilderConfigurationForElement(BuilderConfiguration configuration) { + this.builderConfigurationForElement = configuration; } /** From f2f1899673ac8b0dbe8fad215d459a797250b776 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sat, 1 Nov 2025 22:18:06 +0100 Subject: [PATCH 14/63] remove unused hasAnnotationOverride configuration --- .../processor/dtos/BuilderConfiguration.java | 28 ++----------------- .../util/BuilderConfigurationReader.java | 4 --- .../util/CompilerArgumentsReader.java | 6 +--- 3 files changed, 3 insertions(+), 35 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java index 7872757e..27b4327d 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java @@ -52,7 +52,6 @@ * @param usingHashSetBuilderWithElementBuilders Use HashSetBuilderWithElementBuilders * @param usingHashMapBuilder Use HashMapBuilder for maps * @param generateWithInterface Generate With interface - * @param hasAnnotationOverride Whether annotation was used (vs just compiler options) */ public record BuilderConfiguration( OptionState generateFieldSupplier, @@ -67,8 +66,7 @@ public record BuilderConfiguration( OptionState usingHashSetBuilder, OptionState usingHashSetBuilderWithElementBuilders, OptionState usingHashMapBuilder, - OptionState generateWithInterface, - OptionState hasAnnotationOverride) { + OptionState generateWithInterface) { public static final BuilderConfiguration DEFAULT = builder() @@ -85,7 +83,6 @@ public record BuilderConfiguration( .usingHashSetBuilderWithElementBuilders(ENABLED) .usingHashMapBuilder(ENABLED) .generateWithInterface(ENABLED) - .hasAnnotationOverride(ENABLED) .build(); // === Convenience accessors with 'is' prefix for boolean properties === @@ -208,10 +205,6 @@ public BuilderConfiguration merge(BuilderConfiguration other) { other.generateWithInterface != UNSET ? other.generateWithInterface : this.generateWithInterface) - .hasAnnotationOverride( - other.hasAnnotationOverride != UNSET - ? other.hasAnnotationOverride - : this.hasAnnotationOverride) .build(); } @@ -260,9 +253,6 @@ public String toString() { if (generateWithInterface != UNSET) { builder.append("generateWithInterface", generateWithInterface); } - if (hasAnnotationOverride != UNSET) { - builder.append("hasAnnotationOverride", hasAnnotationOverride); - } return builder.toString(); } @@ -293,9 +283,6 @@ public static class Builder { // === Integration === private OptionState generateWithInterface = OptionState.UNSET; - // === Source Information === - private OptionState hasAnnotationOverride = OptionState.UNSET; - // === Setters === public Builder generateSupplier(OptionState value) { this.generateFieldSupplier = value; @@ -427,16 +414,6 @@ public Builder methodAccess(String value) { return this; } - public Builder hasAnnotationOverride(OptionState value) { - this.hasAnnotationOverride = value; - return this; - } - - public Builder hasAnnotationOverride(boolean value) { - this.hasAnnotationOverride = value ? ENABLED : DISABLED; - return this; - } - public BuilderConfiguration build() { return new BuilderConfiguration( generateFieldSupplier, @@ -451,8 +428,7 @@ public BuilderConfiguration build() { usingHashSetBuilder, usingHashSetBuilderWithElementBuilders, usingHashMapBuilder, - generateWithInterface, - hasAnnotationOverride); + generateWithInterface); } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java index 9bab19ed..d75da770 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java @@ -24,8 +24,6 @@ package org.javahelpers.simple.builders.processor.util; -import static org.javahelpers.simple.builders.core.enums.OptionState.ENABLED; - import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; @@ -90,7 +88,6 @@ public BuilderConfiguration readFromOptions(Element element) { .usingHashSetBuilderWithElementBuilders(options.usingHashSetBuilderWithElementBuilders()) .usingHashMapBuilder(options.usingHashMapBuilder()) .generateWithInterface(options.generateWithInterface()) - .hasAnnotationOverride(ENABLED) .build(); } @@ -140,7 +137,6 @@ public BuilderConfiguration readFromTemplate(Element element) { options.usingHashSetBuilderWithElementBuilders()) .usingHashMapBuilder(options.usingHashMapBuilder()) .generateWithInterface(options.generateWithInterface()) - .hasAnnotationOverride(ENABLED) .build(); } } catch (ClassNotFoundException e) { diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java index 5d411811..0003c873 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java @@ -137,9 +137,7 @@ public AccessModifier readAccessModifier(CompilerArgumentsEnum argument) { *
  • etc. * * - *

    All values default to UNSET or DEFAULT if not specified in compiler arguments. The {@code - * hasAnnotationOverride} field is set to DISABLED since compiler arguments don't count as - * annotation overrides. + *

    All values default to UNSET or DEFAULT if not specified in compiler arguments. * * @return a BuilderConfiguration with values read from compiler arguments */ @@ -161,8 +159,6 @@ public BuilderConfiguration readBuilderConfiguration() { readOptionState(CompilerArgumentsEnum.USING_HASH_SET_BUILDER_WITH_ELEMENT_BUILDERS)) .usingHashMapBuilder(readOptionState(CompilerArgumentsEnum.USING_HASH_MAP_BUILDER)) .generateWithInterface(readOptionState(CompilerArgumentsEnum.GENERATE_WITH_INTERFACE)) - .hasAnnotationOverride( - OptionState.DISABLED) // Compiler arguments don't count as annotation override .build(); } } From 4c7a6e6ebe755299c140c87c3f3e7e22f302d9e8 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sat, 1 Nov 2025 22:36:26 +0100 Subject: [PATCH 15/63] Add a test for configuration processing --- .../ConfigurationProcessingTest.java | 216 ++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java new file mode 100644 index 00000000..d884285a --- /dev/null +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java @@ -0,0 +1,216 @@ +package org.javahelpers.simple.builders.processor; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.javahelpers.simple.builders.core.enums.AccessModifier; +import org.javahelpers.simple.builders.core.enums.OptionState; +import org.javahelpers.simple.builders.processor.dtos.BuilderConfiguration; +import org.junit.jupiter.api.Test; + +/** + * Tests ensuring all configuration options are properly handled. + * + *

    These tests are designed to fail at compile-time if: + * + *

      + *
    • A new configuration option is added but not included in the builder + *
    • A new option is added but not handled in merge logic + *
    • A new option is added but not included in toString + *
    + * + *

    This ensures that when extending the configuration, all three places must be updated: + * + *

      + *
    1. BuilderConfiguration record parameters + *
    2. BuilderConfiguration.Builder + *
    3. BuilderConfiguration.merge() method + *
    + */ +class ConfigurationProcessingTest { + + /** + * Completeness test: All configuration options must be settable via builder. + * + *

    If you add a new configuration option and this test doesn't compile, you need to: + * + *

      + *
    1. Add the parameter to BuilderConfiguration record + *
    2. Add builder methods in BuilderConfiguration.Builder + *
    3. Update DEFAULT configuration + *
    4. Update this test to include the new option + *
    + */ + @Test + void allConfigurationOptions_MustBeSettableViaBuilder() { + // This test will fail to compile if any builder method is missing + BuilderConfiguration config = + BuilderConfiguration.builder() + // Field setter generation options + .generateSupplier(OptionState.ENABLED) + .generateProvider(OptionState.ENABLED) + .generateBuilderProvider(OptionState.ENABLED) + // Conditional logic + .generateConditionalLogic(OptionState.ENABLED) + // Access control + .builderAccess(AccessModifier.PACKAGE_PRIVATE) + .methodAccess(AccessModifier.PACKAGE_PRIVATE) + // Collection options + .generateVarArgsHelpers(OptionState.ENABLED) + .usingArrayListBuilder(OptionState.ENABLED) + .usingArrayListBuilderWithElementBuilders(OptionState.ENABLED) + .usingHashSetBuilder(OptionState.ENABLED) + .usingHashSetBuilderWithElementBuilders(OptionState.ENABLED) + .usingHashMapBuilder(OptionState.ENABLED) + // Integration + .generateWithInterface(OptionState.ENABLED) + .build(); + + // Verify all options are accessible (this will fail to compile if accessors are missing) + assertNotNull(config); + assertEquals(OptionState.ENABLED, config.generateFieldSupplier()); + assertEquals(OptionState.ENABLED, config.generateFieldProvider()); + assertEquals(OptionState.ENABLED, config.generateBuilderProvider()); + assertEquals(OptionState.ENABLED, config.generateConditionalHelper()); + assertEquals(AccessModifier.PACKAGE_PRIVATE, config.getBuilderAccess()); + assertEquals(AccessModifier.PACKAGE_PRIVATE, config.getMethodAccess()); + assertEquals(OptionState.ENABLED, config.generateVarArgsHelpers()); + assertEquals(OptionState.ENABLED, config.usingArrayListBuilder()); + assertEquals(OptionState.ENABLED, config.usingArrayListBuilderWithElementBuilders()); + assertEquals(OptionState.ENABLED, config.usingHashSetBuilder()); + assertEquals(OptionState.ENABLED, config.usingHashSetBuilderWithElementBuilders()); + assertEquals(OptionState.ENABLED, config.usingHashMapBuilder()); + assertEquals(OptionState.ENABLED, config.generateWithInterface()); + } + + /** + * Merge logic test: Configuration merging must respect priority correctly. + * + *

    Priority: other > this (for non-UNSET/DEFAULT values) + */ + @Test + void configurationMerge_MustRespectPriority() { + // Given: Base configuration with some values + BuilderConfiguration base = + BuilderConfiguration.builder() + .generateSupplier(OptionState.ENABLED) + .generateProvider(OptionState.ENABLED) + .builderAccess(AccessModifier.PUBLIC) + .build(); + + // When: Merge with override configuration + BuilderConfiguration override = + BuilderConfiguration.builder() + .generateSupplier(OptionState.DISABLED) // Override + .generateBuilderProvider(OptionState.DISABLED) // New value + // generateProvider not set, should keep base value + .build(); + + BuilderConfiguration merged = base.merge(override); + + // Then: Override values win, base values kept for unset + assertEquals( + OptionState.DISABLED, + merged.generateFieldSupplier(), + "Override should win for generateFieldSupplier"); + assertEquals( + OptionState.ENABLED, + merged.generateFieldProvider(), + "Base value should be kept when override is UNSET"); + assertEquals( + OptionState.DISABLED, merged.generateBuilderProvider(), "Override should set new value"); + assertEquals( + AccessModifier.PUBLIC, + merged.getBuilderAccess(), + "Base value should be kept when override is DEFAULT"); + } + + /** + * toString test: Configuration must produce human-readable output. + * + *

    This ensures debugging and logging shows meaningful information. + */ + @Test + void configurationToString_MustBeHumanReadable() { + BuilderConfiguration config = + BuilderConfiguration.builder() + .generateSupplier(OptionState.DISABLED) + .generateProvider(OptionState.ENABLED) + .builderAccess(AccessModifier.PRIVATE) + .methodAccess(AccessModifier.PROTECTED) + .build(); + + String configString = config.toString(); + + // Verify it contains field names and values + assertNotNull(configString, "toString should not return null"); + assertTrue( + configString.contains("generateFieldSupplier"), "toString should mention field names"); + assertTrue(configString.contains("DISABLED"), "toString should show enum values"); + assertTrue(configString.contains("ENABLED"), "toString should show enum values"); + assertTrue(configString.contains("PRIVATE"), "toString should show AccessModifier values"); + assertTrue(configString.contains("PROTECTED"), "toString should show AccessModifier values"); + + // Verify it's not just a hash code + assertTrue(configString.length() > 100, "toString should be detailed, not just class@hashcode"); + } + + /** Null-safe merge test: Merging with null should return this. */ + @Test + void configurationMerge_WithNull_ShouldReturnThis() { + BuilderConfiguration config = + BuilderConfiguration.builder().generateSupplier(OptionState.DISABLED).build(); + + BuilderConfiguration merged = config.merge(null); + + assertEquals(config, merged, "Merging with null should return the same configuration"); + } + + /** + * Chain merge test: Multiple merges should apply in order. + * + *

    This simulates: Defaults -> Compiler Args -> Template -> Options + */ + @Test + void configurationMerge_Chain_ShouldApplyInOrder() { + // Layer 1: Defaults + BuilderConfiguration defaults = BuilderConfiguration.DEFAULT; + + // Layer 2: Compiler arguments (override some defaults) + BuilderConfiguration compilerArgs = + BuilderConfiguration.builder() + .generateSupplier(OptionState.DISABLED) + .builderAccess(AccessModifier.PROTECTED) + .build(); + + // Layer 3: Template (override some compiler args) + BuilderConfiguration template = + BuilderConfiguration.builder().generateSupplier(OptionState.ENABLED).build(); + + // Layer 4: Direct options (highest priority) + BuilderConfiguration options = + BuilderConfiguration.builder().generateProvider(OptionState.DISABLED).build(); + + // Apply chain: defaults -> compiler -> template -> options + BuilderConfiguration finalConfig = defaults.merge(compilerArgs).merge(template).merge(options); + + // Verify final values + assertEquals( + OptionState.ENABLED, + finalConfig.generateFieldSupplier(), + "Template should override compiler args"); + assertEquals( + OptionState.DISABLED, + finalConfig.generateFieldProvider(), + "Options should override all others"); + assertEquals( + AccessModifier.PROTECTED, + finalConfig.getBuilderAccess(), + "Compiler args should override defaults"); + assertEquals( + OptionState.ENABLED, + finalConfig.generateConditionalHelper(), + "Default should remain when not overridden"); + } +} From 8688a7e31c8ebed1589fccaf376a6e21aea89f9f Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sat, 1 Nov 2025 23:05:29 +0100 Subject: [PATCH 16/63] Adding a minimal feature-test, the result should (at the end) only contain basic value-setting functionality --- .../ConfigurationProcessingTest.java | 539 ++++++++++++++++++ 1 file changed, 539 insertions(+) diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java index d884285a..e7f91300 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java @@ -1,12 +1,18 @@ package org.javahelpers.simple.builders.processor; +import static com.google.testing.compile.CompilationSubject.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import com.google.testing.compile.Compilation; +import com.google.testing.compile.Compiler; +import javax.tools.JavaFileObject; import org.javahelpers.simple.builders.core.enums.AccessModifier; import org.javahelpers.simple.builders.core.enums.OptionState; import org.javahelpers.simple.builders.processor.dtos.BuilderConfiguration; +import org.javahelpers.simple.builders.processor.testing.ProcessorAsserts; +import org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils; import org.junit.jupiter.api.Test; /** @@ -84,6 +90,539 @@ void allConfigurationOptions_MustBeSettableViaBuilder() { assertEquals(OptionState.ENABLED, config.generateWithInterface()); } + /** + * Compiler arguments integration test: Verify generated builder with all options disabled. + * + *

    This test documents the current state of generated builder code when all compiler arguments + * are set to false. It ensures that disabling features actually removes them from generated code. + * + *

    If generated code format changes, update the expected text block to reflect current state. + */ + @Test + void compilerArguments_AllDisabled_ShouldGenerateMinimalBuilder() { + // Given: DTO with various property types including nested DTO with builder + JavaFileObject nestedDto = + ProcessorTestUtils.simpleBuilderClass( + "test", + "NestedDto", + """ + private String value; + public String getValue() { return value; } + public void setValue(String value) { this.value = value; } + """); + + JavaFileObject source = + ProcessorTestUtils.simpleBuilderClass( + "test", + "MinimalDto", + """ + private String name; + private java.util.List items; + private java.util.Map properties; + private java.util.Optional description; + private java.util.Set tags; + private NestedDto nested; + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + + public java.util.List getItems() { return items; } + public void setItems(java.util.List items) { this.items = items; } + + public java.util.Map getProperties() { return properties; } + public void setProperties(java.util.Map properties) { this.properties = properties; } + + public java.util.Optional getDescription() { return description; } + public void setDescription(java.util.Optional description) { this.description = description; } + + public java.util.Set getTags() { return tags; } + public void setTags(java.util.Set tags) { this.tags = tags; } + + public NestedDto getNested() { return nested; } + public void setNested(NestedDto nested) { this.nested = nested; } + """); + + // When: Compile with ALL compiler arguments disabled + Compilation compilation = + Compiler.javac() + .withProcessors(new BuilderProcessor()) + .withOptions( + "-Asimplebuilder.generateFieldSupplier=false", + "-Asimplebuilder.generateFieldProvider=false", + "-Asimplebuilder.generateBuilderProvider=false", + "-Asimplebuilder.generateConditionalHelper=false", + "-Asimplebuilder.generateVarArgsHelpers=false", + "-Asimplebuilder.usingArrayListBuilder=false", + "-Asimplebuilder.usingArrayListBuilderWithElementBuilders=false", + "-Asimplebuilder.usingHashSetBuilder=false", + "-Asimplebuilder.usingHashSetBuilderWithElementBuilders=false", + "-Asimplebuilder.usingHashMapBuilder=false", + "-Asimplebuilder.generateWithInterface=false") + .compile(nestedDto, source); + + // Then: Compilation should succeed + assertThat(compilation).succeeded(); + + // And: Generated builder should contain only basic functionality + String generatedCode = ProcessorTestUtils.loadGeneratedSource(compilation, "MinimalDtoBuilder"); + + // Expected: Documents current state when all compiler arguments are disabled + // Note: Even with all options disabled, still generates: + // - supplier methods, StringBuilder consumer, String.format for String fields + // - ArrayListBuilder methods, varargs for List fields + // - conditional() methods and With interface + String expected = + """ + package test; + + import static org.javahelpers.simple.builders.core.util.TrackedValue.changedValue; + import static org.javahelpers.simple.builders.core.util.TrackedValue.initialValue; + import static org.javahelpers.simple.builders.core.util.TrackedValue.unsetValue; + + import java.util.List; + import java.util.Map; + import java.util.Map.Entry; + import java.util.Optional; + import java.util.Set; + import java.util.function.BooleanSupplier; + import java.util.function.Consumer; + import java.util.function.Supplier; + import javax.annotation.processing.Generated; + import org.javahelpers.simple.builders.core.annotations.BuilderImplementation; + import org.javahelpers.simple.builders.core.builders.ArrayListBuilder; + import org.javahelpers.simple.builders.core.builders.HashMapBuilder; + import org.javahelpers.simple.builders.core.builders.HashSetBuilder; + import org.javahelpers.simple.builders.core.interfaces.IBuilderBase; + import org.javahelpers.simple.builders.core.util.TrackedValue; + + /** + * Builder for {@code test.MinimalDto}. + */ + @Generated("Generated by org.javahelpers.simple.builders.processor.BuilderProcessor") + @BuilderImplementation( + forClass = MinimalDto.class + ) + class MinimalDtoBuilder implements IBuilderBase { + /** + * Tracked value for name: name. + */ + private TrackedValue name = unsetValue(); + + /** + * Tracked value for items: items. + */ + private TrackedValue> items = unsetValue(); + + /** + * Tracked value for properties: properties. + */ + private TrackedValue> properties = unsetValue(); + + /** + * Tracked value for description: description. + */ + private TrackedValue> description = unsetValue(); + + /** + * Tracked value for tags: tags. + */ + private TrackedValue> tags = unsetValue(); + + /** + * Tracked value for nested: nested. + */ + private TrackedValue nested = unsetValue(); + + /** + * Initialisation of builder for {@code test.MinimalDto} by a instance. + * + * @param instance object instance for initialisiation + */ + public MinimalDtoBuilder(MinimalDto instance) { + this.name = initialValue(instance.getName()); + this.items = initialValue(instance.getItems()); + this.properties = initialValue(instance.getProperties()); + this.description = initialValue(instance.getDescription()); + this.tags = initialValue(instance.getTags()); + this.nested = initialValue(instance.getNested()); + } + + /** + * Empty constructor of builder for {@code test.MinimalDto}. + */ + public MinimalDtoBuilder() { + } + + /** + * Sets the value for items using a builder consumer that produces the value. + * + * @param itemsBuilderConsumer consumer providing an instance of a builder for items + * @return current instance of builder + */ + public MinimalDtoBuilder items(Consumer> itemsBuilderConsumer) { + ArrayListBuilder builder = this.items.isSet() ? new ArrayListBuilder(this.items.value()) : new ArrayListBuilder(); + itemsBuilderConsumer.accept(builder); + this.items = changedValue(builder.build()); + return this; + } + + /** + * Sets the value for nested by invoking the provided supplier. + * + * @param nestedSupplier supplier for nested + * @return current instance of builder + */ + public MinimalDtoBuilder nested(Supplier nestedSupplier) { + this.nested = changedValue(nestedSupplier.get()); + return this; + } + + /** + * Sets the value for properties by invoking the provided supplier. + * + * @param propertiesSupplier supplier for properties + * @return current instance of builder + */ + public MinimalDtoBuilder properties(Supplier> propertiesSupplier) { + this.properties = changedValue(propertiesSupplier.get()); + return this; + } + + /** + * Sets the value for items. + * + * @param items items + * @return current instance of builder + */ + public MinimalDtoBuilder items(List items) { + this.items = changedValue(items); + return this; + } + + /** + * Sets the value for tags. + * + * @param tags tags + * @return current instance of builder + */ + public MinimalDtoBuilder tags(Set tags) { + this.tags = changedValue(tags); + return this; + } + + /** + * Sets the value for name. + * + * @param format name + * @param args name + * @return current instance of builder + */ + public MinimalDtoBuilder name(String format, Object... args) { + this.name = changedValue(String.format(format, args)); + return this; + } + + /** + * Sets the value for description by invoking the provided supplier. + * + * @param descriptionSupplier supplier for description + * @return current instance of builder + */ + public MinimalDtoBuilder description(Supplier> descriptionSupplier) { + this.description = changedValue(descriptionSupplier.get()); + return this; + } + + /** + * Sets the value for name by executing the provided consumer. + * + * @param nameStringBuilderConsumer consumer providing an instance of name + * @return current instance of builder + */ + public MinimalDtoBuilder name(Consumer nameStringBuilderConsumer) { + StringBuilder builder = new StringBuilder(); + nameStringBuilderConsumer.accept(builder); + this.name = changedValue(builder.toString()); + return this; + } + + /** + * Sets the value for description. + * + * @param description description + * @return current instance of builder + */ + public MinimalDtoBuilder description(Optional description) { + this.description = changedValue(description); + return this; + } + + /** + * Sets the value for properties. + * + * @param properties properties + * @return current instance of builder + */ + public MinimalDtoBuilder properties(Map properties) { + this.properties = changedValue(properties); + return this; + } + + /** + * Sets the value for tags using a builder consumer that produces the value. + * + * @param tagsBuilderConsumer consumer providing an instance of a builder for tags + * @return current instance of builder + */ + public MinimalDtoBuilder tags(Consumer> tagsBuilderConsumer) { + HashSetBuilder builder = this.tags.isSet() ? new HashSetBuilder(this.tags.value()) : new HashSetBuilder(); + tagsBuilderConsumer.accept(builder); + this.tags = changedValue(builder.build()); + return this; + } + + /** + * Sets the value for items by invoking the provided supplier. + * + * @param itemsSupplier supplier for items + * @return current instance of builder + */ + public MinimalDtoBuilder items(Supplier> itemsSupplier) { + this.items = changedValue(itemsSupplier.get()); + return this; + } + + /** + * Sets the value for description. + * + * @param format description + * @param args description + * @return current instance of builder + */ + public MinimalDtoBuilder description(String format, Object... args) { + this.description = changedValue(Optional.of(String.format(format, args))); + return this; + } + + /** + * Sets the value for nested using a builder consumer that produces the value. + * + * @param nestedBuilderConsumer consumer providing an instance of a builder for nested + * @return current instance of builder + */ + public MinimalDtoBuilder nested(Consumer nestedBuilderConsumer) { + NestedDtoBuilder builder = this.nested.isSet() ? new NestedDtoBuilder(this.nested.value()) : new NestedDtoBuilder(); + nestedBuilderConsumer.accept(builder); + this.nested = changedValue(builder.build()); + return this; + } + + /** + * Sets the value for name by invoking the provided supplier. + * + * @param nameSupplier supplier for name + * @return current instance of builder + */ + public MinimalDtoBuilder name(Supplier nameSupplier) { + this.name = changedValue(nameSupplier.get()); + return this; + } + + /** + * Sets the value for description by executing the provided consumer. + * + * @param descriptionStringBuilderConsumer consumer providing an instance of description + * @return current instance of builder + */ + public MinimalDtoBuilder description(Consumer descriptionStringBuilderConsumer) { + StringBuilder builder = new StringBuilder(); + descriptionStringBuilderConsumer.accept(builder); + this.description = changedValue(Optional.of(builder.toString())); + return this; + } + + /** + * Sets the value for items. + * + * @param items items + * @return current instance of builder + */ + public MinimalDtoBuilder items(String... items) { + this.items = changedValue(List.of(items)); + return this; + } + + /** + * Sets the value for nested. + * + * @param nested nested + * @return current instance of builder + */ + public MinimalDtoBuilder nested(NestedDto nested) { + this.nested = changedValue(nested); + return this; + } + + /** + * Sets the value for tags by invoking the provided supplier. + * + * @param tagsSupplier supplier for tags + * @return current instance of builder + */ + public MinimalDtoBuilder tags(Supplier> tagsSupplier) { + this.tags = changedValue(tagsSupplier.get()); + return this; + } + + /** + * Sets the value for properties using a builder consumer that produces the value. + * + * @param propertiesBuilderConsumer consumer providing an instance of a builder for properties + * @return current instance of builder + */ + public MinimalDtoBuilder properties( + Consumer> propertiesBuilderConsumer) { + HashMapBuilder builder = this.properties.isSet() ? new HashMapBuilder(this.properties.value()) : new HashMapBuilder(); + propertiesBuilderConsumer.accept(builder); + this.properties = changedValue(builder.build()); + return this; + } + + /** + * Sets the value for name. + * + * @param name name + * @return current instance of builder + */ + public MinimalDtoBuilder name(String name) { + this.name = changedValue(name); + return this; + } + + /** + * Sets the value for properties. + * + * @param properties properties + * @return current instance of builder + */ + public MinimalDtoBuilder properties(Map.Entry... properties) { + this.properties = changedValue(Map.ofEntries(properties)); + return this; + } + + /** + * Sets the value for tags. + * + * @param tags tags + * @return current instance of builder + */ + public MinimalDtoBuilder tags(String... tags) { + this.tags = changedValue(Set.of(tags)); + return this; + } + + /** + * Sets the value for description. + * + * @param description description + * @return current instance of builder + */ + public MinimalDtoBuilder description(String description) { + this.description = changedValue(Optional.ofNullable(description)); + return this; + } + + @Override + public MinimalDto build() { + MinimalDto result = new MinimalDto(); + this.name.ifSet(result::setName); + this.items.ifSet(result::setItems); + this.properties.ifSet(result::setProperties); + this.description.ifSet(result::setDescription); + this.tags.ifSet(result::setTags); + this.nested.ifSet(result::setNested); + return result; + } + + /** + * Creating a new builder for {@code test.MinimalDto}. + * + * @return builder for {@code test.MinimalDto} + */ + public static MinimalDtoBuilder create() { + return new MinimalDtoBuilder(); + } + + /** + * Conditionally applies builder modifications based on a condition. + * + * @param condition the condition to evaluate + * @param trueCase the consumer to apply if condition is true + * @param falseCase the consumer to apply if condition is false (can be null) + * @return this builder instance + */ + public MinimalDtoBuilder conditional(BooleanSupplier condition, + Consumer trueCase, Consumer falseCase) { + if (condition.getAsBoolean()) { + trueCase.accept(this); + } else if (falseCase != null) { + falseCase.accept(this); + } + return this; + } + + /** + * Conditionally applies builder modifications if the condition is true. + * + * @param condition the condition to evaluate + * @param yesCondition the consumer to apply if condition is true + * @return this builder instance + */ + public MinimalDtoBuilder conditional(BooleanSupplier condition, + Consumer yesCondition) { + return conditional(condition, yesCondition, null); + } + + /** + * Interface that can be implemented by the DTO to provide fluent modification methods. + */ + public interface With { + /** + * Applies modifications to a builder initialized from this instance and returns the built object. + * + * @param b the consumer to apply modifications + * @return the modified instance + */ + default MinimalDto with(Consumer b) { + MinimalDtoBuilder builder; + try { + builder = new MinimalDtoBuilder(MinimalDto.class.cast(this)); + } catch (ClassCastException ex) { + throw new IllegalArgumentException("The interface 'MinimalDtoBuilder.With' should only be implemented by classes, which could be casted to 'MinimalDto'", ex); + } + b.accept(builder); + return builder.build(); + } + + /** + * Creates a builder initialized from this instance. + * + * @return a builder initialized with this instance's values + */ + default MinimalDtoBuilder with() { + try { + return new MinimalDtoBuilder(MinimalDto.class.cast(this)); + } catch (ClassCastException ex) { + throw new IllegalArgumentException("The interface 'MinimalDtoBuilder.With' should only be implemented by classes, which could be casted to 'MinimalDto'", ex); + } + } + } + } + """; + + ProcessorAsserts.assertingResult(generatedCode, ProcessorAsserts.contains(expected)); + } + /** * Merge logic test: Configuration merging must respect priority correctly. * From 2229a4ce140ad82592b6380e2eeb9e19640781ed Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sat, 1 Nov 2025 23:29:30 +0100 Subject: [PATCH 17/63] Implement configuration feature generateFieldSupplier (and refactoring test to check for method signatures) --- .../util/BuilderDefinitionCreator.java | 11 +- .../ConfigurationProcessingTest.java | 489 ++---------------- 2 files changed, 43 insertions(+), 457 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java index 4cd4ac10..d108fd9c 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java @@ -564,7 +564,14 @@ private static boolean tryAddSetConsumer( } private static void addSupplierMethodsForField( - FieldDto field, TypeElement fieldTypeElement, TypeName builderType) { + FieldDto field, + TypeElement fieldTypeElement, + TypeName builderType, + ProcessingContext context) { + // Check if supplier generation is enabled in configuration + if (!context.getBuilderConfigurationForElement().isGenerateSupplier()) { + return; + } // Skip supplier generation for functional interfaces if (isFunctionalInterface(fieldTypeElement)) { return; @@ -771,7 +778,7 @@ private static Optional createFieldDto( // Add consumer/supplier/helper methods - use ORIGINAL field name for method names addConsumerMethodsForField(field, param, fieldTypeElement, builderType, context); - addSupplierMethodsForField(field, fieldTypeElement, builderType); + addSupplierMethodsForField(field, fieldTypeElement, builderType, context); addAdditionalHelperMethodsForField(field, annotations, builderType); return Optional.of(field); diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java index e7f91300..a34cfa83 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java @@ -166,461 +166,40 @@ void compilerArguments_AllDisabled_ShouldGenerateMinimalBuilder() { // And: Generated builder should contain only basic functionality String generatedCode = ProcessorTestUtils.loadGeneratedSource(compilation, "MinimalDtoBuilder"); - // Expected: Documents current state when all compiler arguments are disabled - // Note: Even with all options disabled, still generates: - // - supplier methods, StringBuilder consumer, String.format for String fields - // - ArrayListBuilder methods, varargs for List fields - // - conditional() methods and With interface - String expected = - """ - package test; - - import static org.javahelpers.simple.builders.core.util.TrackedValue.changedValue; - import static org.javahelpers.simple.builders.core.util.TrackedValue.initialValue; - import static org.javahelpers.simple.builders.core.util.TrackedValue.unsetValue; - - import java.util.List; - import java.util.Map; - import java.util.Map.Entry; - import java.util.Optional; - import java.util.Set; - import java.util.function.BooleanSupplier; - import java.util.function.Consumer; - import java.util.function.Supplier; - import javax.annotation.processing.Generated; - import org.javahelpers.simple.builders.core.annotations.BuilderImplementation; - import org.javahelpers.simple.builders.core.builders.ArrayListBuilder; - import org.javahelpers.simple.builders.core.builders.HashMapBuilder; - import org.javahelpers.simple.builders.core.builders.HashSetBuilder; - import org.javahelpers.simple.builders.core.interfaces.IBuilderBase; - import org.javahelpers.simple.builders.core.util.TrackedValue; - - /** - * Builder for {@code test.MinimalDto}. - */ - @Generated("Generated by org.javahelpers.simple.builders.processor.BuilderProcessor") - @BuilderImplementation( - forClass = MinimalDto.class - ) - class MinimalDtoBuilder implements IBuilderBase { - /** - * Tracked value for name: name. - */ - private TrackedValue name = unsetValue(); - - /** - * Tracked value for items: items. - */ - private TrackedValue> items = unsetValue(); - - /** - * Tracked value for properties: properties. - */ - private TrackedValue> properties = unsetValue(); - - /** - * Tracked value for description: description. - */ - private TrackedValue> description = unsetValue(); - - /** - * Tracked value for tags: tags. - */ - private TrackedValue> tags = unsetValue(); - - /** - * Tracked value for nested: nested. - */ - private TrackedValue nested = unsetValue(); - - /** - * Initialisation of builder for {@code test.MinimalDto} by a instance. - * - * @param instance object instance for initialisiation - */ - public MinimalDtoBuilder(MinimalDto instance) { - this.name = initialValue(instance.getName()); - this.items = initialValue(instance.getItems()); - this.properties = initialValue(instance.getProperties()); - this.description = initialValue(instance.getDescription()); - this.tags = initialValue(instance.getTags()); - this.nested = initialValue(instance.getNested()); - } - - /** - * Empty constructor of builder for {@code test.MinimalDto}. - */ - public MinimalDtoBuilder() { - } - - /** - * Sets the value for items using a builder consumer that produces the value. - * - * @param itemsBuilderConsumer consumer providing an instance of a builder for items - * @return current instance of builder - */ - public MinimalDtoBuilder items(Consumer> itemsBuilderConsumer) { - ArrayListBuilder builder = this.items.isSet() ? new ArrayListBuilder(this.items.value()) : new ArrayListBuilder(); - itemsBuilderConsumer.accept(builder); - this.items = changedValue(builder.build()); - return this; - } - - /** - * Sets the value for nested by invoking the provided supplier. - * - * @param nestedSupplier supplier for nested - * @return current instance of builder - */ - public MinimalDtoBuilder nested(Supplier nestedSupplier) { - this.nested = changedValue(nestedSupplier.get()); - return this; - } - - /** - * Sets the value for properties by invoking the provided supplier. - * - * @param propertiesSupplier supplier for properties - * @return current instance of builder - */ - public MinimalDtoBuilder properties(Supplier> propertiesSupplier) { - this.properties = changedValue(propertiesSupplier.get()); - return this; - } - - /** - * Sets the value for items. - * - * @param items items - * @return current instance of builder - */ - public MinimalDtoBuilder items(List items) { - this.items = changedValue(items); - return this; - } - - /** - * Sets the value for tags. - * - * @param tags tags - * @return current instance of builder - */ - public MinimalDtoBuilder tags(Set tags) { - this.tags = changedValue(tags); - return this; - } - - /** - * Sets the value for name. - * - * @param format name - * @param args name - * @return current instance of builder - */ - public MinimalDtoBuilder name(String format, Object... args) { - this.name = changedValue(String.format(format, args)); - return this; - } - - /** - * Sets the value for description by invoking the provided supplier. - * - * @param descriptionSupplier supplier for description - * @return current instance of builder - */ - public MinimalDtoBuilder description(Supplier> descriptionSupplier) { - this.description = changedValue(descriptionSupplier.get()); - return this; - } - - /** - * Sets the value for name by executing the provided consumer. - * - * @param nameStringBuilderConsumer consumer providing an instance of name - * @return current instance of builder - */ - public MinimalDtoBuilder name(Consumer nameStringBuilderConsumer) { - StringBuilder builder = new StringBuilder(); - nameStringBuilderConsumer.accept(builder); - this.name = changedValue(builder.toString()); - return this; - } - - /** - * Sets the value for description. - * - * @param description description - * @return current instance of builder - */ - public MinimalDtoBuilder description(Optional description) { - this.description = changedValue(description); - return this; - } - - /** - * Sets the value for properties. - * - * @param properties properties - * @return current instance of builder - */ - public MinimalDtoBuilder properties(Map properties) { - this.properties = changedValue(properties); - return this; - } - - /** - * Sets the value for tags using a builder consumer that produces the value. - * - * @param tagsBuilderConsumer consumer providing an instance of a builder for tags - * @return current instance of builder - */ - public MinimalDtoBuilder tags(Consumer> tagsBuilderConsumer) { - HashSetBuilder builder = this.tags.isSet() ? new HashSetBuilder(this.tags.value()) : new HashSetBuilder(); - tagsBuilderConsumer.accept(builder); - this.tags = changedValue(builder.build()); - return this; - } - - /** - * Sets the value for items by invoking the provided supplier. - * - * @param itemsSupplier supplier for items - * @return current instance of builder - */ - public MinimalDtoBuilder items(Supplier> itemsSupplier) { - this.items = changedValue(itemsSupplier.get()); - return this; - } - - /** - * Sets the value for description. - * - * @param format description - * @param args description - * @return current instance of builder - */ - public MinimalDtoBuilder description(String format, Object... args) { - this.description = changedValue(Optional.of(String.format(format, args))); - return this; - } - - /** - * Sets the value for nested using a builder consumer that produces the value. - * - * @param nestedBuilderConsumer consumer providing an instance of a builder for nested - * @return current instance of builder - */ - public MinimalDtoBuilder nested(Consumer nestedBuilderConsumer) { - NestedDtoBuilder builder = this.nested.isSet() ? new NestedDtoBuilder(this.nested.value()) : new NestedDtoBuilder(); - nestedBuilderConsumer.accept(builder); - this.nested = changedValue(builder.build()); - return this; - } - - /** - * Sets the value for name by invoking the provided supplier. - * - * @param nameSupplier supplier for name - * @return current instance of builder - */ - public MinimalDtoBuilder name(Supplier nameSupplier) { - this.name = changedValue(nameSupplier.get()); - return this; - } - - /** - * Sets the value for description by executing the provided consumer. - * - * @param descriptionStringBuilderConsumer consumer providing an instance of description - * @return current instance of builder - */ - public MinimalDtoBuilder description(Consumer descriptionStringBuilderConsumer) { - StringBuilder builder = new StringBuilder(); - descriptionStringBuilderConsumer.accept(builder); - this.description = changedValue(Optional.of(builder.toString())); - return this; - } - - /** - * Sets the value for items. - * - * @param items items - * @return current instance of builder - */ - public MinimalDtoBuilder items(String... items) { - this.items = changedValue(List.of(items)); - return this; - } - - /** - * Sets the value for nested. - * - * @param nested nested - * @return current instance of builder - */ - public MinimalDtoBuilder nested(NestedDto nested) { - this.nested = changedValue(nested); - return this; - } - - /** - * Sets the value for tags by invoking the provided supplier. - * - * @param tagsSupplier supplier for tags - * @return current instance of builder - */ - public MinimalDtoBuilder tags(Supplier> tagsSupplier) { - this.tags = changedValue(tagsSupplier.get()); - return this; - } - - /** - * Sets the value for properties using a builder consumer that produces the value. - * - * @param propertiesBuilderConsumer consumer providing an instance of a builder for properties - * @return current instance of builder - */ - public MinimalDtoBuilder properties( - Consumer> propertiesBuilderConsumer) { - HashMapBuilder builder = this.properties.isSet() ? new HashMapBuilder(this.properties.value()) : new HashMapBuilder(); - propertiesBuilderConsumer.accept(builder); - this.properties = changedValue(builder.build()); - return this; - } - - /** - * Sets the value for name. - * - * @param name name - * @return current instance of builder - */ - public MinimalDtoBuilder name(String name) { - this.name = changedValue(name); - return this; - } - - /** - * Sets the value for properties. - * - * @param properties properties - * @return current instance of builder - */ - public MinimalDtoBuilder properties(Map.Entry... properties) { - this.properties = changedValue(Map.ofEntries(properties)); - return this; - } - - /** - * Sets the value for tags. - * - * @param tags tags - * @return current instance of builder - */ - public MinimalDtoBuilder tags(String... tags) { - this.tags = changedValue(Set.of(tags)); - return this; - } - - /** - * Sets the value for description. - * - * @param description description - * @return current instance of builder - */ - public MinimalDtoBuilder description(String description) { - this.description = changedValue(Optional.ofNullable(description)); - return this; - } - - @Override - public MinimalDto build() { - MinimalDto result = new MinimalDto(); - this.name.ifSet(result::setName); - this.items.ifSet(result::setItems); - this.properties.ifSet(result::setProperties); - this.description.ifSet(result::setDescription); - this.tags.ifSet(result::setTags); - this.nested.ifSet(result::setNested); - return result; - } - - /** - * Creating a new builder for {@code test.MinimalDto}. - * - * @return builder for {@code test.MinimalDto} - */ - public static MinimalDtoBuilder create() { - return new MinimalDtoBuilder(); - } - - /** - * Conditionally applies builder modifications based on a condition. - * - * @param condition the condition to evaluate - * @param trueCase the consumer to apply if condition is true - * @param falseCase the consumer to apply if condition is false (can be null) - * @return this builder instance - */ - public MinimalDtoBuilder conditional(BooleanSupplier condition, - Consumer trueCase, Consumer falseCase) { - if (condition.getAsBoolean()) { - trueCase.accept(this); - } else if (falseCase != null) { - falseCase.accept(this); - } - return this; - } - - /** - * Conditionally applies builder modifications if the condition is true. - * - * @param condition the condition to evaluate - * @param yesCondition the consumer to apply if condition is true - * @return this builder instance - */ - public MinimalDtoBuilder conditional(BooleanSupplier condition, - Consumer yesCondition) { - return conditional(condition, yesCondition, null); - } - - /** - * Interface that can be implemented by the DTO to provide fluent modification methods. - */ - public interface With { - /** - * Applies modifications to a builder initialized from this instance and returns the built object. - * - * @param b the consumer to apply modifications - * @return the modified instance - */ - default MinimalDto with(Consumer b) { - MinimalDtoBuilder builder; - try { - builder = new MinimalDtoBuilder(MinimalDto.class.cast(this)); - } catch (ClassCastException ex) { - throw new IllegalArgumentException("The interface 'MinimalDtoBuilder.With' should only be implemented by classes, which could be casted to 'MinimalDto'", ex); - } - b.accept(builder); - return builder.build(); - } - - /** - * Creates a builder initialized from this instance. - * - * @return a builder initialized with this instance's values - */ - default MinimalDtoBuilder with() { - try { - return new MinimalDtoBuilder(MinimalDto.class.cast(this)); - } catch (ClassCastException ex) { - throw new IllegalArgumentException("The interface 'MinimalDtoBuilder.With' should only be implemented by classes, which could be casted to 'MinimalDto'", ex); - } - } - } - } - """; - - ProcessorAsserts.assertingResult(generatedCode, ProcessorAsserts.contains(expected)); + // With generateFieldSupplier=false, NO supplier methods should be generated + ProcessorAsserts.assertNotContaining( + generatedCode, + "public MinimalDtoBuilder name(Supplier nameSupplier)", + "public MinimalDtoBuilder items(Supplier> itemsSupplier)", + "public MinimalDtoBuilder properties(Supplier> propertiesSupplier)", + "public MinimalDtoBuilder description(Supplier> descriptionSupplier)", + "public MinimalDtoBuilder tags(Supplier> tagsSupplier)", + "public MinimalDtoBuilder nested(Supplier nestedSupplier)"); + + // Still generates: basic setters, StringBuilder consumer, String.format for String fields + ProcessorAsserts.assertContaining( + generatedCode, + "public MinimalDtoBuilder name(String name)", + "public MinimalDtoBuilder name(String format, Object... args)", + "public MinimalDtoBuilder name(Consumer nameStringBuilderConsumer)", + "public MinimalDtoBuilder items(List items)", + "public MinimalDtoBuilder items(String... items)", + "public MinimalDtoBuilder properties(Map properties)", + "public MinimalDtoBuilder description(Optional description)", + "public MinimalDtoBuilder description(String description)", + "public MinimalDtoBuilder tags(Set tags)", + "public MinimalDtoBuilder tags(String... tags)", + "public MinimalDtoBuilder nested(NestedDto nested)", + // Builder consumer methods for collections and nested builders + "public MinimalDtoBuilder items(Consumer> itemsBuilderConsumer)", + "public MinimalDtoBuilder tags(Consumer> tagsBuilderConsumer)", + "public MinimalDtoBuilder properties(Consumer> propertiesBuilderConsumer)", + "public MinimalDtoBuilder nested(Consumer nestedBuilderConsumer)", + // Conditional methods + "public MinimalDtoBuilder conditional(BooleanSupplier condition, Consumer trueCase, Consumer falseCase)", + // With interface + "public interface With", + "default MinimalDto with(Consumer b)"); } /** From 72cd758495efa2fb3f0db4b14cb28adb6b095f12 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sat, 1 Nov 2025 23:40:49 +0100 Subject: [PATCH 18/63] Refactoring BuilderConfiguration to use better readable helper functions --- .../processor/dtos/BuilderConfiguration.java | 22 +++++++++---------- .../util/BuilderDefinitionCreator.java | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java index 27b4327d..56a30416 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java @@ -86,47 +86,47 @@ public record BuilderConfiguration( .build(); // === Convenience accessors with 'is' prefix for boolean properties === - public boolean isGenerateSupplier() { + public boolean shouldGenerateFieldSupplier() { return generateFieldSupplier == ENABLED; } - public boolean isGenerateProvider() { + public boolean shouldGenerateFieldProvider() { return generateFieldProvider == ENABLED; } - public boolean isGenerateBuilderProvider() { + public boolean shouldGenerateBuilderProvider() { return generateBuilderProvider == ENABLED; } - public boolean isGenerateConditionalLogic() { + public boolean shouldGenerateConditionalLogic() { return generateConditionalHelper == ENABLED; } - public boolean isGenerateWithInterface() { + public boolean shouldGenerateWithInterface() { return generateWithInterface == ENABLED; } - public boolean isGenerateVarArgsHelpers() { + public boolean shouldGenerateVarArgsHelpers() { return generateVarArgsHelpers == ENABLED; } - public boolean isUsingArrayListBuilder() { + public boolean shouldUseArrayListBuilder() { return usingArrayListBuilder == ENABLED; } - public boolean isUsingArrayListBuilderWithElementBuilders() { + public boolean shouldUseArrayListBuilderWithElementBuilders() { return usingArrayListBuilderWithElementBuilders == ENABLED; } - public boolean isUsingHashSetBuilder() { + public boolean shouldUseHashSetBuilder() { return usingHashSetBuilder == ENABLED; } - public boolean isUsingHashSetBuilderWithElementBuilders() { + public boolean shouldUseHashSetBuilderWithElementBuilders() { return usingHashSetBuilderWithElementBuilders == ENABLED; } - public boolean isUsingHashMapBuilder() { + public boolean shouldUseHashMapBuilder() { return usingHashMapBuilder == ENABLED; } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java index d108fd9c..f7b35c22 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java @@ -569,7 +569,7 @@ private static void addSupplierMethodsForField( TypeName builderType, ProcessingContext context) { // Check if supplier generation is enabled in configuration - if (!context.getBuilderConfigurationForElement().isGenerateSupplier()) { + if (!context.getBuilderConfigurationForElement().shouldGenerateFieldSupplier()) { return; } // Skip supplier generation for functional interfaces From 28e0c33b58fbf873ba7532003e9534bc7c6ece24 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 2 Nov 2025 00:02:10 +0100 Subject: [PATCH 19/63] Implement configuration feature generateFieldConsumer --- .../core/annotations/SimpleBuilder.java | 6 +- .../processor/dtos/BuilderConfiguration.java | 34 +++--- .../enums/CompilerArgumentsEnum.java | 4 +- .../util/BuilderConfigurationReader.java | 4 +- .../util/BuilderDefinitionCreator.java | 6 +- .../util/CompilerArgumentsReader.java | 2 +- .../ConfigurationProcessingTest.java | 106 +++++++++++++----- 7 files changed, 111 insertions(+), 51 deletions(-) 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 354adee8..d161f3c4 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 @@ -82,13 +82,13 @@ OptionState generateFieldSupplier() default OptionState.UNSET; /** - * Generate a provider method with parameter-type {@code Provider} with T being the type of + * Generate a consumer method with parameter-type {@code Consumer} with T being the type of * the field.
    * This is only done for complex field types, so that users could use setter to change the * properties of that parameter. Default: ENABLED Compiler option: - * -Asimplebuilder.generateFieldProvider + * -Asimplebuilder.generateFieldConsumer */ - OptionState generateFieldProvider() default OptionState.UNSET; + OptionState generateFieldConsumer() default OptionState.UNSET; /** * Generate a builder provider method with parameter-type {@code Provider>} with T diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java index 56a30416..276e5639 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java @@ -40,7 +40,7 @@ * reflection. * * @param generateFieldSupplier Generate field supplier methods - * @param generateFieldProvider Generate field provider methods + * @param generateFieldConsumer Generate field consumer methods * @param generateBuilderProvider Generate builder provider methods * @param generateConditionalHelper Generate conditional logic methods * @param builderAccess Access level for builder class @@ -55,7 +55,7 @@ */ public record BuilderConfiguration( OptionState generateFieldSupplier, - OptionState generateFieldProvider, + OptionState generateFieldConsumer, OptionState generateBuilderProvider, OptionState generateConditionalHelper, AccessModifier builderAccess, @@ -71,7 +71,7 @@ public record BuilderConfiguration( public static final BuilderConfiguration DEFAULT = builder() .generateSupplier(ENABLED) - .generateProvider(ENABLED) + .generateConsumer(ENABLED) .generateBuilderProvider(ENABLED) .generateConditionalLogic(ENABLED) .builderAccess(PUBLIC) @@ -90,8 +90,8 @@ public boolean shouldGenerateFieldSupplier() { return generateFieldSupplier == ENABLED; } - public boolean shouldGenerateFieldProvider() { - return generateFieldProvider == ENABLED; + public boolean shouldGenerateFieldConsumer() { + return generateFieldConsumer == ENABLED; } public boolean shouldGenerateBuilderProvider() { @@ -159,10 +159,10 @@ public BuilderConfiguration merge(BuilderConfiguration other) { other.generateFieldSupplier != UNSET ? other.generateFieldSupplier : this.generateFieldSupplier) - .generateProvider( - other.generateFieldProvider != UNSET - ? other.generateFieldProvider - : this.generateFieldProvider) + .generateConsumer( + other.generateFieldConsumer != UNSET + ? other.generateFieldConsumer + : this.generateFieldConsumer) .generateBuilderProvider( other.generateBuilderProvider != UNSET ? other.generateBuilderProvider @@ -215,8 +215,8 @@ public String toString() { if (generateFieldSupplier != UNSET) { builder.append("generateFieldSupplier", generateFieldSupplier); } - if (generateFieldProvider != UNSET) { - builder.append("generateFieldProvider", generateFieldProvider); + if (generateFieldConsumer != UNSET) { + builder.append("generateFieldConsumer", generateFieldConsumer); } if (generateBuilderProvider != UNSET) { builder.append("generateBuilderProvider", generateBuilderProvider); @@ -262,7 +262,7 @@ public String toString() { public static class Builder { // === Field Setter Generation === private OptionState generateFieldSupplier = OptionState.UNSET; - private OptionState generateFieldProvider = OptionState.UNSET; + private OptionState generateFieldConsumer = OptionState.UNSET; private OptionState generateBuilderProvider = OptionState.UNSET; // === Conditional Logic === @@ -294,13 +294,13 @@ public Builder generateSupplier(boolean value) { return this; } - public Builder generateProvider(OptionState value) { - this.generateFieldProvider = value; + public Builder generateConsumer(OptionState value) { + this.generateFieldConsumer = value; return this; } - public Builder generateProvider(boolean value) { - this.generateFieldProvider = value ? ENABLED : DISABLED; + public Builder generateConsumer(boolean value) { + this.generateFieldConsumer = value ? ENABLED : DISABLED; return this; } @@ -417,7 +417,7 @@ public Builder methodAccess(String value) { public BuilderConfiguration build() { return new BuilderConfiguration( generateFieldSupplier, - generateFieldProvider, + generateFieldConsumer, generateBuilderProvider, generateConditionalHelper, builderAccess, diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java index 12292aef..42f581c8 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java @@ -42,8 +42,8 @@ public enum CompilerArgumentsEnum { /** Option for field supplier generation. */ GENERATE_FIELD_SUPPLIER("generateFieldSupplier"), - /** Option for field provider generation. */ - GENERATE_FIELD_PROVIDER("generateFieldProvider"), + /** Option for field consumer generation. */ + GENERATE_FIELD_CONSUMER("generateFieldConsumer"), /** Option for builder provider generation. */ GENERATE_BUILDER_PROVIDER("generateBuilderProvider"), diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java index d75da770..26abe0ea 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java @@ -75,7 +75,7 @@ public BuilderConfiguration readFromOptions(Element element) { // Read raw values from annotation without merging return BuilderConfiguration.builder() .generateSupplier(options.generateFieldSupplier()) - .generateProvider(options.generateFieldProvider()) + .generateConsumer(options.generateFieldConsumer()) .generateBuilderProvider(options.generateBuilderProvider()) .generateConditionalLogic(options.generateConditionalHelper()) .builderAccess(options.builderAccess()) @@ -123,7 +123,7 @@ public BuilderConfiguration readFromTemplate(Element element) { return BuilderConfiguration.builder() .generateSupplier(options.generateFieldSupplier()) - .generateProvider(options.generateFieldProvider()) + .generateConsumer(options.generateFieldConsumer()) .generateBuilderProvider(options.generateBuilderProvider()) .generateConditionalLogic(options.generateConditionalHelper()) .builderAccess(options.builderAccess()) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java index f7b35c22..a7882179 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java @@ -410,12 +410,16 @@ private static boolean tryAddBuilderConsumer( return false; } - /** Tries to add a consumer using an empty constructor of a concrete non-java class. */ + /** Tries to add a field consumer when the field type has an accessible empty constructor. */ private static boolean tryAddFieldConsumer( FieldDto field, TypeElement fieldTypeElement, TypeName builderType, ProcessingContext context) { + // Check if field consumer generation is enabled in configuration + if (!context.getBuilderConfigurationForElement().shouldGenerateFieldConsumer()) { + return false; + } if (!isJavaClass(field.getFieldType()) && fieldTypeElement != null && fieldTypeElement.getKind() == javax.lang.model.element.ElementKind.CLASS diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java index 0003c873..ff0ee269 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java @@ -144,7 +144,7 @@ public AccessModifier readAccessModifier(CompilerArgumentsEnum argument) { public BuilderConfiguration readBuilderConfiguration() { return BuilderConfiguration.builder() .generateSupplier(readOptionState(CompilerArgumentsEnum.GENERATE_FIELD_SUPPLIER)) - .generateProvider(readOptionState(CompilerArgumentsEnum.GENERATE_FIELD_PROVIDER)) + .generateConsumer(readOptionState(CompilerArgumentsEnum.GENERATE_FIELD_CONSUMER)) .generateBuilderProvider(readOptionState(CompilerArgumentsEnum.GENERATE_BUILDER_PROVIDER)) .generateConditionalLogic( readOptionState(CompilerArgumentsEnum.GENERATE_CONDITIONAL_HELPER)) diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java index a34cfa83..94411734 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java @@ -55,7 +55,7 @@ void allConfigurationOptions_MustBeSettableViaBuilder() { BuilderConfiguration.builder() // Field setter generation options .generateSupplier(OptionState.ENABLED) - .generateProvider(OptionState.ENABLED) + .generateConsumer(OptionState.ENABLED) .generateBuilderProvider(OptionState.ENABLED) // Conditional logic .generateConditionalLogic(OptionState.ENABLED) @@ -76,7 +76,7 @@ void allConfigurationOptions_MustBeSettableViaBuilder() { // Verify all options are accessible (this will fail to compile if accessors are missing) assertNotNull(config); assertEquals(OptionState.ENABLED, config.generateFieldSupplier()); - assertEquals(OptionState.ENABLED, config.generateFieldProvider()); + assertEquals(OptionState.ENABLED, config.generateFieldConsumer()); assertEquals(OptionState.ENABLED, config.generateBuilderProvider()); assertEquals(OptionState.ENABLED, config.generateConditionalHelper()); assertEquals(AccessModifier.PACKAGE_PRIVATE, config.getBuilderAccess()); @@ -100,7 +100,8 @@ void allConfigurationOptions_MustBeSettableViaBuilder() { */ @Test void compilerArguments_AllDisabled_ShouldGenerateMinimalBuilder() { - // Given: DTO with various property types including nested DTO with builder + // Given: DTO with various property types including nested DTO with builder and Address without + // builder JavaFileObject nestedDto = ProcessorTestUtils.simpleBuilderClass( "test", @@ -111,6 +112,25 @@ void compilerArguments_AllDisabled_ShouldGenerateMinimalBuilder() { public void setValue(String value) { this.value = value; } """); + JavaFileObject addressDto = + ProcessorTestUtils.forSource( + """ + package test; + + public class Address { + private String street; + private String city; + + public Address() {} + + public String getStreet() { return street; } + public void setStreet(String street) { this.street = street; } + + public String getCity() { return city; } + public void setCity(String city) { this.city = city; } + } + """); + JavaFileObject source = ProcessorTestUtils.simpleBuilderClass( "test", @@ -122,6 +142,7 @@ void compilerArguments_AllDisabled_ShouldGenerateMinimalBuilder() { private java.util.Optional description; private java.util.Set tags; private NestedDto nested; + private Address address; public String getName() { return name; } public void setName(String name) { this.name = name; } @@ -140,6 +161,9 @@ void compilerArguments_AllDisabled_ShouldGenerateMinimalBuilder() { public NestedDto getNested() { return nested; } public void setNested(NestedDto nested) { this.nested = nested; } + + public Address getAddress() { return address; } + public void setAddress(Address address) { this.address = address; } """); // When: Compile with ALL compiler arguments disabled @@ -148,7 +172,7 @@ void compilerArguments_AllDisabled_ShouldGenerateMinimalBuilder() { .withProcessors(new BuilderProcessor()) .withOptions( "-Asimplebuilder.generateFieldSupplier=false", - "-Asimplebuilder.generateFieldProvider=false", + "-Asimplebuilder.generateFieldConsumer=false", "-Asimplebuilder.generateBuilderProvider=false", "-Asimplebuilder.generateConditionalHelper=false", "-Asimplebuilder.generateVarArgsHelpers=false", @@ -158,7 +182,7 @@ void compilerArguments_AllDisabled_ShouldGenerateMinimalBuilder() { "-Asimplebuilder.usingHashSetBuilderWithElementBuilders=false", "-Asimplebuilder.usingHashMapBuilder=false", "-Asimplebuilder.generateWithInterface=false") - .compile(nestedDto, source); + .compile(nestedDto, addressDto, source); // Then: Compilation should succeed assertThat(compilation).succeeded(); @@ -174,32 +198,64 @@ void compilerArguments_AllDisabled_ShouldGenerateMinimalBuilder() { "public MinimalDtoBuilder properties(Supplier> propertiesSupplier)", "public MinimalDtoBuilder description(Supplier> descriptionSupplier)", "public MinimalDtoBuilder tags(Supplier> tagsSupplier)", - "public MinimalDtoBuilder nested(Supplier nestedSupplier)"); + "public MinimalDtoBuilder nested(Supplier nestedSupplier)", + "public MinimalDtoBuilder address(Supplier

    addressSupplier)"); + + // With generateFieldConsumer=false, NO field consumer methods should be generated + // Field consumer = Consumer where T is a custom type with empty constructor + ProcessorAsserts.assertNotContaining( + generatedCode, + "public MinimalDtoBuilder address(Consumer
    addressConsumer)", + "public MinimalDtoBuilder address(Consumer
    addressConsumer)", + "public MinimalDtoBuilder nested(Consumer nestedConsumer)", + "public MinimalDtoBuilder items(Consumer> itemsConsumer)", + "public MinimalDtoBuilder tags(Consumer> tagsConsumer)", + "public MinimalDtoBuilder properties(Consumer> propertiesConsumer)"); + + // With generateBuilderProvider=false, NO builder consumer methods should be generated + // Builder consumers include: StringBuilder, collection builders, nested DTO builders + ProcessorAsserts.assertContaining( + generatedCode, + "public MinimalDtoBuilder nested(Consumer nestedBuilderConsumer)"); + + // With generateConditionalHelper=false, NO conditional methods + ProcessorAsserts.assertContaining( + generatedCode, "public MinimalDtoBuilder conditional(BooleanSupplier condition"); + + // With generateWithInterface=false, NO With interface + ProcessorAsserts.assertContaining(generatedCode, "public interface With"); + + // With usingArrayListBuilder=false, NO ArrayListBuilder should be used + ProcessorAsserts.assertContaining( + generatedCode, + "public MinimalDtoBuilder items(Consumer> itemsBuilderConsumer)"); + + // With usingHashSetBuilder=false, NO HashSetBuilder should be used + ProcessorAsserts.assertContaining( + generatedCode, + "public MinimalDtoBuilder tags(Consumer> tagsBuilderConsumer)"); + + // With usingHashMapBuilder=false, NO HashMapBuilder should be used + ProcessorAsserts.assertContaining( + generatedCode, + "public MinimalDtoBuilder properties(Consumer> propertiesBuilderConsumer)"); - // Still generates: basic setters, StringBuilder consumer, String.format for String fields + // Still generates: basic setters, String.format for String fields, varargs ProcessorAsserts.assertContaining( generatedCode, "public MinimalDtoBuilder name(String name)", "public MinimalDtoBuilder name(String format, Object... args)", + "public MinimalDtoBuilder description(String format, Object... args)", "public MinimalDtoBuilder name(Consumer nameStringBuilderConsumer)", + "public MinimalDtoBuilder description(Consumer descriptionStringBuilderConsumer)", "public MinimalDtoBuilder items(List items)", - "public MinimalDtoBuilder items(String... items)", "public MinimalDtoBuilder properties(Map properties)", + "public MinimalDtoBuilder properties(Map.Entry... properties)", "public MinimalDtoBuilder description(Optional description)", "public MinimalDtoBuilder description(String description)", "public MinimalDtoBuilder tags(Set tags)", - "public MinimalDtoBuilder tags(String... tags)", "public MinimalDtoBuilder nested(NestedDto nested)", - // Builder consumer methods for collections and nested builders - "public MinimalDtoBuilder items(Consumer> itemsBuilderConsumer)", - "public MinimalDtoBuilder tags(Consumer> tagsBuilderConsumer)", - "public MinimalDtoBuilder properties(Consumer> propertiesBuilderConsumer)", - "public MinimalDtoBuilder nested(Consumer nestedBuilderConsumer)", - // Conditional methods - "public MinimalDtoBuilder conditional(BooleanSupplier condition, Consumer trueCase, Consumer falseCase)", - // With interface - "public interface With", - "default MinimalDto with(Consumer b)"); + "public MinimalDtoBuilder address(Address address)"); } /** @@ -213,7 +269,7 @@ void configurationMerge_MustRespectPriority() { BuilderConfiguration base = BuilderConfiguration.builder() .generateSupplier(OptionState.ENABLED) - .generateProvider(OptionState.ENABLED) + .generateConsumer(OptionState.ENABLED) .builderAccess(AccessModifier.PUBLIC) .build(); @@ -222,7 +278,7 @@ void configurationMerge_MustRespectPriority() { BuilderConfiguration.builder() .generateSupplier(OptionState.DISABLED) // Override .generateBuilderProvider(OptionState.DISABLED) // New value - // generateProvider not set, should keep base value + // generateConsumer not set, should keep base value .build(); BuilderConfiguration merged = base.merge(override); @@ -234,7 +290,7 @@ void configurationMerge_MustRespectPriority() { "Override should win for generateFieldSupplier"); assertEquals( OptionState.ENABLED, - merged.generateFieldProvider(), + merged.generateFieldConsumer(), "Base value should be kept when override is UNSET"); assertEquals( OptionState.DISABLED, merged.generateBuilderProvider(), "Override should set new value"); @@ -254,7 +310,7 @@ void configurationToString_MustBeHumanReadable() { BuilderConfiguration config = BuilderConfiguration.builder() .generateSupplier(OptionState.DISABLED) - .generateProvider(OptionState.ENABLED) + .generateConsumer(OptionState.ENABLED) .builderAccess(AccessModifier.PRIVATE) .methodAccess(AccessModifier.PROTECTED) .build(); @@ -308,7 +364,7 @@ void configurationMerge_Chain_ShouldApplyInOrder() { // Layer 4: Direct options (highest priority) BuilderConfiguration options = - BuilderConfiguration.builder().generateProvider(OptionState.DISABLED).build(); + BuilderConfiguration.builder().generateConsumer(OptionState.DISABLED).build(); // Apply chain: defaults -> compiler -> template -> options BuilderConfiguration finalConfig = defaults.merge(compilerArgs).merge(template).merge(options); @@ -320,7 +376,7 @@ void configurationMerge_Chain_ShouldApplyInOrder() { "Template should override compiler args"); assertEquals( OptionState.DISABLED, - finalConfig.generateFieldProvider(), + finalConfig.generateFieldConsumer(), "Options should override all others"); assertEquals( AccessModifier.PROTECTED, From 9c71d93bd20525ed8a4a02633c09545f203d5680 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 2 Nov 2025 00:14:40 +0100 Subject: [PATCH 20/63] Implementing further configurations --- .../util/BuilderDefinitionCreator.java | 57 ++++++++++++++----- .../ConfigurationProcessingTest.java | 25 ++++---- 2 files changed, 56 insertions(+), 26 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java index a7882179..14935bb1 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java @@ -105,9 +105,11 @@ public static BuilderDefinitionDto extractFromElement( extractSetterFields(annotatedType, result, context, fieldNameRegistry); result.addAllFields(setterFields); - // Create the With interface - NestedTypeDto withInterface = createWithInterface(result, context); - result.addNestedType(withInterface); + // Create the With interface if enabled in configuration + if (context.getBuilderConfigurationForElement().shouldGenerateWithInterface()) { + NestedTypeDto withInterface = createWithInterface(result, context); + result.addNestedType(withInterface); + } return result; } @@ -120,6 +122,7 @@ private static BuilderDefinitionDto initializeBuilderDefinition( String simpleClassName = annotatedType.getSimpleName().toString(); result.setBuilderTypeName(new TypeName(packageName, simpleClassName + BUILDER_SUFFIX)); result.setBuildingTargetTypeName(new TypeName(packageName, simpleClassName)); + result.setConfiguration(context.getBuilderConfigurationForElement()); context.debug( "Builder will be generated as: %s.%s", packageName, simpleClassName + BUILDER_SUFFIX); @@ -387,9 +390,9 @@ private static void addConsumerMethodsForField( if (!tryAddBuilderConsumer(field, fieldParameter, builderType, context) && !tryAddFieldConsumer(field, fieldTypeElement, builderType, context) && !tryAddListConsumer(field, fieldParameter, builderType, context) - && !tryAddMapConsumer(field, builderType) + && !tryAddMapConsumer(field, builderType, context) && !tryAddSetConsumer(field, fieldParameter, builderType, context)) { - tryAddStringBuilderConsumer(field, builderType); + tryAddStringBuilderConsumer(field, builderType, context); } } @@ -399,6 +402,10 @@ private static boolean tryAddBuilderConsumer( VariableElement fieldParameter, TypeName builderType, ProcessingContext context) { + // Builder consumers are controlled by generateBuilderProvider + if (!context.getBuilderConfigurationForElement().shouldGenerateBuilderProvider()) { + return false; + } Optional fieldBuilderOpt = resolveBuilderType(fieldParameter, context); if (fieldBuilderOpt.isPresent()) { TypeName fieldBuilderType = fieldBuilderOpt.get(); @@ -435,7 +442,12 @@ && hasEmptyConstructor(fieldTypeElement, context)) { } /** Tries to add StringBuilder-based consumer for String and Optional. */ - private static boolean tryAddStringBuilderConsumer(FieldDto field, TypeName builderType) { + private static boolean tryAddStringBuilderConsumer( + FieldDto field, TypeName builderType, ProcessingContext context) { + // StringBuilder is a builder pattern, controlled by generateBuilderProvider + if (!context.getBuilderConfigurationForElement().shouldGenerateBuilderProvider()) { + return false; + } if (shouldGenerateStringBuilderConsumer(field.getFieldType())) { String transform = isOptionalString(field.getFieldType()) @@ -470,8 +482,11 @@ private static boolean tryAddListConsumer( Optional elementBuilderType = resolveBuilderType(elementType, elementTypeMirror, context); - if (elementBuilderType.isPresent()) { - // Element type has a builder - use ArrayListBuilderWithElementBuilders + if (elementBuilderType.isPresent() + && context + .getBuilderConfigurationForElement() + .shouldUseArrayListBuilderWithElementBuilders()) { + // Element type has a builder - use ArrayListBuilderWithElementBuilders if enabled TypeName collectionBuilderType = new TypeNameGeneric( map2TypeName(ArrayListBuilderWithElementBuilders.class), @@ -484,8 +499,8 @@ private static boolean tryAddListConsumer( collectionBuilderType, elementBuilderType.get(), builderType)); - } else { - // Regular ArrayListBuilder + } else if (context.getBuilderConfigurationForElement().shouldUseArrayListBuilder()) { + // Regular ArrayListBuilder if enabled TypeName collectionBuilderType = map2TypeName(ArrayListBuilder.class); field.addMethod( createFieldConsumerWithBuilder( @@ -494,12 +509,19 @@ private static boolean tryAddListConsumer( collectionBuilderType, elementType, builderType)); + } else { + return false; } return true; } /** Tries to add Map-specific consumer methods. Returns true if handled. */ - private static boolean tryAddMapConsumer(FieldDto field, TypeName builderType) { + private static boolean tryAddMapConsumer( + FieldDto field, TypeName builderType, ProcessingContext context) { + // Check if HashMapBuilder is enabled + if (!context.getBuilderConfigurationForElement().shouldUseHashMapBuilder()) { + return false; + } if (!(isMap(field.getFieldType()) && field.getFieldType() instanceof TypeNameGeneric fieldTypeGeneric && fieldTypeGeneric.getInnerTypeArguments().size() == 2)) { @@ -539,8 +561,11 @@ private static boolean tryAddSetConsumer( Optional elementBuilderType = resolveBuilderType(elementType, elementTypeMirror, context); - if (elementBuilderType.isPresent()) { - // Element type has a builder - use HashSetBuilderWithElementBuilders + if (elementBuilderType.isPresent() + && context + .getBuilderConfigurationForElement() + .shouldUseHashSetBuilderWithElementBuilders()) { + // Element type has a builder - use HashSetBuilderWithElementBuilders if enabled TypeName collectionBuilderType = new TypeNameGeneric( map2TypeName(HashSetBuilderWithElementBuilders.class), @@ -553,8 +578,8 @@ private static boolean tryAddSetConsumer( collectionBuilderType, elementBuilderType.get(), builderType)); - } else { - // Regular HashSetBuilder + } else if (context.getBuilderConfigurationForElement().shouldUseHashSetBuilder()) { + // Regular HashSetBuilder if enabled TypeName collectionBuilderType = map2TypeName(HashSetBuilder.class); field.addMethod( createFieldConsumerWithBuilder( @@ -563,6 +588,8 @@ private static boolean tryAddSetConsumer( collectionBuilderType, elementType, builderType)); + } else { + return false; } return true; } diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java index 94411734..cdd13d37 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java @@ -214,29 +214,34 @@ public Address() {} // With generateBuilderProvider=false, NO builder consumer methods should be generated // Builder consumers include: StringBuilder, collection builders, nested DTO builders - ProcessorAsserts.assertContaining( + ProcessorAsserts.assertNotContaining( generatedCode, - "public MinimalDtoBuilder nested(Consumer nestedBuilderConsumer)"); + "public MinimalDtoBuilder nested(Consumer nestedBuilderConsumer)", + "public MinimalDtoBuilder name(Consumer nameStringBuilderConsumer)", + "public MinimalDtoBuilder description(Consumer descriptionStringBuilderConsumer)"); // With generateConditionalHelper=false, NO conditional methods ProcessorAsserts.assertContaining( generatedCode, "public MinimalDtoBuilder conditional(BooleanSupplier condition"); // With generateWithInterface=false, NO With interface - ProcessorAsserts.assertContaining(generatedCode, "public interface With"); + ProcessorAsserts.assertNotContaining(generatedCode, "public interface With"); - // With usingArrayListBuilder=false, NO ArrayListBuilder should be used - ProcessorAsserts.assertContaining( + // With usingArrayListBuilder=false AND generateBuilderProvider=false, NO ArrayListBuilder + // should be used + ProcessorAsserts.assertNotContaining( generatedCode, "public MinimalDtoBuilder items(Consumer> itemsBuilderConsumer)"); - // With usingHashSetBuilder=false, NO HashSetBuilder should be used - ProcessorAsserts.assertContaining( + // With usingHashSetBuilder=false AND generateBuilderProvider=false, NO HashSetBuilder should be + // used + ProcessorAsserts.assertNotContaining( generatedCode, "public MinimalDtoBuilder tags(Consumer> tagsBuilderConsumer)"); - // With usingHashMapBuilder=false, NO HashMapBuilder should be used - ProcessorAsserts.assertContaining( + // With usingHashMapBuilder=false AND generateBuilderProvider=false, NO HashMapBuilder should be + // used + ProcessorAsserts.assertNotContaining( generatedCode, "public MinimalDtoBuilder properties(Consumer> propertiesBuilderConsumer)"); @@ -246,8 +251,6 @@ public Address() {} "public MinimalDtoBuilder name(String name)", "public MinimalDtoBuilder name(String format, Object... args)", "public MinimalDtoBuilder description(String format, Object... args)", - "public MinimalDtoBuilder name(Consumer nameStringBuilderConsumer)", - "public MinimalDtoBuilder description(Consumer descriptionStringBuilderConsumer)", "public MinimalDtoBuilder items(List items)", "public MinimalDtoBuilder properties(Map properties)", "public MinimalDtoBuilder properties(Map.Entry... properties)", From 17061701d3af4398408c157f066f9b4e2998d32e Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 2 Nov 2025 00:16:03 +0100 Subject: [PATCH 21/63] Implementation of generateConditionalHelper=false --- .../processor/dtos/BuilderDefinitionDto.java | 21 +++++++++++++++++++ .../processor/util/JavaCodeGenerator.java | 8 +++++-- .../ConfigurationProcessingTest.java | 2 +- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderDefinitionDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderDefinitionDto.java index 4b375cc0..e64d53a1 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderDefinitionDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderDefinitionDto.java @@ -57,6 +57,9 @@ public class BuilderDefinitionDto { */ private final List nestedTypes = new LinkedList<>(); + /** Configuration for builder generation. */ + private BuilderConfiguration configuration; + /** * Getting type of builder. * @@ -197,4 +200,22 @@ public List getNestedTypes() { public void addNestedType(NestedTypeDto nestedType) { this.nestedTypes.add(nestedType); } + + /** + * Returns the builder configuration. + * + * @return the builder configuration + */ + public BuilderConfiguration getConfiguration() { + return configuration; + } + + /** + * Sets the builder configuration. + * + * @param configuration the builder configuration + */ + public void setConfiguration(BuilderConfiguration configuration) { + this.configuration = configuration; + } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java index bbbcad9c..9f7461f4 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java @@ -164,8 +164,12 @@ public void generateBuilder(BuilderDefinitionDto builderDef) throws BuilderExcep classBuilder.addMethod( createMethodStaticCreate( builderBaseClass, builderTypeName, dtoBaseClass, builderDef.getGenerics())); - classBuilder.addMethod(createMethodConditional(builderTypeName)); - classBuilder.addMethod(createMethodConditionalPositiveOnly(builderTypeName)); + + // Add conditional methods only if enabled in configuration + if (builderDef.getConfiguration().shouldGenerateConditionalLogic()) { + classBuilder.addMethod(createMethodConditional(builderTypeName)); + classBuilder.addMethod(createMethodConditionalPositiveOnly(builderTypeName)); + } // Adding nested types (e.g., With interface) for (NestedTypeDto nestedType : builderDef.getNestedTypes()) { diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java index cdd13d37..81886889 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java @@ -221,7 +221,7 @@ public Address() {} "public MinimalDtoBuilder description(Consumer descriptionStringBuilderConsumer)"); // With generateConditionalHelper=false, NO conditional methods - ProcessorAsserts.assertContaining( + ProcessorAsserts.assertNotContaining( generatedCode, "public MinimalDtoBuilder conditional(BooleanSupplier condition"); // With generateWithInterface=false, NO With interface From 997a3cfe7ee553240b4b81041aeb0479e828cd88 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 2 Nov 2025 00:19:50 +0100 Subject: [PATCH 22/63] Implementation of generateVarArgsHelpers=false --- .../util/BuilderDefinitionCreator.java | 78 +++++++++++-------- .../ConfigurationProcessingTest.java | 8 +- 2 files changed, 52 insertions(+), 34 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java index 14935bb1..a2a2f38e 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java @@ -270,7 +270,10 @@ private static boolean isMethodRelevantForBuilder( } private static void addAdditionalHelperMethodsForField( - FieldDto field, List annotations, TypeName builderType) { + FieldDto field, + List annotations, + TypeName builderType, + ProcessingContext context) { String fieldNameInBuilder = field.getFieldName(); String fieldJavaDoc = field.getJavaDoc(); // Check for String type (not array) and add format method @@ -312,39 +315,48 @@ private static void addAdditionalHelperMethodsForField( List innerTypes = fieldTypeGeneric.getInnerTypeArguments(); int innerTypesCnt = innerTypes.size(); if (isList(field.getFieldType()) && innerTypesCnt == 1) { - String fieldName = field.getFieldNameEstimated(); - field.addMethod( - createFieldSetterWithTransform( - fieldName, - fieldNameInBuilder, - fieldJavaDoc, - "List.of(%s)", - new TypeNameArray(innerTypes.get(0), false), - builderType)); + // Only add varargs helper if enabled in configuration + if (context.getBuilderConfigurationForElement().shouldGenerateVarArgsHelpers()) { + String fieldName = field.getFieldNameEstimated(); + field.addMethod( + createFieldSetterWithTransform( + fieldName, + fieldNameInBuilder, + fieldJavaDoc, + "List.of(%s)", + new TypeNameArray(innerTypes.get(0), false), + builderType)); + } } else if (isSet(field.getFieldType()) && innerTypesCnt == 1) { - String fieldName = field.getFieldNameEstimated(); - field.addMethod( - createFieldSetterWithTransform( - fieldName, - fieldNameInBuilder, - fieldJavaDoc, - "Set.of(%s)", - new TypeNameArray(innerTypes.get(0), true), - builderType)); + // Only add varargs helper if enabled in configuration + if (context.getBuilderConfigurationForElement().shouldGenerateVarArgsHelpers()) { + String fieldName = field.getFieldNameEstimated(); + field.addMethod( + createFieldSetterWithTransform( + fieldName, + fieldNameInBuilder, + fieldJavaDoc, + "Set.of(%s)", + new TypeNameArray(innerTypes.get(0), true), + builderType)); + } } else if (isMap(field.getFieldType()) && innerTypesCnt == 2) { - TypeName mapEntryType = - new TypeNameArray( - new TypeNameGeneric("java.util", "Map.Entry", innerTypes.get(0), innerTypes.get(1)), - false); - String fieldName = field.getFieldNameEstimated(); - field.addMethod( - createFieldSetterWithTransform( - fieldName, - fieldNameInBuilder, - fieldJavaDoc, - "Map.ofEntries(%s)", - mapEntryType, - builderType)); + // Only add varargs helper if enabled in configuration + if (context.getBuilderConfigurationForElement().shouldGenerateVarArgsHelpers()) { + TypeName mapEntryType = + new TypeNameArray( + new TypeNameGeneric("java.util", "Map.Entry", innerTypes.get(0), innerTypes.get(1)), + false); + String fieldName = field.getFieldNameEstimated(); + field.addMethod( + createFieldSetterWithTransform( + fieldName, + fieldNameInBuilder, + fieldJavaDoc, + "Map.ofEntries(%s)", + mapEntryType, + builderType)); + } } else if (isOptional(field.getFieldType()) && innerTypesCnt == 1) { // Add setter that accepts the inner type T and wraps it in Optional.ofNullable() String fieldName = field.getFieldNameEstimated(); @@ -810,7 +822,7 @@ private static Optional createFieldDto( // Add consumer/supplier/helper methods - use ORIGINAL field name for method names addConsumerMethodsForField(field, param, fieldTypeElement, builderType, context); addSupplierMethodsForField(field, fieldTypeElement, builderType, context); - addAdditionalHelperMethodsForField(field, annotations, builderType); + addAdditionalHelperMethodsForField(field, annotations, builderType, context); return Optional.of(field); } diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java index 81886889..a79c0e6f 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java @@ -224,6 +224,13 @@ public Address() {} ProcessorAsserts.assertNotContaining( generatedCode, "public MinimalDtoBuilder conditional(BooleanSupplier condition"); + // With generateVarArgsHelpers=false, NO VarArgs helpers should be generated + ProcessorAsserts.assertNotContaining( + generatedCode, + "public MinimalDtoBuilder items(String... items)", + "public MinimalDtoBuilder properties(Map.Entry... properties)", + "public MinimalDtoBuilder tags(String... tags)"); + // With generateWithInterface=false, NO With interface ProcessorAsserts.assertNotContaining(generatedCode, "public interface With"); @@ -253,7 +260,6 @@ public Address() {} "public MinimalDtoBuilder description(String format, Object... args)", "public MinimalDtoBuilder items(List items)", "public MinimalDtoBuilder properties(Map properties)", - "public MinimalDtoBuilder properties(Map.Entry... properties)", "public MinimalDtoBuilder description(Optional description)", "public MinimalDtoBuilder description(String description)", "public MinimalDtoBuilder tags(Set tags)", From fb9f199ba6c4a215da492688dafa546959984162 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 2 Nov 2025 00:29:23 +0100 Subject: [PATCH 23/63] Adding further asserts on generated functions --- .../builders/processor/ConfigurationProcessingTest.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java index a79c0e6f..45303de0 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java @@ -255,6 +255,10 @@ public Address() {} // Still generates: basic setters, String.format for String fields, varargs ProcessorAsserts.assertContaining( generatedCode, + "@Generated(\"Generated by org.javahelpers.simple.builders.processor.BuilderProcessor\")", + "@BuilderImplementation(forClass = MinimalDto.class)", + "class MinimalDtoBuilder implements IBuilderBase", + "public MinimalDtoBuilder(MinimalDto instance)", "public MinimalDtoBuilder name(String name)", "public MinimalDtoBuilder name(String format, Object... args)", "public MinimalDtoBuilder description(String format, Object... args)", @@ -264,7 +268,9 @@ public Address() {} "public MinimalDtoBuilder description(String description)", "public MinimalDtoBuilder tags(Set tags)", "public MinimalDtoBuilder nested(NestedDto nested)", - "public MinimalDtoBuilder address(Address address)"); + "public MinimalDtoBuilder address(Address address)", + "public MinimalDto build()", + "public static MinimalDtoBuilder create()"); } /** From 3097fc20173afc4d2a15d1cbaef2e842f45de865 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 2 Nov 2025 00:36:21 +0100 Subject: [PATCH 24/63] Adding a new configuration: generateUnboxedOptional --- .../core/annotations/SimpleBuilder.java | 9 ++++++++ .../processor/dtos/BuilderConfiguration.java | 23 +++++++++++++++++++ .../enums/CompilerArgumentsEnum.java | 3 +++ .../util/BuilderConfigurationReader.java | 2 ++ .../util/BuilderDefinitionCreator.java | 22 ++++++++++-------- .../util/CompilerArgumentsReader.java | 1 + .../ConfigurationProcessingTest.java | 6 ++++- 7 files changed, 56 insertions(+), 10 deletions(-) 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 d161f3c4..18f25a5c 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 @@ -133,6 +133,15 @@ */ OptionState generateVarArgsHelpers() default OptionState.UNSET; + /** + * Generate unboxed optional methods that accept the inner type T directly instead of + * Optional<T>.
    + * For Optional fields, this generates a setter that accepts T and wraps it with + * Optional.ofNullable().
    + * Default: ENABLED Compiler option: -Asimplebuilder.generateUnboxedOptional + */ + OptionState generateUnboxedOptional() default OptionState.UNSET; + /** * Generate helper methods with a ArrayListBuilder supplier for lists instead of simple * supplier, which would not allow to use in a chanined way:
    diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java index 276e5639..29e0fa67 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java @@ -46,6 +46,7 @@ * @param builderAccess Access level for builder class * @param methodAccess Access level for builder methods * @param generateVarArgsHelpers Generate varargs helper methods + * @param generateUnboxedOptional Generate unboxed optional methods * @param usingArrayListBuilder Use ArrayListBuilder for lists * @param usingArrayListBuilderWithElementBuilders Use ArrayListBuilderWithElementBuilders * @param usingHashSetBuilder Use HashSetBuilder for sets @@ -61,6 +62,7 @@ public record BuilderConfiguration( AccessModifier builderAccess, AccessModifier methodAccess, OptionState generateVarArgsHelpers, + OptionState generateUnboxedOptional, OptionState usingArrayListBuilder, OptionState usingArrayListBuilderWithElementBuilders, OptionState usingHashSetBuilder, @@ -77,6 +79,7 @@ public record BuilderConfiguration( .builderAccess(PUBLIC) .methodAccess(PUBLIC) .generateVarArgsHelpers(ENABLED) + .generateUnboxedOptional(ENABLED) .usingArrayListBuilder(ENABLED) .usingArrayListBuilderWithElementBuilders(ENABLED) .usingHashSetBuilder(ENABLED) @@ -130,6 +133,10 @@ public boolean shouldUseHashMapBuilder() { return usingHashMapBuilder == ENABLED; } + public boolean shouldGenerateUnboxedOptional() { + return generateUnboxedOptional == ENABLED; + } + // === String accessors === public AccessModifier getBuilderAccess() { return builderAccess; @@ -181,6 +188,10 @@ public BuilderConfiguration merge(BuilderConfiguration other) { other.generateVarArgsHelpers != UNSET ? other.generateVarArgsHelpers : this.generateVarArgsHelpers) + .generateUnboxedOptional( + other.generateUnboxedOptional != UNSET + ? other.generateUnboxedOptional + : this.generateUnboxedOptional) .usingArrayListBuilder( other.usingArrayListBuilder != UNSET ? other.usingArrayListBuilder @@ -274,6 +285,7 @@ public static class Builder { // === Collection Options === private OptionState generateVarArgsHelpers = OptionState.UNSET; + private OptionState generateUnboxedOptional = OptionState.UNSET; private OptionState usingArrayListBuilder = OptionState.UNSET; private OptionState usingArrayListBuilderWithElementBuilders = OptionState.UNSET; private OptionState usingHashSetBuilder = OptionState.UNSET; @@ -344,6 +356,16 @@ public Builder generateVarArgsHelpers(boolean value) { return this; } + public Builder generateUnboxedOptional(OptionState value) { + this.generateUnboxedOptional = value; + return this; + } + + public Builder generateUnboxedOptional(boolean value) { + this.generateUnboxedOptional = value ? ENABLED : DISABLED; + return this; + } + public Builder usingArrayListBuilder(OptionState value) { this.usingArrayListBuilder = value; return this; @@ -423,6 +445,7 @@ public BuilderConfiguration build() { builderAccess, methodAccess, generateVarArgsHelpers, + generateUnboxedOptional, usingArrayListBuilder, usingArrayListBuilderWithElementBuilders, usingHashSetBuilder, diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java index 42f581c8..e310f117 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java @@ -63,6 +63,9 @@ public enum CompilerArgumentsEnum { /** Option for varargs helper generation. */ GENERATE_VAR_ARGS_HELPERS("generateVarArgsHelpers"), + /** Option for unboxed optional generation. */ + GENERATE_UNBOXED_OPTIONAL("generateUnboxedOptional"), + /** Option for ArrayList builder usage. */ USING_ARRAY_LIST_BUILDER("usingArrayListBuilder"), diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java index 26abe0ea..64741d61 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java @@ -81,6 +81,7 @@ public BuilderConfiguration readFromOptions(Element element) { .builderAccess(options.builderAccess()) .methodAccess(options.methodAccess()) .generateVarArgsHelpers(options.generateVarArgsHelpers()) + .generateUnboxedOptional(options.generateUnboxedOptional()) .usingArrayListBuilder(options.usingArrayListBuilder()) .usingArrayListBuilderWithElementBuilders( options.usingArrayListBuilderWithElementBuilders()) @@ -129,6 +130,7 @@ public BuilderConfiguration readFromTemplate(Element element) { .builderAccess(options.builderAccess()) .methodAccess(options.methodAccess()) .generateVarArgsHelpers(options.generateVarArgsHelpers()) + .generateUnboxedOptional(options.generateUnboxedOptional()) .usingArrayListBuilder(options.usingArrayListBuilder()) .usingArrayListBuilderWithElementBuilders( options.usingArrayListBuilderWithElementBuilders()) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java index a2a2f38e..e713473d 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java @@ -358,16 +358,20 @@ private static void addAdditionalHelperMethodsForField( builderType)); } } else if (isOptional(field.getFieldType()) && innerTypesCnt == 1) { - // Add setter that accepts the inner type T and wraps it in Optional.ofNullable() String fieldName = field.getFieldNameEstimated(); - field.addMethod( - createFieldSetterWithTransform( - fieldName, - fieldNameInBuilder, - fieldJavaDoc, - "Optional.ofNullable(%s)", - innerTypes.get(0), - builderType)); + + // Only generate unboxed optional method if enabled in configuration + if (context.getBuilderConfigurationForElement().shouldGenerateUnboxedOptional()) { + // Add setter that accepts the inner type T and wraps it in Optional.ofNullable() + field.addMethod( + createFieldSetterWithTransform( + fieldName, + fieldNameInBuilder, + fieldJavaDoc, + "Optional.ofNullable(%s)", + innerTypes.get(0), + builderType)); + } // If Optional, add format method TypeName innerType = innerTypes.get(0); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java index ff0ee269..9c5e0d6d 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java @@ -151,6 +151,7 @@ public BuilderConfiguration readBuilderConfiguration() { .builderAccess(readAccessModifier(CompilerArgumentsEnum.BUILDER_ACCESS)) .methodAccess(readAccessModifier(CompilerArgumentsEnum.METHOD_ACCESS)) .generateVarArgsHelpers(readOptionState(CompilerArgumentsEnum.GENERATE_VAR_ARGS_HELPERS)) + .generateUnboxedOptional(readOptionState(CompilerArgumentsEnum.GENERATE_UNBOXED_OPTIONAL)) .usingArrayListBuilder(readOptionState(CompilerArgumentsEnum.USING_ARRAY_LIST_BUILDER)) .usingArrayListBuilderWithElementBuilders( readOptionState(CompilerArgumentsEnum.USING_ARRAY_LIST_BUILDER_WITH_ELEMENT_BUILDERS)) diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java index 45303de0..eac7aa48 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java @@ -176,6 +176,7 @@ public Address() {} "-Asimplebuilder.generateBuilderProvider=false", "-Asimplebuilder.generateConditionalHelper=false", "-Asimplebuilder.generateVarArgsHelpers=false", + "-Asimplebuilder.generateUnboxedOptional=false", "-Asimplebuilder.usingArrayListBuilder=false", "-Asimplebuilder.usingArrayListBuilderWithElementBuilders=false", "-Asimplebuilder.usingHashSetBuilder=false", @@ -234,6 +235,10 @@ public Address() {} // With generateWithInterface=false, NO With interface ProcessorAsserts.assertNotContaining(generatedCode, "public interface With"); + // With generateUnboxedOptional=false, NO unboxed optional methods should be generated + ProcessorAsserts.assertNotContaining( + generatedCode, "public MinimalDtoBuilder description(String description)"); + // With usingArrayListBuilder=false AND generateBuilderProvider=false, NO ArrayListBuilder // should be used ProcessorAsserts.assertNotContaining( @@ -265,7 +270,6 @@ public Address() {} "public MinimalDtoBuilder items(List items)", "public MinimalDtoBuilder properties(Map properties)", "public MinimalDtoBuilder description(Optional description)", - "public MinimalDtoBuilder description(String description)", "public MinimalDtoBuilder tags(Set tags)", "public MinimalDtoBuilder nested(NestedDto nested)", "public MinimalDtoBuilder address(Address address)", From ccb070b4aa0993f913c54f8b6a5feac3678a2754 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 2 Nov 2025 00:41:38 +0100 Subject: [PATCH 25/63] Adding a new configuration: usingGeneratedAnnotation --- .../core/annotations/SimpleBuilder.java | 8 ++++++ .../processor/dtos/BuilderConfiguration.java | 25 +++++++++++++++++++ .../enums/CompilerArgumentsEnum.java | 4 +++ .../util/BuilderConfigurationReader.java | 2 ++ .../util/CompilerArgumentsReader.java | 1 + .../processor/util/JavaCodeGenerator.java | 4 ++- .../ConfigurationProcessingTest.java | 7 ++++-- 7 files changed, 48 insertions(+), 3 deletions(-) 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 18f25a5c..64776274 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 @@ -288,6 +288,14 @@ */ OptionState usingHashMapBuilder() default OptionState.UNSET; + // === Annotations === + /** + * Use {@code @Generated} annotation on the generated builder class.
    + * Default: ENABLED Compiler option: -Asimplebuilder.usingGeneratedAnnotation + */ + OptionState usingGeneratedAnnotation() default OptionState.UNSET; + + // === Integration === /** * Generate With interface for integrating builder into DTOs.
    * Default: ENABLED Compiler option: -Asimplebuilder.generateWithInterface diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java index 29e0fa67..97f507cb 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java @@ -52,6 +52,7 @@ * @param usingHashSetBuilder Use HashSetBuilder for sets * @param usingHashSetBuilderWithElementBuilders Use HashSetBuilderWithElementBuilders * @param usingHashMapBuilder Use HashMapBuilder for maps + * @param usingGeneratedAnnotation Use Generated annotation * @param generateWithInterface Generate With interface */ public record BuilderConfiguration( @@ -68,6 +69,7 @@ public record BuilderConfiguration( OptionState usingHashSetBuilder, OptionState usingHashSetBuilderWithElementBuilders, OptionState usingHashMapBuilder, + OptionState usingGeneratedAnnotation, OptionState generateWithInterface) { public static final BuilderConfiguration DEFAULT = @@ -85,6 +87,7 @@ public record BuilderConfiguration( .usingHashSetBuilder(ENABLED) .usingHashSetBuilderWithElementBuilders(ENABLED) .usingHashMapBuilder(ENABLED) + .usingGeneratedAnnotation(ENABLED) .generateWithInterface(ENABLED) .build(); @@ -137,6 +140,10 @@ public boolean shouldGenerateUnboxedOptional() { return generateUnboxedOptional == ENABLED; } + public boolean shouldUseGeneratedAnnotation() { + return usingGeneratedAnnotation == ENABLED; + } + // === String accessors === public AccessModifier getBuilderAccess() { return builderAccess; @@ -212,6 +219,10 @@ public BuilderConfiguration merge(BuilderConfiguration other) { other.usingHashMapBuilder != UNSET ? other.usingHashMapBuilder : this.usingHashMapBuilder) + .usingGeneratedAnnotation( + other.usingGeneratedAnnotation != UNSET + ? other.usingGeneratedAnnotation + : this.usingGeneratedAnnotation) .generateWithInterface( other.generateWithInterface != UNSET ? other.generateWithInterface @@ -292,6 +303,9 @@ public static class Builder { private OptionState usingHashSetBuilderWithElementBuilders = OptionState.UNSET; private OptionState usingHashMapBuilder = OptionState.UNSET; + // === Annotations === + private OptionState usingGeneratedAnnotation = OptionState.UNSET; + // === Integration === private OptionState generateWithInterface = OptionState.UNSET; @@ -416,6 +430,16 @@ public Builder usingHashMapBuilder(boolean value) { return this; } + public Builder usingGeneratedAnnotation(OptionState value) { + this.usingGeneratedAnnotation = value; + return this; + } + + public Builder usingGeneratedAnnotation(boolean value) { + this.usingGeneratedAnnotation = value ? ENABLED : DISABLED; + return this; + } + public Builder builderAccess(AccessModifier value) { this.builderAccess = value; return this; @@ -451,6 +475,7 @@ public BuilderConfiguration build() { usingHashSetBuilder, usingHashSetBuilderWithElementBuilders, usingHashMapBuilder, + usingGeneratedAnnotation, generateWithInterface); } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java index e310f117..35065da2 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java @@ -81,6 +81,10 @@ public enum CompilerArgumentsEnum { /** Option for HashMap builder usage. */ USING_HASH_MAP_BUILDER("usingHashMapBuilder"), + // === Annotations === + /** Option for using Generated annotation. */ + USING_GENERATED_ANNOTATION("usingGeneratedAnnotation"), + // === Integration === /** Option for With interface generation. */ GENERATE_WITH_INTERFACE("generateWithInterface"), diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java index 64741d61..19166d4b 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java @@ -88,6 +88,7 @@ public BuilderConfiguration readFromOptions(Element element) { .usingHashSetBuilder(options.usingHashSetBuilder()) .usingHashSetBuilderWithElementBuilders(options.usingHashSetBuilderWithElementBuilders()) .usingHashMapBuilder(options.usingHashMapBuilder()) + .usingGeneratedAnnotation(options.usingGeneratedAnnotation()) .generateWithInterface(options.generateWithInterface()) .build(); } @@ -138,6 +139,7 @@ public BuilderConfiguration readFromTemplate(Element element) { .usingHashSetBuilderWithElementBuilders( options.usingHashSetBuilderWithElementBuilders()) .usingHashMapBuilder(options.usingHashMapBuilder()) + .usingGeneratedAnnotation(options.usingGeneratedAnnotation()) .generateWithInterface(options.generateWithInterface()) .build(); } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java index 9c5e0d6d..c01dfbbc 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java @@ -159,6 +159,7 @@ public BuilderConfiguration readBuilderConfiguration() { .usingHashSetBuilderWithElementBuilders( readOptionState(CompilerArgumentsEnum.USING_HASH_SET_BUILDER_WITH_ELEMENT_BUILDERS)) .usingHashMapBuilder(readOptionState(CompilerArgumentsEnum.USING_HASH_MAP_BUILDER)) + .usingGeneratedAnnotation(readOptionState(CompilerArgumentsEnum.USING_GENERATED_ANNOTATION)) .generateWithInterface(readOptionState(CompilerArgumentsEnum.GENERATE_WITH_INTERFACE)) .build(); } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java index 9f7461f4..02c6b507 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java @@ -179,7 +179,9 @@ public void generateBuilder(BuilderDefinitionDto builderDef) throws BuilderExcep } // Adding annotations - classBuilder.addAnnotation(createAnnotationGenerated()); + if (builderDef.getConfiguration().shouldUseGeneratedAnnotation()) { + classBuilder.addAnnotation(createAnnotationGenerated()); + } classBuilder.addAnnotation(createAnnotationBuilderImplementation(dtoBaseClass)); logger.debug( diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java index eac7aa48..4a521f10 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java @@ -182,6 +182,7 @@ public Address() {} "-Asimplebuilder.usingHashSetBuilder=false", "-Asimplebuilder.usingHashSetBuilderWithElementBuilders=false", "-Asimplebuilder.usingHashMapBuilder=false", + "-Asimplebuilder.usingGeneratedAnnotation=false", "-Asimplebuilder.generateWithInterface=false") .compile(nestedDto, addressDto, source); @@ -239,6 +240,9 @@ public Address() {} ProcessorAsserts.assertNotContaining( generatedCode, "public MinimalDtoBuilder description(String description)"); + // With usingGeneratedAnnotation=false, NO @Generated annotation should be used + ProcessorAsserts.assertNotContaining(generatedCode, "@Generated("); + // With usingArrayListBuilder=false AND generateBuilderProvider=false, NO ArrayListBuilder // should be used ProcessorAsserts.assertNotContaining( @@ -257,10 +261,9 @@ public Address() {} generatedCode, "public MinimalDtoBuilder properties(Consumer> propertiesBuilderConsumer)"); - // Still generates: basic setters, String.format for String fields, varargs + // Still generates: basic setters, String.format for String fields, annotations ProcessorAsserts.assertContaining( generatedCode, - "@Generated(\"Generated by org.javahelpers.simple.builders.processor.BuilderProcessor\")", "@BuilderImplementation(forClass = MinimalDto.class)", "class MinimalDtoBuilder implements IBuilderBase", "public MinimalDtoBuilder(MinimalDto instance)", From 9dc102d4bb6daaa75168153c2e8eefb33ffd0171 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 2 Nov 2025 00:45:36 +0100 Subject: [PATCH 26/63] Adding a new configuration: usingBuilderImplementationAnnotation --- .../core/annotations/SimpleBuilder.java | 6 +++++ .../processor/dtos/BuilderConfiguration.java | 23 +++++++++++++++++++ .../enums/CompilerArgumentsEnum.java | 3 +++ .../util/BuilderConfigurationReader.java | 2 ++ .../util/CompilerArgumentsReader.java | 2 ++ .../processor/util/JavaCodeGenerator.java | 4 +++- .../ConfigurationProcessingTest.java | 8 +++++-- 7 files changed, 45 insertions(+), 3 deletions(-) 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 64776274..b5ffe9b0 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 @@ -295,6 +295,12 @@ */ OptionState usingGeneratedAnnotation() default OptionState.UNSET; + /** + * Use {@code @BuilderImplementation} annotation on the generated builder class.
    + * Default: ENABLED Compiler option: -Asimplebuilder.usingBuilderImplementationAnnotation + */ + OptionState usingBuilderImplementationAnnotation() default OptionState.UNSET; + // === Integration === /** * Generate With interface for integrating builder into DTOs.
    diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java index 97f507cb..b9658d95 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java @@ -53,6 +53,7 @@ * @param usingHashSetBuilderWithElementBuilders Use HashSetBuilderWithElementBuilders * @param usingHashMapBuilder Use HashMapBuilder for maps * @param usingGeneratedAnnotation Use Generated annotation + * @param usingBuilderImplementationAnnotation Use BuilderImplementation annotation * @param generateWithInterface Generate With interface */ public record BuilderConfiguration( @@ -70,6 +71,7 @@ public record BuilderConfiguration( OptionState usingHashSetBuilderWithElementBuilders, OptionState usingHashMapBuilder, OptionState usingGeneratedAnnotation, + OptionState usingBuilderImplementationAnnotation, OptionState generateWithInterface) { public static final BuilderConfiguration DEFAULT = @@ -88,6 +90,7 @@ public record BuilderConfiguration( .usingHashSetBuilderWithElementBuilders(ENABLED) .usingHashMapBuilder(ENABLED) .usingGeneratedAnnotation(ENABLED) + .usingBuilderImplementationAnnotation(ENABLED) .generateWithInterface(ENABLED) .build(); @@ -144,6 +147,10 @@ public boolean shouldUseGeneratedAnnotation() { return usingGeneratedAnnotation == ENABLED; } + public boolean shouldUseBuilderImplementationAnnotation() { + return usingBuilderImplementationAnnotation == ENABLED; + } + // === String accessors === public AccessModifier getBuilderAccess() { return builderAccess; @@ -223,6 +230,10 @@ public BuilderConfiguration merge(BuilderConfiguration other) { other.usingGeneratedAnnotation != UNSET ? other.usingGeneratedAnnotation : this.usingGeneratedAnnotation) + .usingBuilderImplementationAnnotation( + other.usingBuilderImplementationAnnotation != UNSET + ? other.usingBuilderImplementationAnnotation + : this.usingBuilderImplementationAnnotation) .generateWithInterface( other.generateWithInterface != UNSET ? other.generateWithInterface @@ -305,6 +316,7 @@ public static class Builder { // === Annotations === private OptionState usingGeneratedAnnotation = OptionState.UNSET; + private OptionState usingBuilderImplementationAnnotation = OptionState.UNSET; // === Integration === private OptionState generateWithInterface = OptionState.UNSET; @@ -440,6 +452,16 @@ public Builder usingGeneratedAnnotation(boolean value) { return this; } + public Builder usingBuilderImplementationAnnotation(OptionState value) { + this.usingBuilderImplementationAnnotation = value; + return this; + } + + public Builder usingBuilderImplementationAnnotation(boolean value) { + this.usingBuilderImplementationAnnotation = value ? ENABLED : DISABLED; + return this; + } + public Builder builderAccess(AccessModifier value) { this.builderAccess = value; return this; @@ -476,6 +498,7 @@ public BuilderConfiguration build() { usingHashSetBuilderWithElementBuilders, usingHashMapBuilder, usingGeneratedAnnotation, + usingBuilderImplementationAnnotation, generateWithInterface); } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java index 35065da2..58c64925 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java @@ -85,6 +85,9 @@ public enum CompilerArgumentsEnum { /** Option for using Generated annotation. */ USING_GENERATED_ANNOTATION("usingGeneratedAnnotation"), + /** Option for using BuilderImplementation annotation. */ + USING_BUILDER_IMPLEMENTATION_ANNOTATION("usingBuilderImplementationAnnotation"), + // === Integration === /** Option for With interface generation. */ GENERATE_WITH_INTERFACE("generateWithInterface"), diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java index 19166d4b..468ca255 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java @@ -89,6 +89,7 @@ public BuilderConfiguration readFromOptions(Element element) { .usingHashSetBuilderWithElementBuilders(options.usingHashSetBuilderWithElementBuilders()) .usingHashMapBuilder(options.usingHashMapBuilder()) .usingGeneratedAnnotation(options.usingGeneratedAnnotation()) + .usingBuilderImplementationAnnotation(options.usingBuilderImplementationAnnotation()) .generateWithInterface(options.generateWithInterface()) .build(); } @@ -140,6 +141,7 @@ public BuilderConfiguration readFromTemplate(Element element) { options.usingHashSetBuilderWithElementBuilders()) .usingHashMapBuilder(options.usingHashMapBuilder()) .usingGeneratedAnnotation(options.usingGeneratedAnnotation()) + .usingBuilderImplementationAnnotation(options.usingBuilderImplementationAnnotation()) .generateWithInterface(options.generateWithInterface()) .build(); } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java index c01dfbbc..3632c215 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java @@ -160,6 +160,8 @@ public BuilderConfiguration readBuilderConfiguration() { readOptionState(CompilerArgumentsEnum.USING_HASH_SET_BUILDER_WITH_ELEMENT_BUILDERS)) .usingHashMapBuilder(readOptionState(CompilerArgumentsEnum.USING_HASH_MAP_BUILDER)) .usingGeneratedAnnotation(readOptionState(CompilerArgumentsEnum.USING_GENERATED_ANNOTATION)) + .usingBuilderImplementationAnnotation( + readOptionState(CompilerArgumentsEnum.USING_BUILDER_IMPLEMENTATION_ANNOTATION)) .generateWithInterface(readOptionState(CompilerArgumentsEnum.GENERATE_WITH_INTERFACE)) .build(); } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java index 02c6b507..32d522b7 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java @@ -182,7 +182,9 @@ public void generateBuilder(BuilderDefinitionDto builderDef) throws BuilderExcep if (builderDef.getConfiguration().shouldUseGeneratedAnnotation()) { classBuilder.addAnnotation(createAnnotationGenerated()); } - classBuilder.addAnnotation(createAnnotationBuilderImplementation(dtoBaseClass)); + if (builderDef.getConfiguration().shouldUseBuilderImplementationAnnotation()) { + classBuilder.addAnnotation(createAnnotationBuilderImplementation(dtoBaseClass)); + } logger.debug( "Writing builder class to file: %s.%s", diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java index 4a521f10..657a5c11 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java @@ -183,6 +183,7 @@ public Address() {} "-Asimplebuilder.usingHashSetBuilderWithElementBuilders=false", "-Asimplebuilder.usingHashMapBuilder=false", "-Asimplebuilder.usingGeneratedAnnotation=false", + "-Asimplebuilder.usingBuilderImplementationAnnotation=false", "-Asimplebuilder.generateWithInterface=false") .compile(nestedDto, addressDto, source); @@ -243,6 +244,10 @@ public Address() {} // With usingGeneratedAnnotation=false, NO @Generated annotation should be used ProcessorAsserts.assertNotContaining(generatedCode, "@Generated("); + // With usingBuilderImplementationAnnotation=false, NO @BuilderImplementation annotation should + // be used + ProcessorAsserts.assertNotContaining(generatedCode, "@BuilderImplementation"); + // With usingArrayListBuilder=false AND generateBuilderProvider=false, NO ArrayListBuilder // should be used ProcessorAsserts.assertNotContaining( @@ -261,10 +266,9 @@ public Address() {} generatedCode, "public MinimalDtoBuilder properties(Consumer> propertiesBuilderConsumer)"); - // Still generates: basic setters, String.format for String fields, annotations + // Still generates: basic setters, String.format for String fields ProcessorAsserts.assertContaining( generatedCode, - "@BuilderImplementation(forClass = MinimalDto.class)", "class MinimalDtoBuilder implements IBuilderBase", "public MinimalDtoBuilder(MinimalDto instance)", "public MinimalDtoBuilder name(String name)", From 9a6dc621cd8c3d2617e5f02901fc8839cb1ab0e0 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 2 Nov 2025 00:51:45 +0100 Subject: [PATCH 27/63] Adding a new configuration: generateStringFormatHelpers --- .../core/annotations/SimpleBuilder.java | 6 +++++ .../processor/dtos/BuilderConfiguration.java | 23 +++++++++++++++++++ .../enums/CompilerArgumentsEnum.java | 3 +++ .../util/BuilderConfigurationReader.java | 2 ++ .../util/BuilderDefinitionCreator.java | 7 ++++-- .../util/CompilerArgumentsReader.java | 2 ++ .../ConfigurationProcessingTest.java | 11 ++++++--- 7 files changed, 49 insertions(+), 5 deletions(-) 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 b5ffe9b0..c6e98ac9 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 @@ -133,6 +133,12 @@ */ OptionState generateVarArgsHelpers() default OptionState.UNSET; + /** + * Generate String format helper methods for String fields.
    + * Default: ENABLED Compiler option: -Asimplebuilder.generateStringFormatHelpers + */ + OptionState generateStringFormatHelpers() default OptionState.UNSET; + /** * Generate unboxed optional methods that accept the inner type T directly instead of * Optional<T>.
    diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java index b9658d95..2c51d785 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java @@ -46,6 +46,7 @@ * @param builderAccess Access level for builder class * @param methodAccess Access level for builder methods * @param generateVarArgsHelpers Generate varargs helper methods + * @param generateStringFormatHelpers Generate string format helper methods * @param generateUnboxedOptional Generate unboxed optional methods * @param usingArrayListBuilder Use ArrayListBuilder for lists * @param usingArrayListBuilderWithElementBuilders Use ArrayListBuilderWithElementBuilders @@ -64,6 +65,7 @@ public record BuilderConfiguration( AccessModifier builderAccess, AccessModifier methodAccess, OptionState generateVarArgsHelpers, + OptionState generateStringFormatHelpers, OptionState generateUnboxedOptional, OptionState usingArrayListBuilder, OptionState usingArrayListBuilderWithElementBuilders, @@ -83,6 +85,7 @@ public record BuilderConfiguration( .builderAccess(PUBLIC) .methodAccess(PUBLIC) .generateVarArgsHelpers(ENABLED) + .generateStringFormatHelpers(ENABLED) .generateUnboxedOptional(ENABLED) .usingArrayListBuilder(ENABLED) .usingArrayListBuilderWithElementBuilders(ENABLED) @@ -119,6 +122,10 @@ public boolean shouldGenerateVarArgsHelpers() { return generateVarArgsHelpers == ENABLED; } + public boolean shouldGenerateStringFormatHelpers() { + return generateStringFormatHelpers == ENABLED; + } + public boolean shouldUseArrayListBuilder() { return usingArrayListBuilder == ENABLED; } @@ -202,6 +209,10 @@ public BuilderConfiguration merge(BuilderConfiguration other) { other.generateVarArgsHelpers != UNSET ? other.generateVarArgsHelpers : this.generateVarArgsHelpers) + .generateStringFormatHelpers( + other.generateStringFormatHelpers != UNSET + ? other.generateStringFormatHelpers + : this.generateStringFormatHelpers) .generateUnboxedOptional( other.generateUnboxedOptional != UNSET ? other.generateUnboxedOptional @@ -307,6 +318,7 @@ public static class Builder { // === Collection Options === private OptionState generateVarArgsHelpers = OptionState.UNSET; + private OptionState generateStringFormatHelpers = OptionState.UNSET; private OptionState generateUnboxedOptional = OptionState.UNSET; private OptionState usingArrayListBuilder = OptionState.UNSET; private OptionState usingArrayListBuilderWithElementBuilders = OptionState.UNSET; @@ -382,6 +394,16 @@ public Builder generateVarArgsHelpers(boolean value) { return this; } + public Builder generateStringFormatHelpers(OptionState value) { + this.generateStringFormatHelpers = value; + return this; + } + + public Builder generateStringFormatHelpers(boolean value) { + this.generateStringFormatHelpers = value ? ENABLED : DISABLED; + return this; + } + public Builder generateUnboxedOptional(OptionState value) { this.generateUnboxedOptional = value; return this; @@ -491,6 +513,7 @@ public BuilderConfiguration build() { builderAccess, methodAccess, generateVarArgsHelpers, + generateStringFormatHelpers, generateUnboxedOptional, usingArrayListBuilder, usingArrayListBuilderWithElementBuilders, diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java index 58c64925..95fef4a0 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java @@ -63,6 +63,9 @@ public enum CompilerArgumentsEnum { /** Option for varargs helper generation. */ GENERATE_VAR_ARGS_HELPERS("generateVarArgsHelpers"), + /** Option for string format helper generation. */ + GENERATE_STRING_FORMAT_HELPERS("generateStringFormatHelpers"), + /** Option for unboxed optional generation. */ GENERATE_UNBOXED_OPTIONAL("generateUnboxedOptional"), diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java index 468ca255..52f0c4ea 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java @@ -81,6 +81,7 @@ public BuilderConfiguration readFromOptions(Element element) { .builderAccess(options.builderAccess()) .methodAccess(options.methodAccess()) .generateVarArgsHelpers(options.generateVarArgsHelpers()) + .generateStringFormatHelpers(options.generateStringFormatHelpers()) .generateUnboxedOptional(options.generateUnboxedOptional()) .usingArrayListBuilder(options.usingArrayListBuilder()) .usingArrayListBuilderWithElementBuilders( @@ -132,6 +133,7 @@ public BuilderConfiguration readFromTemplate(Element element) { .builderAccess(options.builderAccess()) .methodAccess(options.methodAccess()) .generateVarArgsHelpers(options.generateVarArgsHelpers()) + .generateStringFormatHelpers(options.generateStringFormatHelpers()) .generateUnboxedOptional(options.generateUnboxedOptional()) .usingArrayListBuilder(options.usingArrayListBuilder()) .usingArrayListBuilderWithElementBuilders( diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java index e713473d..5eb95908 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java @@ -277,7 +277,9 @@ private static void addAdditionalHelperMethodsForField( String fieldNameInBuilder = field.getFieldName(); String fieldJavaDoc = field.getJavaDoc(); // Check for String type (not array) and add format method - if (isString(field.getFieldType()) && !(field.getFieldType() instanceof TypeNameArray)) { + if (isString(field.getFieldType()) + && !(field.getFieldType() instanceof TypeNameArray) + && context.getBuilderConfigurationForElement().shouldGenerateStringFormatHelpers()) { String fieldName = field.getFieldNameEstimated(); field.addMethod( createStringFormatMethodWithTransform( @@ -375,7 +377,8 @@ private static void addAdditionalHelperMethodsForField( // If Optional, add format method TypeName innerType = innerTypes.get(0); - if (isString(innerType)) { + if (isString(innerType) + && context.getBuilderConfigurationForElement().shouldGenerateStringFormatHelpers()) { field.addMethod( createStringFormatMethodWithTransform( fieldName, diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java index 3632c215..a3c987a2 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java @@ -151,6 +151,8 @@ public BuilderConfiguration readBuilderConfiguration() { .builderAccess(readAccessModifier(CompilerArgumentsEnum.BUILDER_ACCESS)) .methodAccess(readAccessModifier(CompilerArgumentsEnum.METHOD_ACCESS)) .generateVarArgsHelpers(readOptionState(CompilerArgumentsEnum.GENERATE_VAR_ARGS_HELPERS)) + .generateStringFormatHelpers( + readOptionState(CompilerArgumentsEnum.GENERATE_STRING_FORMAT_HELPERS)) .generateUnboxedOptional(readOptionState(CompilerArgumentsEnum.GENERATE_UNBOXED_OPTIONAL)) .usingArrayListBuilder(readOptionState(CompilerArgumentsEnum.USING_ARRAY_LIST_BUILDER)) .usingArrayListBuilderWithElementBuilders( diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java index 657a5c11..3e98299c 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java @@ -176,6 +176,7 @@ public Address() {} "-Asimplebuilder.generateBuilderProvider=false", "-Asimplebuilder.generateConditionalHelper=false", "-Asimplebuilder.generateVarArgsHelpers=false", + "-Asimplebuilder.generateStringFormatHelpers=false", "-Asimplebuilder.generateUnboxedOptional=false", "-Asimplebuilder.usingArrayListBuilder=false", "-Asimplebuilder.usingArrayListBuilderWithElementBuilders=false", @@ -241,6 +242,12 @@ public Address() {} ProcessorAsserts.assertNotContaining( generatedCode, "public MinimalDtoBuilder description(String description)"); + // With generateStringFormatHelpers=false, NO String format methods should be generated + ProcessorAsserts.assertNotContaining( + generatedCode, + "public MinimalDtoBuilder name(String format, Object... args)", + "public MinimalDtoBuilder description(String format, Object... args)"); + // With usingGeneratedAnnotation=false, NO @Generated annotation should be used ProcessorAsserts.assertNotContaining(generatedCode, "@Generated("); @@ -266,14 +273,12 @@ public Address() {} generatedCode, "public MinimalDtoBuilder properties(Consumer> propertiesBuilderConsumer)"); - // Still generates: basic setters, String.format for String fields + // Still generates: basic setters ProcessorAsserts.assertContaining( generatedCode, "class MinimalDtoBuilder implements IBuilderBase", "public MinimalDtoBuilder(MinimalDto instance)", "public MinimalDtoBuilder name(String name)", - "public MinimalDtoBuilder name(String format, Object... args)", - "public MinimalDtoBuilder description(String format, Object... args)", "public MinimalDtoBuilder items(List items)", "public MinimalDtoBuilder properties(Map properties)", "public MinimalDtoBuilder description(Optional description)", From dabdc031b15cac09a52eed87e3a33e7756933709 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 2 Nov 2025 01:00:50 +0100 Subject: [PATCH 28/63] Adding a new configuration: implementsBuilderBase --- .../core/annotations/SimpleBuilder.java | 6 +++++ .../processor/dtos/BuilderConfiguration.java | 23 ++++++++++++++++++ .../enums/CompilerArgumentsEnum.java | 3 +++ .../util/BuilderConfigurationReader.java | 2 ++ .../util/CompilerArgumentsReader.java | 1 + .../processor/util/JavaCodeGenerator.java | 24 ++++++++++++------- .../ConfigurationProcessingTest.java | 10 ++++++-- 7 files changed, 59 insertions(+), 10 deletions(-) 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 c6e98ac9..4372c2b6 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 @@ -308,6 +308,12 @@ OptionState usingBuilderImplementationAnnotation() default OptionState.UNSET; // === Integration === + /** + * Implement {@code IBuilderBase} interface in the generated builder class.
    + * Default: ENABLED Compiler option: -Asimplebuilder.implementsBuilderBase + */ + OptionState implementsBuilderBase() default OptionState.UNSET; + /** * Generate With interface for integrating builder into DTOs.
    * Default: ENABLED Compiler option: -Asimplebuilder.generateWithInterface diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java index 2c51d785..a8b6230f 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java @@ -55,6 +55,7 @@ * @param usingHashMapBuilder Use HashMapBuilder for maps * @param usingGeneratedAnnotation Use Generated annotation * @param usingBuilderImplementationAnnotation Use BuilderImplementation annotation + * @param implementsBuilderBase Implement IBuilderBase interface * @param generateWithInterface Generate With interface */ public record BuilderConfiguration( @@ -74,6 +75,7 @@ public record BuilderConfiguration( OptionState usingHashMapBuilder, OptionState usingGeneratedAnnotation, OptionState usingBuilderImplementationAnnotation, + OptionState implementsBuilderBase, OptionState generateWithInterface) { public static final BuilderConfiguration DEFAULT = @@ -94,6 +96,7 @@ public record BuilderConfiguration( .usingHashMapBuilder(ENABLED) .usingGeneratedAnnotation(ENABLED) .usingBuilderImplementationAnnotation(ENABLED) + .implementsBuilderBase(ENABLED) .generateWithInterface(ENABLED) .build(); @@ -158,6 +161,10 @@ public boolean shouldUseBuilderImplementationAnnotation() { return usingBuilderImplementationAnnotation == ENABLED; } + public boolean shouldImplementBuilderBase() { + return implementsBuilderBase == ENABLED; + } + // === String accessors === public AccessModifier getBuilderAccess() { return builderAccess; @@ -245,6 +252,10 @@ public BuilderConfiguration merge(BuilderConfiguration other) { other.usingBuilderImplementationAnnotation != UNSET ? other.usingBuilderImplementationAnnotation : this.usingBuilderImplementationAnnotation) + .implementsBuilderBase( + other.implementsBuilderBase != UNSET + ? other.implementsBuilderBase + : this.implementsBuilderBase) .generateWithInterface( other.generateWithInterface != UNSET ? other.generateWithInterface @@ -331,6 +342,7 @@ public static class Builder { private OptionState usingBuilderImplementationAnnotation = OptionState.UNSET; // === Integration === + private OptionState implementsBuilderBase = OptionState.UNSET; private OptionState generateWithInterface = OptionState.UNSET; // === Setters === @@ -484,6 +496,16 @@ public Builder usingBuilderImplementationAnnotation(boolean value) { return this; } + public Builder implementsBuilderBase(OptionState value) { + this.implementsBuilderBase = value; + return this; + } + + public Builder implementsBuilderBase(boolean value) { + this.implementsBuilderBase = value ? ENABLED : DISABLED; + return this; + } + public Builder builderAccess(AccessModifier value) { this.builderAccess = value; return this; @@ -522,6 +544,7 @@ public BuilderConfiguration build() { usingHashMapBuilder, usingGeneratedAnnotation, usingBuilderImplementationAnnotation, + implementsBuilderBase, generateWithInterface); } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java index 95fef4a0..43615030 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java @@ -92,6 +92,9 @@ public enum CompilerArgumentsEnum { USING_BUILDER_IMPLEMENTATION_ANNOTATION("usingBuilderImplementationAnnotation"), // === Integration === + /** Option for implementing IBuilderBase interface. */ + IMPLEMENTS_BUILDER_BASE("implementsBuilderBase"), + /** Option for With interface generation. */ GENERATE_WITH_INTERFACE("generateWithInterface"), diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java index 52f0c4ea..9914f8e7 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java @@ -91,6 +91,7 @@ public BuilderConfiguration readFromOptions(Element element) { .usingHashMapBuilder(options.usingHashMapBuilder()) .usingGeneratedAnnotation(options.usingGeneratedAnnotation()) .usingBuilderImplementationAnnotation(options.usingBuilderImplementationAnnotation()) + .implementsBuilderBase(options.implementsBuilderBase()) .generateWithInterface(options.generateWithInterface()) .build(); } @@ -144,6 +145,7 @@ public BuilderConfiguration readFromTemplate(Element element) { .usingHashMapBuilder(options.usingHashMapBuilder()) .usingGeneratedAnnotation(options.usingGeneratedAnnotation()) .usingBuilderImplementationAnnotation(options.usingBuilderImplementationAnnotation()) + .implementsBuilderBase(options.implementsBuilderBase()) .generateWithInterface(options.generateWithInterface()) .build(); } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java index a3c987a2..feff8059 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java @@ -164,6 +164,7 @@ public BuilderConfiguration readBuilderConfiguration() { .usingGeneratedAnnotation(readOptionState(CompilerArgumentsEnum.USING_GENERATED_ANNOTATION)) .usingBuilderImplementationAnnotation( readOptionState(CompilerArgumentsEnum.USING_BUILDER_IMPLEMENTATION_ANNOTATION)) + .implementsBuilderBase(readOptionState(CompilerArgumentsEnum.IMPLEMENTS_BUILDER_BASE)) .generateWithInterface(readOptionState(CompilerArgumentsEnum.GENERATE_WITH_INTERFACE)) .build(); } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java index 32d522b7..90019713 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java @@ -104,8 +104,12 @@ public void generateBuilder(BuilderDefinitionDto builderDef) throws BuilderExcep TypeSpec.Builder classBuilder = TypeSpec.classBuilder(builderBaseClass) .addTypeVariables(map2TypeVariables(builderDef.getGenerics())) - .addJavadoc(createJavadocForClass(dtoBaseClass)) - .addSuperinterface(createInterfaceBuilderBase(dtoTypeName)); + .addJavadoc(createJavadocForClass(dtoBaseClass)); + + // Conditionally add IBuilderBase interface + if (builderDef.getConfiguration().shouldImplementBuilderBase()) { + classBuilder.addSuperinterface(createInterfaceBuilderBase(dtoTypeName)); + } // Adding Constructors for builder classBuilder.addMethod( @@ -160,7 +164,8 @@ public void generateBuilder(BuilderDefinitionDto builderDef) throws BuilderExcep dtoTypeName, builderDef.getConstructorFieldsForBuilder(), builderDef.getSetterFieldsForBuilder(), - builderDef.getGenerics())); + builderDef.getGenerics(), + builderDef.getConfiguration().shouldImplementBuilderBase())); classBuilder.addMethod( createMethodStaticCreate( builderBaseClass, builderTypeName, dtoBaseClass, builderDef.getGenerics())); @@ -368,12 +373,15 @@ private MethodSpec createMethodBuild( com.palantir.javapoet.TypeName returnType, List constructorFields, List setterFields, - List generics) { + List generics, + boolean implementsBuilderBase) { MethodSpec.Builder mb = - MethodSpec.methodBuilder("build") - .addModifiers(PUBLIC) - .returns(returnType) - .addAnnotation(Override.class); + MethodSpec.methodBuilder("build").addModifiers(PUBLIC).returns(returnType); + + // Only add @Override annotation if implementing IBuilderBase interface + if (implementsBuilderBase) { + mb.addAnnotation(Override.class); + } // Validate non-nullable constructor fields: must be set AND can't be null // If not annotated with @NotNull/@NonNull, constructor fields can be left unset (→ null passed) diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java index 3e98299c..ceaa3255 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java @@ -185,6 +185,7 @@ public Address() {} "-Asimplebuilder.usingHashMapBuilder=false", "-Asimplebuilder.usingGeneratedAnnotation=false", "-Asimplebuilder.usingBuilderImplementationAnnotation=false", + "-Asimplebuilder.implementsBuilderBase=false", "-Asimplebuilder.generateWithInterface=false") .compile(nestedDto, addressDto, source); @@ -255,6 +256,10 @@ public Address() {} // be used ProcessorAsserts.assertNotContaining(generatedCode, "@BuilderImplementation"); + // With implementsBuilderBase=false, NO IBuilderBase interface should be implemented + ProcessorAsserts.assertNotContaining( + generatedCode, "implements IBuilderBase", "@Override public MinimalDto build()"); + // With usingArrayListBuilder=false AND generateBuilderProvider=false, NO ArrayListBuilder // should be used ProcessorAsserts.assertNotContaining( @@ -273,10 +278,11 @@ public Address() {} generatedCode, "public MinimalDtoBuilder properties(Consumer> propertiesBuilderConsumer)"); - // Still generates: basic setters + // Still generates: basic setters and build method (without @Override when interface not + // implemented) ProcessorAsserts.assertContaining( generatedCode, - "class MinimalDtoBuilder implements IBuilderBase", + "class MinimalDtoBuilder", "public MinimalDtoBuilder(MinimalDto instance)", "public MinimalDtoBuilder name(String name)", "public MinimalDtoBuilder items(List items)", From bd6de35683f676dd04a8cebb11673330051ab0e5 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 2 Nov 2025 01:12:56 +0100 Subject: [PATCH 29/63] Adding a new configuration: builderConstructorAccess --- .../core/annotations/SimpleBuilder.java | 10 ++++++++ .../processor/dtos/BuilderConfiguration.java | 23 +++++++++++++++++++ .../enums/CompilerArgumentsEnum.java | 3 +++ .../util/BuilderConfigurationReader.java | 2 ++ .../util/CompilerArgumentsReader.java | 2 ++ .../processor/util/JavaCodeGenerator.java | 20 +++++++++++----- .../processor/util/JavapoetMapper.java | 16 +++++++++++++ .../ConfigurationProcessingTest.java | 4 +++- 8 files changed, 73 insertions(+), 7 deletions(-) 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 4372c2b6..f36efb6c 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 @@ -116,6 +116,16 @@ */ AccessModifier builderAccess() default AccessModifier.PUBLIC; + /** + * Access level for generated builder constructors. + * + *

    Default: {@link AccessModifier#PUBLIC PUBLIC} + * + *

    Compiler option: -Asimplebuilder.builderConstructorAccess (values: PUBLIC, PROTECTED, + * PACKAGE_PRIVATE, PRIVATE) + */ + AccessModifier builderConstructorAccess() default AccessModifier.PUBLIC; + /** * Access level for generated builder methods. * diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java index a8b6230f..02241a56 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java @@ -44,6 +44,7 @@ * @param generateBuilderProvider Generate builder provider methods * @param generateConditionalHelper Generate conditional logic methods * @param builderAccess Access level for builder class + * @param builderConstructorAccess Access level for builder constructors * @param methodAccess Access level for builder methods * @param generateVarArgsHelpers Generate varargs helper methods * @param generateStringFormatHelpers Generate string format helper methods @@ -64,6 +65,7 @@ public record BuilderConfiguration( OptionState generateBuilderProvider, OptionState generateConditionalHelper, AccessModifier builderAccess, + AccessModifier builderConstructorAccess, AccessModifier methodAccess, OptionState generateVarArgsHelpers, OptionState generateStringFormatHelpers, @@ -85,6 +87,7 @@ public record BuilderConfiguration( .generateBuilderProvider(ENABLED) .generateConditionalLogic(ENABLED) .builderAccess(PUBLIC) + .builderConstructorAccess(PUBLIC) .methodAccess(PUBLIC) .generateVarArgsHelpers(ENABLED) .generateStringFormatHelpers(ENABLED) @@ -170,6 +173,10 @@ public AccessModifier getBuilderAccess() { return builderAccess; } + public AccessModifier getBuilderConstructorAccess() { + return builderConstructorAccess; + } + public AccessModifier getMethodAccess() { return methodAccess; } @@ -210,6 +217,10 @@ public BuilderConfiguration merge(BuilderConfiguration other) { other.builderAccess != AccessModifier.DEFAULT ? other.builderAccess : this.builderAccess) + .builderConstructorAccess( + other.builderConstructorAccess != AccessModifier.DEFAULT + ? other.builderConstructorAccess + : this.builderConstructorAccess) .methodAccess( other.methodAccess != AccessModifier.DEFAULT ? other.methodAccess : this.methodAccess) .generateVarArgsHelpers( @@ -325,6 +336,7 @@ public static class Builder { // === Access Control === private AccessModifier builderAccess = AccessModifier.DEFAULT; + private AccessModifier builderConstructorAccess = AccessModifier.DEFAULT; private AccessModifier methodAccess = AccessModifier.DEFAULT; // === Collection Options === @@ -516,6 +528,16 @@ public Builder builderAccess(String value) { return this; } + public Builder builderConstructorAccess(AccessModifier value) { + this.builderConstructorAccess = value; + return this; + } + + public Builder builderConstructorAccess(String value) { + this.builderConstructorAccess = AccessModifier.valueOf(value.toUpperCase()); + return this; + } + public Builder methodAccess(AccessModifier value) { this.methodAccess = value; return this; @@ -533,6 +555,7 @@ public BuilderConfiguration build() { generateBuilderProvider, generateConditionalHelper, builderAccess, + builderConstructorAccess, methodAccess, generateVarArgsHelpers, generateStringFormatHelpers, diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java index 43615030..be60d380 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java @@ -56,6 +56,9 @@ public enum CompilerArgumentsEnum { /** Option for builder access level. */ BUILDER_ACCESS("builderAccess"), + /** Option for builder constructor access level. */ + BUILDER_CONSTRUCTOR_ACCESS("builderConstructorAccess"), + /** Option for method access level. */ METHOD_ACCESS("methodAccess"), diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java index 9914f8e7..98e14ddc 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java @@ -79,6 +79,7 @@ public BuilderConfiguration readFromOptions(Element element) { .generateBuilderProvider(options.generateBuilderProvider()) .generateConditionalLogic(options.generateConditionalHelper()) .builderAccess(options.builderAccess()) + .builderConstructorAccess(options.builderConstructorAccess()) .methodAccess(options.methodAccess()) .generateVarArgsHelpers(options.generateVarArgsHelpers()) .generateStringFormatHelpers(options.generateStringFormatHelpers()) @@ -132,6 +133,7 @@ public BuilderConfiguration readFromTemplate(Element element) { .generateBuilderProvider(options.generateBuilderProvider()) .generateConditionalLogic(options.generateConditionalHelper()) .builderAccess(options.builderAccess()) + .builderConstructorAccess(options.builderConstructorAccess()) .methodAccess(options.methodAccess()) .generateVarArgsHelpers(options.generateVarArgsHelpers()) .generateStringFormatHelpers(options.generateStringFormatHelpers()) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java index feff8059..2e2d44b3 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java @@ -149,6 +149,8 @@ public BuilderConfiguration readBuilderConfiguration() { .generateConditionalLogic( readOptionState(CompilerArgumentsEnum.GENERATE_CONDITIONAL_HELPER)) .builderAccess(readAccessModifier(CompilerArgumentsEnum.BUILDER_ACCESS)) + .builderConstructorAccess( + readAccessModifier(CompilerArgumentsEnum.BUILDER_CONSTRUCTOR_ACCESS)) .methodAccess(readAccessModifier(CompilerArgumentsEnum.METHOD_ACCESS)) .generateVarArgsHelpers(readOptionState(CompilerArgumentsEnum.GENERATE_VAR_ARGS_HELPERS)) .generateStringFormatHelpers( diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java index 90019713..078f5203 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java @@ -112,10 +112,15 @@ public void generateBuilder(BuilderDefinitionDto builderDef) throws BuilderExcep } // Adding Constructors for builder + Modifier constructorAccessModifier = + map2Modifier(builderDef.getConfiguration().getBuilderConstructorAccess()); classBuilder.addMethod( createConstructorWithInstance( - dtoBaseClass, dtoTypeName, builderDef.getAllFieldsForBuilder())); - classBuilder.addMethod(createEmptyConstructor(dtoBaseClass)); + dtoBaseClass, + dtoTypeName, + builderDef.getAllFieldsForBuilder(), + constructorAccessModifier)); + classBuilder.addMethod(createEmptyConstructor(dtoBaseClass, constructorAccessModifier)); logger.debug( "Generating %d constructor fields and %d setter fields", @@ -293,10 +298,10 @@ private AnnotationSpec createAnnotationBuilderImplementation(ClassName dtoClass) .build(); } - private MethodSpec createEmptyConstructor(ClassName dtoClass) { + private MethodSpec createEmptyConstructor(ClassName dtoClass, Modifier accessModifier) { MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder() - .addModifiers(Modifier.PUBLIC) + .addModifiers(accessModifier) .addJavadoc( """ Empty constructor of builder for {@code $1N.$2T}. @@ -307,10 +312,13 @@ private MethodSpec createEmptyConstructor(ClassName dtoClass) { } private MethodSpec createConstructorWithInstance( - ClassName dtoBaseClass, com.palantir.javapoet.TypeName dtoType, List fields) { + ClassName dtoBaseClass, + com.palantir.javapoet.TypeName dtoType, + List fields, + Modifier accessModifier) { MethodSpec.Builder cb = MethodSpec.constructorBuilder() - .addModifiers(Modifier.PUBLIC) + .addModifiers(accessModifier) .addParameter(dtoType, "instance") .addJavadoc( """ diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavapoetMapper.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavapoetMapper.java index 20001890..baa6ff38 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavapoetMapper.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavapoetMapper.java @@ -36,6 +36,7 @@ import java.util.Map; import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; +import org.javahelpers.simple.builders.core.enums.AccessModifier; import org.javahelpers.simple.builders.processor.dtos.*; /** Helper functions to create JavaPoet types from DTOs of simple builder. */ @@ -206,4 +207,19 @@ public static AnnotationSpec map2AnnotationSpec(AnnotationDto annotationDto) { public static List map2AnnotationSpecs(List annotations) { return annotations.stream().map(JavapoetMapper::map2AnnotationSpec).toList(); } + + /** + * Maps an AccessModifier enum value to a javax.lang.model.element.Modifier. + * + * @param accessModifier the access modifier to map + * @return the corresponding Modifier + */ + public static javax.lang.model.element.Modifier map2Modifier(AccessModifier accessModifier) { + return switch (accessModifier) { + case PUBLIC, DEFAULT -> javax.lang.model.element.Modifier.PUBLIC; + case PROTECTED -> javax.lang.model.element.Modifier.PROTECTED; + case PRIVATE -> javax.lang.model.element.Modifier.PRIVATE; + case PACKAGE_PRIVATE -> null; // Package-private has no explicit modifier + }; + } } diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java index ceaa3255..b8ab1599 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java @@ -175,6 +175,7 @@ public Address() {} "-Asimplebuilder.generateFieldConsumer=false", "-Asimplebuilder.generateBuilderProvider=false", "-Asimplebuilder.generateConditionalHelper=false", + "-Asimplebuilder.builderConstructorAccess=PRIVATE", "-Asimplebuilder.generateVarArgsHelpers=false", "-Asimplebuilder.generateStringFormatHelpers=false", "-Asimplebuilder.generateUnboxedOptional=false", @@ -283,7 +284,8 @@ public Address() {} ProcessorAsserts.assertContaining( generatedCode, "class MinimalDtoBuilder", - "public MinimalDtoBuilder(MinimalDto instance)", + "private MinimalDtoBuilder()", + "private MinimalDtoBuilder(MinimalDto instance)", "public MinimalDtoBuilder name(String name)", "public MinimalDtoBuilder items(List items)", "public MinimalDtoBuilder properties(Map properties)", From f133a358a1b947dd0dc894ec104194fd63330491 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 2 Nov 2025 01:25:50 +0100 Subject: [PATCH 30/63] Implementation of methodAccess and builderAccess --- .../util/BuilderDefinitionCreator.java | 144 +++++++++++++----- .../util/CompilerArgumentsReader.java | 2 +- .../processor/util/JavaCodeGenerator.java | 144 ++++++++++-------- .../ConfigurationProcessingTest.java | 50 ++++-- 4 files changed, 222 insertions(+), 118 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java index 5eb95908..50348d19 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java @@ -276,19 +276,23 @@ private static void addAdditionalHelperMethodsForField( ProcessingContext context) { String fieldNameInBuilder = field.getFieldName(); String fieldJavaDoc = field.getJavaDoc(); + Modifier methodAccessModifier = getMethodAccessModifier(context); + // Check for String type (not array) and add format method if (isString(field.getFieldType()) && !(field.getFieldType() instanceof TypeNameArray) && context.getBuilderConfigurationForElement().shouldGenerateStringFormatHelpers()) { String fieldName = field.getFieldNameEstimated(); - field.addMethod( + MethodDto method = createStringFormatMethodWithTransform( fieldName, fieldNameInBuilder, fieldJavaDoc, "String.format(format, args)", annotations, - builderType)); + builderType); + setMethodAccessModifier(method, methodAccessModifier); + field.addMethod(method); } if ((field.getFieldType() instanceof TypeNameArray arrayType)) { @@ -297,15 +301,19 @@ private static void addAdditionalHelperMethodsForField( // Add method accepting List and converting to array TypeNameGeneric listType = new TypeNameGeneric(map2TypeName(List.class), elementType); String fieldName = field.getFieldNameEstimated(); - field.addMethod( + MethodDto method1 = createFieldSetterForArrayFromList( - fieldName, fieldNameInBuilder, listType, elementType, builderType)); + fieldName, fieldNameInBuilder, listType, elementType, builderType); + setMethodAccessModifier(method1, methodAccessModifier); + field.addMethod(method1); // Add Consumer> method TypeName collectionBuilderType = map2TypeName(ArrayListBuilder.class); - field.addMethod( + MethodDto method2 = createFieldConsumerWithArrayBuilder( - fieldName, fieldNameInBuilder, collectionBuilderType, elementType, builderType)); + fieldName, fieldNameInBuilder, collectionBuilderType, elementType, builderType); + setMethodAccessModifier(method2, methodAccessModifier); + field.addMethod(method2); return; } @@ -320,27 +328,31 @@ private static void addAdditionalHelperMethodsForField( // Only add varargs helper if enabled in configuration if (context.getBuilderConfigurationForElement().shouldGenerateVarArgsHelpers()) { String fieldName = field.getFieldNameEstimated(); - field.addMethod( + MethodDto method = createFieldSetterWithTransform( fieldName, fieldNameInBuilder, fieldJavaDoc, "List.of(%s)", new TypeNameArray(innerTypes.get(0), false), - builderType)); + builderType); + setMethodAccessModifier(method, methodAccessModifier); + field.addMethod(method); } } else if (isSet(field.getFieldType()) && innerTypesCnt == 1) { // Only add varargs helper if enabled in configuration if (context.getBuilderConfigurationForElement().shouldGenerateVarArgsHelpers()) { String fieldName = field.getFieldNameEstimated(); - field.addMethod( + MethodDto method = createFieldSetterWithTransform( fieldName, fieldNameInBuilder, fieldJavaDoc, "Set.of(%s)", new TypeNameArray(innerTypes.get(0), true), - builderType)); + builderType); + setMethodAccessModifier(method, methodAccessModifier); + field.addMethod(method); } } else if (isMap(field.getFieldType()) && innerTypesCnt == 2) { // Only add varargs helper if enabled in configuration @@ -350,14 +362,16 @@ private static void addAdditionalHelperMethodsForField( new TypeNameGeneric("java.util", "Map.Entry", innerTypes.get(0), innerTypes.get(1)), false); String fieldName = field.getFieldNameEstimated(); - field.addMethod( + MethodDto method = createFieldSetterWithTransform( fieldName, fieldNameInBuilder, fieldJavaDoc, "Map.ofEntries(%s)", mapEntryType, - builderType)); + builderType); + setMethodAccessModifier(method, methodAccessModifier); + field.addMethod(method); } } else if (isOptional(field.getFieldType()) && innerTypesCnt == 1) { String fieldName = field.getFieldNameEstimated(); @@ -365,28 +379,32 @@ private static void addAdditionalHelperMethodsForField( // Only generate unboxed optional method if enabled in configuration if (context.getBuilderConfigurationForElement().shouldGenerateUnboxedOptional()) { // Add setter that accepts the inner type T and wraps it in Optional.ofNullable() - field.addMethod( + MethodDto method = createFieldSetterWithTransform( fieldName, fieldNameInBuilder, fieldJavaDoc, "Optional.ofNullable(%s)", innerTypes.get(0), - builderType)); + builderType); + setMethodAccessModifier(method, methodAccessModifier); + field.addMethod(method); } // If Optional, add format method TypeName innerType = innerTypes.get(0); if (isString(innerType) && context.getBuilderConfigurationForElement().shouldGenerateStringFormatHelpers()) { - field.addMethod( + MethodDto method = createStringFormatMethodWithTransform( fieldName, fieldNameInBuilder, fieldJavaDoc, "Optional.of(String.format(format, args))", List.of(), - builderType)); + builderType); + setMethodAccessModifier(method, methodAccessModifier); + field.addMethod(method); } } } @@ -428,9 +446,11 @@ private static boolean tryAddBuilderConsumer( Optional fieldBuilderOpt = resolveBuilderType(fieldParameter, context); if (fieldBuilderOpt.isPresent()) { TypeName fieldBuilderType = fieldBuilderOpt.get(); - field.addMethod( + MethodDto method = BuilderDefinitionCreator.createFieldConsumerWithBuilder( - field.getFieldName(), field.getJavaDoc(), fieldBuilderType, builderType)); + field.getFieldName(), field.getJavaDoc(), fieldBuilderType, builderType); + setMethodAccessModifier(method, getMethodAccessModifier(context)); + field.addMethod(method); return true; } return false; @@ -452,9 +472,11 @@ private static boolean tryAddFieldConsumer( && !fieldTypeElement.getModifiers().contains(Modifier.ABSTRACT) && hasEmptyConstructor(fieldTypeElement, context)) { // Only generate a Consumer for concrete classes with an accessible empty constructor - field.addMethod( + MethodDto method = createFieldConsumer( - field.getFieldName(), field.getJavaDoc(), field.getFieldType(), builderType)); + field.getFieldName(), field.getJavaDoc(), field.getFieldType(), builderType); + setMethodAccessModifier(method, getMethodAccessModifier(context)); + field.addMethod(method); return true; } return false; @@ -472,9 +494,11 @@ private static boolean tryAddStringBuilderConsumer( isOptionalString(field.getFieldType()) ? "Optional.of(builder.toString())" : "builder.toString()"; - field.addMethod( + MethodDto method = createStringBuilderConsumer( - field.getFieldName(), field.getJavaDoc(), transform, builderType)); + field.getFieldName(), field.getJavaDoc(), transform, builderType); + setMethodAccessModifier(method, getMethodAccessModifier(context)); + field.addMethod(method); return true; } return false; @@ -511,23 +535,27 @@ private static boolean tryAddListConsumer( map2TypeName(ArrayListBuilderWithElementBuilders.class), elementType, elementBuilderType.get()); - field.addMethod( + MethodDto method = createFieldConsumerWithElementBuilders( field.getFieldName(), field.getJavaDoc(), collectionBuilderType, elementBuilderType.get(), - builderType)); + builderType); + setMethodAccessModifier(method, getMethodAccessModifier(context)); + field.addMethod(method); } else if (context.getBuilderConfigurationForElement().shouldUseArrayListBuilder()) { // Regular ArrayListBuilder if enabled TypeName collectionBuilderType = map2TypeName(ArrayListBuilder.class); - field.addMethod( + MethodDto method = createFieldConsumerWithBuilder( field.getFieldName(), field.getJavaDoc(), collectionBuilderType, elementType, - builderType)); + builderType); + setMethodAccessModifier(method, getMethodAccessModifier(context)); + field.addMethod(method); } else { return false; } @@ -555,6 +583,7 @@ private static boolean tryAddMapConsumer( MethodDto mapConsumerWithBuilder = BuilderDefinitionCreator.createFieldConsumerWithBuilder( field.getFieldName(), field.getJavaDoc(), builderTargetTypeName, builderType); + setMethodAccessModifier(mapConsumerWithBuilder, getMethodAccessModifier(context)); field.addMethod(mapConsumerWithBuilder); return true; } @@ -590,23 +619,27 @@ private static boolean tryAddSetConsumer( map2TypeName(HashSetBuilderWithElementBuilders.class), elementType, elementBuilderType.get()); - field.addMethod( + MethodDto method = createFieldConsumerWithElementBuilders( field.getFieldName(), field.getJavaDoc(), collectionBuilderType, elementBuilderType.get(), - builderType)); + builderType); + setMethodAccessModifier(method, getMethodAccessModifier(context)); + field.addMethod(method); } else if (context.getBuilderConfigurationForElement().shouldUseHashSetBuilder()) { // Regular HashSetBuilder if enabled TypeName collectionBuilderType = map2TypeName(HashSetBuilder.class); - field.addMethod( + MethodDto method = createFieldConsumerWithBuilder( field.getFieldName(), field.getJavaDoc(), collectionBuilderType, elementType, - builderType)); + builderType); + setMethodAccessModifier(method, getMethodAccessModifier(context)); + field.addMethod(method); } else { return false; } @@ -629,9 +662,11 @@ private static void addSupplierMethodsForField( // For all fields including Optional, use the real field type for suppliers String fieldName = field.getFieldNameEstimated(); String fieldNameInBuilder = field.getFieldName(); - field.addMethod( + MethodDto method = createFieldSupplier( - fieldName, fieldNameInBuilder, field.getJavaDoc(), field.getFieldType(), builderType)); + fieldName, fieldNameInBuilder, field.getJavaDoc(), field.getFieldType(), builderType); + setMethodAccessModifier(method, getMethodAccessModifier(context)); + field.addMethod(method); } private static Optional createFieldFromSetter( @@ -822,9 +857,11 @@ private static Optional createFieldDto( } // Add basic setter method with annotations - use ORIGINAL field name for method name - field.addMethod( + MethodDto method = createFieldSetterWithTransform( - fieldName, fieldNameInBuilder, javaDoc, null, fieldType, annotations, builderType)); + fieldName, fieldNameInBuilder, javaDoc, null, fieldType, annotations, builderType); + setMethodAccessModifier(method, getMethodAccessModifier(context)); + field.addMethod(method); // Add consumer/supplier/helper methods - use ORIGINAL field name for method names addConsumerMethodsForField(field, param, fieldTypeElement, builderType, context); @@ -882,7 +919,7 @@ private static MethodDto createFieldSetterWithTransform( methodDto.setMethodName(fieldName); methodDto.setReturnType(builderType); methodDto.addParameter(parameter); - methodDto.setModifier(Modifier.PUBLIC); + // Modifier is controlled by configuration, not set here String params; if (StringUtils.isBlank(transform)) { params = parameter.getParameterName(); @@ -921,7 +958,7 @@ private static MethodDto createFieldConsumer( methodDto.setMethodName(fieldName); methodDto.setReturnType(builderType); methodDto.addParameter(parameter); - methodDto.setModifier(Modifier.PUBLIC); + // Modifier is controlled by configuration, not set here methodDto.setCode( """ $helperType:T consumer = this.$fieldName:N.isSet() ? this.$fieldName:N.value() : new $helperType:T(); @@ -956,7 +993,7 @@ private static MethodDto createStringBuilderConsumer( MethodDto methodDto = new MethodDto(); methodDto.setMethodName(fieldName); methodDto.addParameter(parameter); - methodDto.setModifier(Modifier.PUBLIC); + // Modifier is controlled by configuration, not set here methodDto.setCode( """ StringBuilder builder = new StringBuilder(); @@ -1056,7 +1093,7 @@ private static MethodDto createFieldConsumerWithBuilder( methodDto.setMethodName(fieldName); methodDto.setReturnType(returnBuilderType); methodDto.addParameter(parameter); - methodDto.setModifier(Modifier.PUBLIC); + // Modifier is controlled by configuration, not set here methodDto.setCode( """ $helperType:T builder = this.$fieldName:N.isSet() ? new $helperType:T(%s) : new $helperType:T(%s); @@ -1096,7 +1133,7 @@ private static MethodDto createFieldSupplier( methodDto.setMethodName(fieldName); methodDto.setReturnType(builderType); methodDto.addParameter(parameter); - methodDto.setModifier(Modifier.PUBLIC); + // Modifier is controlled by configuration, not set here methodDto.setCode( """ this.$fieldName:N = $builderFieldWrapper:T.changedValue($dtoMethodParam:N.get()); @@ -1141,7 +1178,7 @@ private static MethodDto createStringFormatMethodWithTransform( methodDto.setReturnType(builderType); methodDto.addParameter(formatParam); methodDto.addParameter(argsParam); - methodDto.setModifier(Modifier.PUBLIC); + // Modifier is controlled by configuration, not set here methodDto.setCode( """ this.$fieldName:N = $builderFieldWrapper:T.changedValue($transform:N); @@ -1191,7 +1228,7 @@ private static MethodDto createFieldSetterForArrayFromList( methodDto.setMethodName(fieldName); methodDto.setReturnType(builderType); methodDto.addParameter(parameter); - methodDto.setModifier(Modifier.PUBLIC); + // Modifier is controlled by configuration, not set here methodDto.setCode( """ this.$fieldName:N = $builderFieldWrapper:T.changedValue($dtoMethodParams:N.toArray(new $elementType:T[0])); @@ -1235,7 +1272,7 @@ private static MethodDto createFieldConsumerWithArrayBuilder( methodDto.setMethodName(fieldName); methodDto.setReturnType(returnBuilderType); methodDto.addParameter(parameter); - methodDto.setModifier(Modifier.PUBLIC); + // Modifier is controlled by configuration, not set here methodDto.setCode( """ @@ -1490,4 +1527,27 @@ private static MethodDto createWithBuilderMethod(BuilderDefinitionDto builderDef return method; } + + /** + * Gets the method access modifier from the builder configuration. + * + * @param context the processing context + * @return the Modifier for method access, or null for package-private + */ + private static Modifier getMethodAccessModifier(ProcessingContext context) { + return JavapoetMapper.map2Modifier( + context.getBuilderConfigurationForElement().getMethodAccess()); + } + + /** + * Sets the access modifier on a MethodDto if the modifier is not null. + * + * @param method the MethodDto to update + * @param modifier the access modifier to set, or null for package-private + */ + private static void setMethodAccessModifier(MethodDto method, Modifier modifier) { + if (modifier != null) { + method.setModifier(modifier); + } + } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java index 2e2d44b3..dc18170b 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java @@ -117,7 +117,7 @@ public AccessModifier readAccessModifier(CompilerArgumentsEnum argument) { return AccessModifier.PUBLIC; } else if (Strings.CI.equals(value, "private")) { return AccessModifier.PRIVATE; - } else if (Strings.CI.equals(value, "package-private")) { + } else if (Strings.CI.equalsAny(value, "package-private", "package_private")) { return AccessModifier.PACKAGE_PRIVATE; } else if (Strings.CI.equals(value, "protected")) { return AccessModifier.PROTECTED; diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java index 078f5203..ab86d654 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java @@ -106,14 +106,21 @@ public void generateBuilder(BuilderDefinitionDto builderDef) throws BuilderExcep .addTypeVariables(map2TypeVariables(builderDef.getGenerics())) .addJavadoc(createJavadocForClass(dtoBaseClass)); + // Set builder class access level + Modifier builderAccessModifier = map2Modifier(builderDef.getConfiguration().getBuilderAccess()); + if (builderAccessModifier != null) { + classBuilder.addModifiers(builderAccessModifier); + } + // Conditionally add IBuilderBase interface if (builderDef.getConfiguration().shouldImplementBuilderBase()) { classBuilder.addSuperinterface(createInterfaceBuilderBase(dtoTypeName)); } - // Adding Constructors for builder + // Get access modifiers from configuration Modifier constructorAccessModifier = map2Modifier(builderDef.getConfiguration().getBuilderConstructorAccess()); + Modifier methodAccessModifier = map2Modifier(builderDef.getConfiguration().getMethodAccess()); classBuilder.addMethod( createConstructorWithInstance( dtoBaseClass, @@ -170,15 +177,21 @@ public void generateBuilder(BuilderDefinitionDto builderDef) throws BuilderExcep builderDef.getConstructorFieldsForBuilder(), builderDef.getSetterFieldsForBuilder(), builderDef.getGenerics(), - builderDef.getConfiguration().shouldImplementBuilderBase())); + builderDef.getConfiguration().shouldImplementBuilderBase(), + methodAccessModifier)); classBuilder.addMethod( createMethodStaticCreate( - builderBaseClass, builderTypeName, dtoBaseClass, builderDef.getGenerics())); + builderBaseClass, + builderTypeName, + dtoBaseClass, + builderDef.getGenerics(), + methodAccessModifier)); // Add conditional methods only if enabled in configuration if (builderDef.getConfiguration().shouldGenerateConditionalLogic()) { - classBuilder.addMethod(createMethodConditional(builderTypeName)); - classBuilder.addMethod(createMethodConditionalPositiveOnly(builderTypeName)); + classBuilder.addMethod(createMethodConditional(builderTypeName, methodAccessModifier)); + classBuilder.addMethod( + createMethodConditionalPositiveOnly(builderTypeName, methodAccessModifier)); } // Adding nested types (e.g., With interface) @@ -382,9 +395,12 @@ private MethodSpec createMethodBuild( List constructorFields, List setterFields, List generics, - boolean implementsBuilderBase) { - MethodSpec.Builder mb = - MethodSpec.methodBuilder("build").addModifiers(PUBLIC).returns(returnType); + boolean implementsBuilderBase, + Modifier methodAccessModifier) { + MethodSpec.Builder mb = MethodSpec.methodBuilder("build").returns(returnType); + if (methodAccessModifier != null) { + mb.addModifiers(methodAccessModifier); + } // Only add @Override annotation if implementing IBuilderBase interface if (implementsBuilderBase) { @@ -453,21 +469,25 @@ private MethodSpec createMethodBuild( } private MethodSpec createMethodStaticCreate( - com.palantir.javapoet.ClassName builderBaseClass, + ClassName builderBaseClass, com.palantir.javapoet.TypeName builderType, - com.palantir.javapoet.ClassName dtoBaseClass, - List generics) { + ClassName dtoBaseClass, + List generics, + Modifier methodAccessModifier) { MethodSpec.Builder methodBuilder = - MethodSpec.methodBuilder(METHOD_NAME_CREATE) - .addModifiers(STATIC, PUBLIC) - .addJavadoc( - """ + MethodSpec.methodBuilder(METHOD_NAME_CREATE).addModifiers(STATIC); + if (methodAccessModifier != null) { + methodBuilder.addModifiers(methodAccessModifier); + } + + methodBuilder.addJavadoc( + """ Creating a new builder for {@code $1N.$2T}. @return builder for {@code $1N.$2T} """, - dtoBaseClass.packageName(), - dtoBaseClass); + dtoBaseClass.packageName(), + dtoBaseClass); if (generics.isEmpty()) { methodBuilder.returns(builderBaseClass).addCode("return new $1T();\n", builderBaseClass); } else { @@ -479,10 +499,14 @@ private MethodSpec createMethodStaticCreate( return methodBuilder.build(); } - private MethodSpec createMethodConditional(com.palantir.javapoet.TypeName builderType) { - return MethodSpec.methodBuilder("conditional") - .addModifiers(PUBLIC) - .returns(builderType) + private MethodSpec createMethodConditional( + com.palantir.javapoet.TypeName builderType, Modifier methodAccessModifier) { + MethodSpec.Builder mb = MethodSpec.methodBuilder("conditional"); + if (methodAccessModifier != null) { + mb.addModifiers(methodAccessModifier); + } + + mb.returns(builderType) .addParameter(ClassName.get(java.util.function.BooleanSupplier.class), "condition") .addParameter( ParameterizedTypeName.get( @@ -494,30 +518,33 @@ private MethodSpec createMethodConditional(com.palantir.javapoet.TypeName builde "falseCase") .addJavadoc( """ - Conditionally applies builder modifications based on a condition. + Conditionally applies builder modifications based on a condition evaluation. - @param condition the condition to evaluate - @param trueCase the consumer to apply if condition is true - @param falseCase the consumer to apply if condition is false (can be null) - @return this builder instance + @param condition condition supplier that is evaluated + @param trueCase consumer to apply when condition is true + @param falseCase consumer to apply when condition is false + @return current instance of builder """) .addCode( """ - if (condition.getAsBoolean()) { + if (condition.getAsBoolean() && trueCase != null) { trueCase.accept(this); } else if (falseCase != null) { falseCase.accept(this); } return this; - """) - .build(); + """); + return mb.build(); } private MethodSpec createMethodConditionalPositiveOnly( - com.palantir.javapoet.TypeName builderType) { - return MethodSpec.methodBuilder("conditional") - .addModifiers(PUBLIC) - .returns(builderType) + com.palantir.javapoet.TypeName builderType, Modifier methodAccessModifier) { + MethodSpec.Builder mb = MethodSpec.methodBuilder("conditional"); + if (methodAccessModifier != null) { + mb.addModifiers(methodAccessModifier); + } + + mb.returns(builderType) .addParameter(ClassName.get(java.util.function.BooleanSupplier.class), "condition") .addParameter( ParameterizedTypeName.get( @@ -527,23 +554,22 @@ private MethodSpec createMethodConditionalPositiveOnly( """ Conditionally applies builder modifications if the condition is true. - @param condition the condition to evaluate - @param yesCondition the consumer to apply if condition is true - @return this builder instance + @param condition condition supplier that is evaluated + @param yesCondition consumer to apply when condition is true + @return current instance of builder """) - .addCode("return conditional(condition, yesCondition, null);\n") - .build(); + .addCode( + """ + if (condition.getAsBoolean() && yesCondition != null) { + yesCondition.accept(this); + } + return this; + """); + return mb.build(); } - /** - * Creates a TypeSpec for a nested type (e.g., With interface). - * - * @param nestedType the nested type definition - * @return the TypeSpec for the nested type - */ private TypeSpec createNestedType(NestedTypeDto nestedType) { TypeSpec.Builder typeBuilder; - boolean isInterface = nestedType.getKind() == NestedTypeDto.NestedTypeKind.INTERFACE; if (isInterface) { typeBuilder = TypeSpec.interfaceBuilder(nestedType.getTypeName()); @@ -559,9 +585,8 @@ private TypeSpec createNestedType(NestedTypeDto nestedType) { typeBuilder.addJavadoc(nestedType.getJavadoc()); } - // Add methods to the nested type - for (MethodDto method : nestedType.getMethods()) { - MethodSpec methodSpec = createNestedTypeMethod(method, isInterface); + for (MethodDto methodDto : nestedType.getMethods()) { + MethodSpec methodSpec = createNestedTypeMethod(methodDto, isInterface); typeBuilder.addMethod(methodSpec); } @@ -569,11 +594,12 @@ private TypeSpec createNestedType(NestedTypeDto nestedType) { } /** - * Creates a MethodSpec for a method of a nested type (e.g., With interface). + * Creates a method specification from a MethodDto for nested types (e.g., With interface + * methods). * - * @param methodDto the method to create - * @param isInterface whether the nested type is an interface - * @return the MethodSpec + * @param methodDto the method definition + * @param isInterface whether the containing type is an interface + * @return the generated MethodSpec */ private MethodSpec createNestedTypeMethod(MethodDto methodDto, boolean isInterface) { MethodSpec.Builder methodBuilder = @@ -587,23 +613,17 @@ private MethodSpec createNestedTypeMethod(MethodDto methodDto, boolean isInterfa methodBuilder.addParameter(createParameter(paramDto)); } - // Add modifiers if defined - methodDto.getModifier().ifPresent(methodBuilder::addModifiers); - - // Add Javadoc if (methodDto.getJavadoc() != null) { methodBuilder.addJavadoc(methodDto.getJavadoc()); } - // Add method body if present - MethodCodeDto codeDto = methodDto.getMethodCodeDto(); - if (codeDto != null) { - // Add default modifier for interface methods with implementation + // Add code only if method has implementation (even for interfaces with default methods) + if (methodDto.getMethodCodeDto() != null) { if (isInterface) { methodBuilder.addModifiers(javax.lang.model.element.Modifier.DEFAULT); } - methodBuilder.addCode(map2CodeBlock(codeDto)); + methodBuilder.addCode(map2CodeBlock(methodDto.getMethodCodeDto())); } return methodBuilder.build(); @@ -612,6 +632,8 @@ private MethodSpec createNestedTypeMethod(MethodDto methodDto, boolean isInterfa private MethodSpec createMethod(MethodDto methodDto, com.palantir.javapoet.TypeName returnType) { MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(methodDto.getMethodName()).returns(returnType); + + // Use modifier from MethodDto if present methodDto.getModifier().ifPresent(methodBuilder::addModifiers); // Use javadoc from MethodDto if available diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java index b8ab1599..8f6a9f86 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java @@ -175,7 +175,9 @@ public Address() {} "-Asimplebuilder.generateFieldConsumer=false", "-Asimplebuilder.generateBuilderProvider=false", "-Asimplebuilder.generateConditionalHelper=false", + "-Asimplebuilder.builderAccess=PACKAGE_PRIVATE", "-Asimplebuilder.builderConstructorAccess=PRIVATE", + "-Asimplebuilder.methodAccess=PACKAGE_PRIVATE", "-Asimplebuilder.generateVarArgsHelpers=false", "-Asimplebuilder.generateStringFormatHelpers=false", "-Asimplebuilder.generateUnboxedOptional=false", @@ -261,40 +263,60 @@ public Address() {} ProcessorAsserts.assertNotContaining( generatedCode, "implements IBuilderBase", "@Override public MinimalDto build()"); + // With builderAccess=PACKAGE_PRIVATE, builder class should NOT have public modifier + ProcessorAsserts.assertNotContaining(generatedCode, "public class MinimalDtoBuilder"); + + // But package-private class should exist + ProcessorAsserts.assertContaining(generatedCode, "class MinimalDtoBuilder"); + + // With methodAccess=PACKAGE_PRIVATE, methods should NOT have public modifier + ProcessorAsserts.assertNotContaining( + generatedCode, + "public MinimalDtoBuilder name(String name)", + "public MinimalDto build()", + "public static MinimalDtoBuilder create()"); + + // But package-private methods should exist + ProcessorAsserts.assertContaining( + generatedCode, + "MinimalDtoBuilder name(String name)", + "MinimalDto build()", + "static MinimalDtoBuilder create()"); + // With usingArrayListBuilder=false AND generateBuilderProvider=false, NO ArrayListBuilder // should be used ProcessorAsserts.assertNotContaining( generatedCode, - "public MinimalDtoBuilder items(Consumer> itemsBuilderConsumer)"); + "MinimalDtoBuilder items(Consumer> itemsBuilderConsumer)"); // With usingHashSetBuilder=false AND generateBuilderProvider=false, NO HashSetBuilder should be // used ProcessorAsserts.assertNotContaining( generatedCode, - "public MinimalDtoBuilder tags(Consumer> tagsBuilderConsumer)"); + "MinimalDtoBuilder tags(Consumer> tagsBuilderConsumer)"); // With usingHashMapBuilder=false AND generateBuilderProvider=false, NO HashMapBuilder should be // used ProcessorAsserts.assertNotContaining( generatedCode, - "public MinimalDtoBuilder properties(Consumer> propertiesBuilderConsumer)"); + "MinimalDtoBuilder properties(Consumer> propertiesBuilderConsumer)"); - // Still generates: basic setters and build method (without @Override when interface not - // implemented) + // Still generates: basic setters and build method (package-private methods, private + // constructors) ProcessorAsserts.assertContaining( generatedCode, "class MinimalDtoBuilder", "private MinimalDtoBuilder()", "private MinimalDtoBuilder(MinimalDto instance)", - "public MinimalDtoBuilder name(String name)", - "public MinimalDtoBuilder items(List items)", - "public MinimalDtoBuilder properties(Map properties)", - "public MinimalDtoBuilder description(Optional description)", - "public MinimalDtoBuilder tags(Set tags)", - "public MinimalDtoBuilder nested(NestedDto nested)", - "public MinimalDtoBuilder address(Address address)", - "public MinimalDto build()", - "public static MinimalDtoBuilder create()"); + "MinimalDtoBuilder name(String name)", + "MinimalDtoBuilder items(List items)", + "MinimalDtoBuilder properties(Map properties)", + "MinimalDtoBuilder description(Optional description)", + "MinimalDtoBuilder tags(Set tags)", + "MinimalDtoBuilder nested(NestedDto nested)", + "MinimalDtoBuilder address(Address address)", + "MinimalDto build()", + "static MinimalDtoBuilder create()"); } /** From dd849cd1bcb93bb304a300b3c951ee8a12335de8 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 2 Nov 2025 01:25:50 +0100 Subject: [PATCH 31/63] Implementation of methodAccess and builderAccess --- .../util/BuilderDefinitionCreator.java | 144 +++++++++++++----- .../util/CompilerArgumentsReader.java | 2 +- .../processor/util/JavaCodeGenerator.java | 122 ++++++++------- .../ConfigurationProcessingTest.java | 50 ++++-- 4 files changed, 208 insertions(+), 110 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java index 5eb95908..50348d19 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java @@ -276,19 +276,23 @@ private static void addAdditionalHelperMethodsForField( ProcessingContext context) { String fieldNameInBuilder = field.getFieldName(); String fieldJavaDoc = field.getJavaDoc(); + Modifier methodAccessModifier = getMethodAccessModifier(context); + // Check for String type (not array) and add format method if (isString(field.getFieldType()) && !(field.getFieldType() instanceof TypeNameArray) && context.getBuilderConfigurationForElement().shouldGenerateStringFormatHelpers()) { String fieldName = field.getFieldNameEstimated(); - field.addMethod( + MethodDto method = createStringFormatMethodWithTransform( fieldName, fieldNameInBuilder, fieldJavaDoc, "String.format(format, args)", annotations, - builderType)); + builderType); + setMethodAccessModifier(method, methodAccessModifier); + field.addMethod(method); } if ((field.getFieldType() instanceof TypeNameArray arrayType)) { @@ -297,15 +301,19 @@ private static void addAdditionalHelperMethodsForField( // Add method accepting List and converting to array TypeNameGeneric listType = new TypeNameGeneric(map2TypeName(List.class), elementType); String fieldName = field.getFieldNameEstimated(); - field.addMethod( + MethodDto method1 = createFieldSetterForArrayFromList( - fieldName, fieldNameInBuilder, listType, elementType, builderType)); + fieldName, fieldNameInBuilder, listType, elementType, builderType); + setMethodAccessModifier(method1, methodAccessModifier); + field.addMethod(method1); // Add Consumer> method TypeName collectionBuilderType = map2TypeName(ArrayListBuilder.class); - field.addMethod( + MethodDto method2 = createFieldConsumerWithArrayBuilder( - fieldName, fieldNameInBuilder, collectionBuilderType, elementType, builderType)); + fieldName, fieldNameInBuilder, collectionBuilderType, elementType, builderType); + setMethodAccessModifier(method2, methodAccessModifier); + field.addMethod(method2); return; } @@ -320,27 +328,31 @@ private static void addAdditionalHelperMethodsForField( // Only add varargs helper if enabled in configuration if (context.getBuilderConfigurationForElement().shouldGenerateVarArgsHelpers()) { String fieldName = field.getFieldNameEstimated(); - field.addMethod( + MethodDto method = createFieldSetterWithTransform( fieldName, fieldNameInBuilder, fieldJavaDoc, "List.of(%s)", new TypeNameArray(innerTypes.get(0), false), - builderType)); + builderType); + setMethodAccessModifier(method, methodAccessModifier); + field.addMethod(method); } } else if (isSet(field.getFieldType()) && innerTypesCnt == 1) { // Only add varargs helper if enabled in configuration if (context.getBuilderConfigurationForElement().shouldGenerateVarArgsHelpers()) { String fieldName = field.getFieldNameEstimated(); - field.addMethod( + MethodDto method = createFieldSetterWithTransform( fieldName, fieldNameInBuilder, fieldJavaDoc, "Set.of(%s)", new TypeNameArray(innerTypes.get(0), true), - builderType)); + builderType); + setMethodAccessModifier(method, methodAccessModifier); + field.addMethod(method); } } else if (isMap(field.getFieldType()) && innerTypesCnt == 2) { // Only add varargs helper if enabled in configuration @@ -350,14 +362,16 @@ private static void addAdditionalHelperMethodsForField( new TypeNameGeneric("java.util", "Map.Entry", innerTypes.get(0), innerTypes.get(1)), false); String fieldName = field.getFieldNameEstimated(); - field.addMethod( + MethodDto method = createFieldSetterWithTransform( fieldName, fieldNameInBuilder, fieldJavaDoc, "Map.ofEntries(%s)", mapEntryType, - builderType)); + builderType); + setMethodAccessModifier(method, methodAccessModifier); + field.addMethod(method); } } else if (isOptional(field.getFieldType()) && innerTypesCnt == 1) { String fieldName = field.getFieldNameEstimated(); @@ -365,28 +379,32 @@ private static void addAdditionalHelperMethodsForField( // Only generate unboxed optional method if enabled in configuration if (context.getBuilderConfigurationForElement().shouldGenerateUnboxedOptional()) { // Add setter that accepts the inner type T and wraps it in Optional.ofNullable() - field.addMethod( + MethodDto method = createFieldSetterWithTransform( fieldName, fieldNameInBuilder, fieldJavaDoc, "Optional.ofNullable(%s)", innerTypes.get(0), - builderType)); + builderType); + setMethodAccessModifier(method, methodAccessModifier); + field.addMethod(method); } // If Optional, add format method TypeName innerType = innerTypes.get(0); if (isString(innerType) && context.getBuilderConfigurationForElement().shouldGenerateStringFormatHelpers()) { - field.addMethod( + MethodDto method = createStringFormatMethodWithTransform( fieldName, fieldNameInBuilder, fieldJavaDoc, "Optional.of(String.format(format, args))", List.of(), - builderType)); + builderType); + setMethodAccessModifier(method, methodAccessModifier); + field.addMethod(method); } } } @@ -428,9 +446,11 @@ private static boolean tryAddBuilderConsumer( Optional fieldBuilderOpt = resolveBuilderType(fieldParameter, context); if (fieldBuilderOpt.isPresent()) { TypeName fieldBuilderType = fieldBuilderOpt.get(); - field.addMethod( + MethodDto method = BuilderDefinitionCreator.createFieldConsumerWithBuilder( - field.getFieldName(), field.getJavaDoc(), fieldBuilderType, builderType)); + field.getFieldName(), field.getJavaDoc(), fieldBuilderType, builderType); + setMethodAccessModifier(method, getMethodAccessModifier(context)); + field.addMethod(method); return true; } return false; @@ -452,9 +472,11 @@ private static boolean tryAddFieldConsumer( && !fieldTypeElement.getModifiers().contains(Modifier.ABSTRACT) && hasEmptyConstructor(fieldTypeElement, context)) { // Only generate a Consumer for concrete classes with an accessible empty constructor - field.addMethod( + MethodDto method = createFieldConsumer( - field.getFieldName(), field.getJavaDoc(), field.getFieldType(), builderType)); + field.getFieldName(), field.getJavaDoc(), field.getFieldType(), builderType); + setMethodAccessModifier(method, getMethodAccessModifier(context)); + field.addMethod(method); return true; } return false; @@ -472,9 +494,11 @@ private static boolean tryAddStringBuilderConsumer( isOptionalString(field.getFieldType()) ? "Optional.of(builder.toString())" : "builder.toString()"; - field.addMethod( + MethodDto method = createStringBuilderConsumer( - field.getFieldName(), field.getJavaDoc(), transform, builderType)); + field.getFieldName(), field.getJavaDoc(), transform, builderType); + setMethodAccessModifier(method, getMethodAccessModifier(context)); + field.addMethod(method); return true; } return false; @@ -511,23 +535,27 @@ private static boolean tryAddListConsumer( map2TypeName(ArrayListBuilderWithElementBuilders.class), elementType, elementBuilderType.get()); - field.addMethod( + MethodDto method = createFieldConsumerWithElementBuilders( field.getFieldName(), field.getJavaDoc(), collectionBuilderType, elementBuilderType.get(), - builderType)); + builderType); + setMethodAccessModifier(method, getMethodAccessModifier(context)); + field.addMethod(method); } else if (context.getBuilderConfigurationForElement().shouldUseArrayListBuilder()) { // Regular ArrayListBuilder if enabled TypeName collectionBuilderType = map2TypeName(ArrayListBuilder.class); - field.addMethod( + MethodDto method = createFieldConsumerWithBuilder( field.getFieldName(), field.getJavaDoc(), collectionBuilderType, elementType, - builderType)); + builderType); + setMethodAccessModifier(method, getMethodAccessModifier(context)); + field.addMethod(method); } else { return false; } @@ -555,6 +583,7 @@ private static boolean tryAddMapConsumer( MethodDto mapConsumerWithBuilder = BuilderDefinitionCreator.createFieldConsumerWithBuilder( field.getFieldName(), field.getJavaDoc(), builderTargetTypeName, builderType); + setMethodAccessModifier(mapConsumerWithBuilder, getMethodAccessModifier(context)); field.addMethod(mapConsumerWithBuilder); return true; } @@ -590,23 +619,27 @@ private static boolean tryAddSetConsumer( map2TypeName(HashSetBuilderWithElementBuilders.class), elementType, elementBuilderType.get()); - field.addMethod( + MethodDto method = createFieldConsumerWithElementBuilders( field.getFieldName(), field.getJavaDoc(), collectionBuilderType, elementBuilderType.get(), - builderType)); + builderType); + setMethodAccessModifier(method, getMethodAccessModifier(context)); + field.addMethod(method); } else if (context.getBuilderConfigurationForElement().shouldUseHashSetBuilder()) { // Regular HashSetBuilder if enabled TypeName collectionBuilderType = map2TypeName(HashSetBuilder.class); - field.addMethod( + MethodDto method = createFieldConsumerWithBuilder( field.getFieldName(), field.getJavaDoc(), collectionBuilderType, elementType, - builderType)); + builderType); + setMethodAccessModifier(method, getMethodAccessModifier(context)); + field.addMethod(method); } else { return false; } @@ -629,9 +662,11 @@ private static void addSupplierMethodsForField( // For all fields including Optional, use the real field type for suppliers String fieldName = field.getFieldNameEstimated(); String fieldNameInBuilder = field.getFieldName(); - field.addMethod( + MethodDto method = createFieldSupplier( - fieldName, fieldNameInBuilder, field.getJavaDoc(), field.getFieldType(), builderType)); + fieldName, fieldNameInBuilder, field.getJavaDoc(), field.getFieldType(), builderType); + setMethodAccessModifier(method, getMethodAccessModifier(context)); + field.addMethod(method); } private static Optional createFieldFromSetter( @@ -822,9 +857,11 @@ private static Optional createFieldDto( } // Add basic setter method with annotations - use ORIGINAL field name for method name - field.addMethod( + MethodDto method = createFieldSetterWithTransform( - fieldName, fieldNameInBuilder, javaDoc, null, fieldType, annotations, builderType)); + fieldName, fieldNameInBuilder, javaDoc, null, fieldType, annotations, builderType); + setMethodAccessModifier(method, getMethodAccessModifier(context)); + field.addMethod(method); // Add consumer/supplier/helper methods - use ORIGINAL field name for method names addConsumerMethodsForField(field, param, fieldTypeElement, builderType, context); @@ -882,7 +919,7 @@ private static MethodDto createFieldSetterWithTransform( methodDto.setMethodName(fieldName); methodDto.setReturnType(builderType); methodDto.addParameter(parameter); - methodDto.setModifier(Modifier.PUBLIC); + // Modifier is controlled by configuration, not set here String params; if (StringUtils.isBlank(transform)) { params = parameter.getParameterName(); @@ -921,7 +958,7 @@ private static MethodDto createFieldConsumer( methodDto.setMethodName(fieldName); methodDto.setReturnType(builderType); methodDto.addParameter(parameter); - methodDto.setModifier(Modifier.PUBLIC); + // Modifier is controlled by configuration, not set here methodDto.setCode( """ $helperType:T consumer = this.$fieldName:N.isSet() ? this.$fieldName:N.value() : new $helperType:T(); @@ -956,7 +993,7 @@ private static MethodDto createStringBuilderConsumer( MethodDto methodDto = new MethodDto(); methodDto.setMethodName(fieldName); methodDto.addParameter(parameter); - methodDto.setModifier(Modifier.PUBLIC); + // Modifier is controlled by configuration, not set here methodDto.setCode( """ StringBuilder builder = new StringBuilder(); @@ -1056,7 +1093,7 @@ private static MethodDto createFieldConsumerWithBuilder( methodDto.setMethodName(fieldName); methodDto.setReturnType(returnBuilderType); methodDto.addParameter(parameter); - methodDto.setModifier(Modifier.PUBLIC); + // Modifier is controlled by configuration, not set here methodDto.setCode( """ $helperType:T builder = this.$fieldName:N.isSet() ? new $helperType:T(%s) : new $helperType:T(%s); @@ -1096,7 +1133,7 @@ private static MethodDto createFieldSupplier( methodDto.setMethodName(fieldName); methodDto.setReturnType(builderType); methodDto.addParameter(parameter); - methodDto.setModifier(Modifier.PUBLIC); + // Modifier is controlled by configuration, not set here methodDto.setCode( """ this.$fieldName:N = $builderFieldWrapper:T.changedValue($dtoMethodParam:N.get()); @@ -1141,7 +1178,7 @@ private static MethodDto createStringFormatMethodWithTransform( methodDto.setReturnType(builderType); methodDto.addParameter(formatParam); methodDto.addParameter(argsParam); - methodDto.setModifier(Modifier.PUBLIC); + // Modifier is controlled by configuration, not set here methodDto.setCode( """ this.$fieldName:N = $builderFieldWrapper:T.changedValue($transform:N); @@ -1191,7 +1228,7 @@ private static MethodDto createFieldSetterForArrayFromList( methodDto.setMethodName(fieldName); methodDto.setReturnType(builderType); methodDto.addParameter(parameter); - methodDto.setModifier(Modifier.PUBLIC); + // Modifier is controlled by configuration, not set here methodDto.setCode( """ this.$fieldName:N = $builderFieldWrapper:T.changedValue($dtoMethodParams:N.toArray(new $elementType:T[0])); @@ -1235,7 +1272,7 @@ private static MethodDto createFieldConsumerWithArrayBuilder( methodDto.setMethodName(fieldName); methodDto.setReturnType(returnBuilderType); methodDto.addParameter(parameter); - methodDto.setModifier(Modifier.PUBLIC); + // Modifier is controlled by configuration, not set here methodDto.setCode( """ @@ -1490,4 +1527,27 @@ private static MethodDto createWithBuilderMethod(BuilderDefinitionDto builderDef return method; } + + /** + * Gets the method access modifier from the builder configuration. + * + * @param context the processing context + * @return the Modifier for method access, or null for package-private + */ + private static Modifier getMethodAccessModifier(ProcessingContext context) { + return JavapoetMapper.map2Modifier( + context.getBuilderConfigurationForElement().getMethodAccess()); + } + + /** + * Sets the access modifier on a MethodDto if the modifier is not null. + * + * @param method the MethodDto to update + * @param modifier the access modifier to set, or null for package-private + */ + private static void setMethodAccessModifier(MethodDto method, Modifier modifier) { + if (modifier != null) { + method.setModifier(modifier); + } + } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java index 2e2d44b3..dc18170b 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java @@ -117,7 +117,7 @@ public AccessModifier readAccessModifier(CompilerArgumentsEnum argument) { return AccessModifier.PUBLIC; } else if (Strings.CI.equals(value, "private")) { return AccessModifier.PRIVATE; - } else if (Strings.CI.equals(value, "package-private")) { + } else if (Strings.CI.equalsAny(value, "package-private", "package_private")) { return AccessModifier.PACKAGE_PRIVATE; } else if (Strings.CI.equals(value, "protected")) { return AccessModifier.PROTECTED; diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java index 078f5203..e9e429cb 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java @@ -106,14 +106,21 @@ public void generateBuilder(BuilderDefinitionDto builderDef) throws BuilderExcep .addTypeVariables(map2TypeVariables(builderDef.getGenerics())) .addJavadoc(createJavadocForClass(dtoBaseClass)); + // Set builder class access level + Modifier builderAccessModifier = map2Modifier(builderDef.getConfiguration().getBuilderAccess()); + if (builderAccessModifier != null) { + classBuilder.addModifiers(builderAccessModifier); + } + // Conditionally add IBuilderBase interface if (builderDef.getConfiguration().shouldImplementBuilderBase()) { classBuilder.addSuperinterface(createInterfaceBuilderBase(dtoTypeName)); } - // Adding Constructors for builder + // Get access modifiers from configuration Modifier constructorAccessModifier = map2Modifier(builderDef.getConfiguration().getBuilderConstructorAccess()); + Modifier methodAccessModifier = map2Modifier(builderDef.getConfiguration().getMethodAccess()); classBuilder.addMethod( createConstructorWithInstance( dtoBaseClass, @@ -170,15 +177,21 @@ public void generateBuilder(BuilderDefinitionDto builderDef) throws BuilderExcep builderDef.getConstructorFieldsForBuilder(), builderDef.getSetterFieldsForBuilder(), builderDef.getGenerics(), - builderDef.getConfiguration().shouldImplementBuilderBase())); + builderDef.getConfiguration().shouldImplementBuilderBase(), + methodAccessModifier)); classBuilder.addMethod( createMethodStaticCreate( - builderBaseClass, builderTypeName, dtoBaseClass, builderDef.getGenerics())); + builderBaseClass, + builderTypeName, + dtoBaseClass, + builderDef.getGenerics(), + methodAccessModifier)); // Add conditional methods only if enabled in configuration if (builderDef.getConfiguration().shouldGenerateConditionalLogic()) { - classBuilder.addMethod(createMethodConditional(builderTypeName)); - classBuilder.addMethod(createMethodConditionalPositiveOnly(builderTypeName)); + classBuilder.addMethod(createMethodConditional(builderTypeName, methodAccessModifier)); + classBuilder.addMethod( + createMethodConditionalPositiveOnly(builderTypeName, methodAccessModifier)); } // Adding nested types (e.g., With interface) @@ -382,9 +395,12 @@ private MethodSpec createMethodBuild( List constructorFields, List setterFields, List generics, - boolean implementsBuilderBase) { - MethodSpec.Builder mb = - MethodSpec.methodBuilder("build").addModifiers(PUBLIC).returns(returnType); + boolean implementsBuilderBase, + Modifier methodAccessModifier) { + MethodSpec.Builder mb = MethodSpec.methodBuilder("build").returns(returnType); + if (methodAccessModifier != null) { + mb.addModifiers(methodAccessModifier); + } // Only add @Override annotation if implementing IBuilderBase interface if (implementsBuilderBase) { @@ -453,21 +469,25 @@ private MethodSpec createMethodBuild( } private MethodSpec createMethodStaticCreate( - com.palantir.javapoet.ClassName builderBaseClass, + ClassName builderBaseClass, com.palantir.javapoet.TypeName builderType, - com.palantir.javapoet.ClassName dtoBaseClass, - List generics) { + ClassName dtoBaseClass, + List generics, + Modifier methodAccessModifier) { MethodSpec.Builder methodBuilder = - MethodSpec.methodBuilder(METHOD_NAME_CREATE) - .addModifiers(STATIC, PUBLIC) - .addJavadoc( - """ + MethodSpec.methodBuilder(METHOD_NAME_CREATE).addModifiers(STATIC); + if (methodAccessModifier != null) { + methodBuilder.addModifiers(methodAccessModifier); + } + + methodBuilder.addJavadoc( + """ Creating a new builder for {@code $1N.$2T}. @return builder for {@code $1N.$2T} """, - dtoBaseClass.packageName(), - dtoBaseClass); + dtoBaseClass.packageName(), + dtoBaseClass); if (generics.isEmpty()) { methodBuilder.returns(builderBaseClass).addCode("return new $1T();\n", builderBaseClass); } else { @@ -479,10 +499,14 @@ private MethodSpec createMethodStaticCreate( return methodBuilder.build(); } - private MethodSpec createMethodConditional(com.palantir.javapoet.TypeName builderType) { - return MethodSpec.methodBuilder("conditional") - .addModifiers(PUBLIC) - .returns(builderType) + private MethodSpec createMethodConditional( + com.palantir.javapoet.TypeName builderType, Modifier methodAccessModifier) { + MethodSpec.Builder mb = MethodSpec.methodBuilder("conditional"); + if (methodAccessModifier != null) { + mb.addModifiers(methodAccessModifier); + } + + mb.returns(builderType) .addParameter(ClassName.get(java.util.function.BooleanSupplier.class), "condition") .addParameter( ParameterizedTypeName.get( @@ -494,7 +518,7 @@ private MethodSpec createMethodConditional(com.palantir.javapoet.TypeName builde "falseCase") .addJavadoc( """ - Conditionally applies builder modifications based on a condition. + Conditionally applies builder modifications based on a condition evaluation. @param condition the condition to evaluate @param trueCase the consumer to apply if condition is true @@ -509,15 +533,18 @@ private MethodSpec createMethodConditional(com.palantir.javapoet.TypeName builde falseCase.accept(this); } return this; - """) - .build(); + """); + return mb.build(); } private MethodSpec createMethodConditionalPositiveOnly( - com.palantir.javapoet.TypeName builderType) { - return MethodSpec.methodBuilder("conditional") - .addModifiers(PUBLIC) - .returns(builderType) + com.palantir.javapoet.TypeName builderType, Modifier methodAccessModifier) { + MethodSpec.Builder mb = MethodSpec.methodBuilder("conditional"); + if (methodAccessModifier != null) { + mb.addModifiers(methodAccessModifier); + } + + mb.returns(builderType) .addParameter(ClassName.get(java.util.function.BooleanSupplier.class), "condition") .addParameter( ParameterizedTypeName.get( @@ -531,19 +558,12 @@ private MethodSpec createMethodConditionalPositiveOnly( @param yesCondition the consumer to apply if condition is true @return this builder instance """) - .addCode("return conditional(condition, yesCondition, null);\n") - .build(); + .addCode("return conditional(condition, yesCondition, null);\n"); + return mb.build(); } - /** - * Creates a TypeSpec for a nested type (e.g., With interface). - * - * @param nestedType the nested type definition - * @return the TypeSpec for the nested type - */ private TypeSpec createNestedType(NestedTypeDto nestedType) { TypeSpec.Builder typeBuilder; - boolean isInterface = nestedType.getKind() == NestedTypeDto.NestedTypeKind.INTERFACE; if (isInterface) { typeBuilder = TypeSpec.interfaceBuilder(nestedType.getTypeName()); @@ -559,9 +579,8 @@ private TypeSpec createNestedType(NestedTypeDto nestedType) { typeBuilder.addJavadoc(nestedType.getJavadoc()); } - // Add methods to the nested type - for (MethodDto method : nestedType.getMethods()) { - MethodSpec methodSpec = createNestedTypeMethod(method, isInterface); + for (MethodDto methodDto : nestedType.getMethods()) { + MethodSpec methodSpec = createNestedTypeMethod(methodDto, isInterface); typeBuilder.addMethod(methodSpec); } @@ -569,11 +588,12 @@ private TypeSpec createNestedType(NestedTypeDto nestedType) { } /** - * Creates a MethodSpec for a method of a nested type (e.g., With interface). + * Creates a method specification from a MethodDto for nested types (e.g., With interface + * methods). * - * @param methodDto the method to create - * @param isInterface whether the nested type is an interface - * @return the MethodSpec + * @param methodDto the method definition + * @param isInterface whether the containing type is an interface + * @return the generated MethodSpec */ private MethodSpec createNestedTypeMethod(MethodDto methodDto, boolean isInterface) { MethodSpec.Builder methodBuilder = @@ -587,23 +607,17 @@ private MethodSpec createNestedTypeMethod(MethodDto methodDto, boolean isInterfa methodBuilder.addParameter(createParameter(paramDto)); } - // Add modifiers if defined - methodDto.getModifier().ifPresent(methodBuilder::addModifiers); - - // Add Javadoc if (methodDto.getJavadoc() != null) { methodBuilder.addJavadoc(methodDto.getJavadoc()); } - // Add method body if present - MethodCodeDto codeDto = methodDto.getMethodCodeDto(); - if (codeDto != null) { - // Add default modifier for interface methods with implementation + // Add code only if method has implementation (even for interfaces with default methods) + if (methodDto.getMethodCodeDto() != null) { if (isInterface) { methodBuilder.addModifiers(javax.lang.model.element.Modifier.DEFAULT); } - methodBuilder.addCode(map2CodeBlock(codeDto)); + methodBuilder.addCode(map2CodeBlock(methodDto.getMethodCodeDto())); } return methodBuilder.build(); @@ -612,6 +626,8 @@ private MethodSpec createNestedTypeMethod(MethodDto methodDto, boolean isInterfa private MethodSpec createMethod(MethodDto methodDto, com.palantir.javapoet.TypeName returnType) { MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(methodDto.getMethodName()).returns(returnType); + + // Use modifier from MethodDto if present methodDto.getModifier().ifPresent(methodBuilder::addModifiers); // Use javadoc from MethodDto if available diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java index b8ab1599..8f6a9f86 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java @@ -175,7 +175,9 @@ public Address() {} "-Asimplebuilder.generateFieldConsumer=false", "-Asimplebuilder.generateBuilderProvider=false", "-Asimplebuilder.generateConditionalHelper=false", + "-Asimplebuilder.builderAccess=PACKAGE_PRIVATE", "-Asimplebuilder.builderConstructorAccess=PRIVATE", + "-Asimplebuilder.methodAccess=PACKAGE_PRIVATE", "-Asimplebuilder.generateVarArgsHelpers=false", "-Asimplebuilder.generateStringFormatHelpers=false", "-Asimplebuilder.generateUnboxedOptional=false", @@ -261,40 +263,60 @@ public Address() {} ProcessorAsserts.assertNotContaining( generatedCode, "implements IBuilderBase", "@Override public MinimalDto build()"); + // With builderAccess=PACKAGE_PRIVATE, builder class should NOT have public modifier + ProcessorAsserts.assertNotContaining(generatedCode, "public class MinimalDtoBuilder"); + + // But package-private class should exist + ProcessorAsserts.assertContaining(generatedCode, "class MinimalDtoBuilder"); + + // With methodAccess=PACKAGE_PRIVATE, methods should NOT have public modifier + ProcessorAsserts.assertNotContaining( + generatedCode, + "public MinimalDtoBuilder name(String name)", + "public MinimalDto build()", + "public static MinimalDtoBuilder create()"); + + // But package-private methods should exist + ProcessorAsserts.assertContaining( + generatedCode, + "MinimalDtoBuilder name(String name)", + "MinimalDto build()", + "static MinimalDtoBuilder create()"); + // With usingArrayListBuilder=false AND generateBuilderProvider=false, NO ArrayListBuilder // should be used ProcessorAsserts.assertNotContaining( generatedCode, - "public MinimalDtoBuilder items(Consumer> itemsBuilderConsumer)"); + "MinimalDtoBuilder items(Consumer> itemsBuilderConsumer)"); // With usingHashSetBuilder=false AND generateBuilderProvider=false, NO HashSetBuilder should be // used ProcessorAsserts.assertNotContaining( generatedCode, - "public MinimalDtoBuilder tags(Consumer> tagsBuilderConsumer)"); + "MinimalDtoBuilder tags(Consumer> tagsBuilderConsumer)"); // With usingHashMapBuilder=false AND generateBuilderProvider=false, NO HashMapBuilder should be // used ProcessorAsserts.assertNotContaining( generatedCode, - "public MinimalDtoBuilder properties(Consumer> propertiesBuilderConsumer)"); + "MinimalDtoBuilder properties(Consumer> propertiesBuilderConsumer)"); - // Still generates: basic setters and build method (without @Override when interface not - // implemented) + // Still generates: basic setters and build method (package-private methods, private + // constructors) ProcessorAsserts.assertContaining( generatedCode, "class MinimalDtoBuilder", "private MinimalDtoBuilder()", "private MinimalDtoBuilder(MinimalDto instance)", - "public MinimalDtoBuilder name(String name)", - "public MinimalDtoBuilder items(List items)", - "public MinimalDtoBuilder properties(Map properties)", - "public MinimalDtoBuilder description(Optional description)", - "public MinimalDtoBuilder tags(Set tags)", - "public MinimalDtoBuilder nested(NestedDto nested)", - "public MinimalDtoBuilder address(Address address)", - "public MinimalDto build()", - "public static MinimalDtoBuilder create()"); + "MinimalDtoBuilder name(String name)", + "MinimalDtoBuilder items(List items)", + "MinimalDtoBuilder properties(Map properties)", + "MinimalDtoBuilder description(Optional description)", + "MinimalDtoBuilder tags(Set tags)", + "MinimalDtoBuilder nested(NestedDto nested)", + "MinimalDtoBuilder address(Address address)", + "MinimalDto build()", + "static MinimalDtoBuilder create()"); } /** From 0d5b0cb4bb2b91864bfd11da1908a55cc98bc40f Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 2 Nov 2025 01:57:12 +0100 Subject: [PATCH 32/63] Adding a new configuration: builderSuffix --- .../core/annotations/SimpleBuilder.java | 17 ++ .../processor/dtos/BuilderConfiguration.java | 27 ++- .../enums/CompilerArgumentsEnum.java | 4 + .../util/BuilderConfigurationReader.java | 2 + .../util/BuilderDefinitionCreator.java | 125 +++++++++---- .../util/CompilerArgumentsReader.java | 1 + .../ConfigurationProcessingTest.java | 176 +++++++++++++----- 7 files changed, 272 insertions(+), 80 deletions(-) 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 f36efb6c..e7c0ae7c 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 @@ -329,6 +329,23 @@ * Default: ENABLED Compiler option: -Asimplebuilder.generateWithInterface */ OptionState generateWithInterface() default OptionState.UNSET; + + // === Naming === + /** + * Suffix to append to the DTO name to generate the builder class name.
    + * For example, with suffix "Builder", a DTO named "PersonDto" will generate "PersonDtoBuilder". + *
    + * Default: "Builder" Compiler option: -Asimplebuilder.builderSuffix + */ + String builderSuffix() default "Builder"; + + /** + * Suffix to append to setter method names in the generated builder.
    + * For example, with suffix "with", a field named "name" will generate "withName()".
    + * When a suffix is set, the field name is capitalized after the suffix.
    + * Default: "" (empty - no suffix) Compiler option: -Asimplebuilder.setterSuffix + */ + String setterSuffix() default ""; } /** diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java index 02241a56..2a084df1 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java @@ -58,6 +58,7 @@ * @param usingBuilderImplementationAnnotation Use BuilderImplementation annotation * @param implementsBuilderBase Implement IBuilderBase interface * @param generateWithInterface Generate With interface + * @param builderSuffix Suffix for builder class name */ public record BuilderConfiguration( OptionState generateFieldSupplier, @@ -78,7 +79,8 @@ public record BuilderConfiguration( OptionState usingGeneratedAnnotation, OptionState usingBuilderImplementationAnnotation, OptionState implementsBuilderBase, - OptionState generateWithInterface) { + OptionState generateWithInterface, + String builderSuffix, public static final BuilderConfiguration DEFAULT = builder() @@ -101,6 +103,7 @@ public record BuilderConfiguration( .usingBuilderImplementationAnnotation(ENABLED) .implementsBuilderBase(ENABLED) .generateWithInterface(ENABLED) + .builderSuffix("Builder") .build(); // === Convenience accessors with 'is' prefix for boolean properties === @@ -181,6 +184,10 @@ public AccessModifier getMethodAccess() { return methodAccess; } + public String getBuilderSuffix() { + return builderSuffix; + } + /** * Merges this configuration with another configuration. * @@ -271,6 +278,10 @@ public BuilderConfiguration merge(BuilderConfiguration other) { other.generateWithInterface != UNSET ? other.generateWithInterface : this.generateWithInterface) + .builderSuffix( + other.builderSuffix != null && !other.builderSuffix.isEmpty() + ? other.builderSuffix + : this.builderSuffix) .build(); } @@ -319,6 +330,9 @@ public String toString() { if (generateWithInterface != UNSET) { builder.append("generateWithInterface", generateWithInterface); } + if (builderSuffix != null && !builderSuffix.equals("Builder")) { + builder.append("builderSuffix", builderSuffix); + } return builder.toString(); } @@ -357,6 +371,9 @@ public static class Builder { private OptionState implementsBuilderBase = OptionState.UNSET; private OptionState generateWithInterface = OptionState.UNSET; + // === Naming === + private String builderSuffix = "Builder"; + // === Setters === public Builder generateSupplier(OptionState value) { this.generateFieldSupplier = value; @@ -548,6 +565,11 @@ public Builder methodAccess(String value) { return this; } + public Builder builderSuffix(String value) { + this.builderSuffix = value; + return this; + } + public BuilderConfiguration build() { return new BuilderConfiguration( generateFieldSupplier, @@ -568,7 +590,8 @@ public BuilderConfiguration build() { usingGeneratedAnnotation, usingBuilderImplementationAnnotation, implementsBuilderBase, - generateWithInterface); + generateWithInterface, + builderSuffix, } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java index be60d380..59092632 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java @@ -101,6 +101,10 @@ public enum CompilerArgumentsEnum { /** Option for With interface generation. */ GENERATE_WITH_INTERFACE("generateWithInterface"), + // === Naming === + /** Option for builder class name suffix. */ + BUILDER_SUFFIX("builderSuffix"), + // === Logging === /** Option for verbose logging output. */ VERBOSE("verbose"); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java index 98e14ddc..5142c49e 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java @@ -94,6 +94,7 @@ public BuilderConfiguration readFromOptions(Element element) { .usingBuilderImplementationAnnotation(options.usingBuilderImplementationAnnotation()) .implementsBuilderBase(options.implementsBuilderBase()) .generateWithInterface(options.generateWithInterface()) + .builderSuffix(options.builderSuffix()) .build(); } @@ -149,6 +150,7 @@ public BuilderConfiguration readFromTemplate(Element element) { .usingBuilderImplementationAnnotation(options.usingBuilderImplementationAnnotation()) .implementsBuilderBase(options.implementsBuilderBase()) .generateWithInterface(options.generateWithInterface()) + .builderSuffix(options.builderSuffix()) .build(); } } catch (ClassNotFoundException e) { diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java index 50348d19..ea1e4d09 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java @@ -120,12 +120,13 @@ private static BuilderDefinitionDto initializeBuilderDefinition( BuilderDefinitionDto result = new BuilderDefinitionDto(); String packageName = context.getPackageName(annotatedType); String simpleClassName = annotatedType.getSimpleName().toString(); - result.setBuilderTypeName(new TypeName(packageName, simpleClassName + BUILDER_SUFFIX)); + String builderSuffix = context.getBuilderConfigurationForElement().getBuilderSuffix(); + result.setBuilderTypeName(new TypeName(packageName, simpleClassName + builderSuffix)); result.setBuildingTargetTypeName(new TypeName(packageName, simpleClassName)); result.setConfiguration(context.getBuilderConfigurationForElement()); context.debug( - "Builder will be generated as: %s.%s", packageName, simpleClassName + BUILDER_SUFFIX); + "Builder will be generated as: %s.%s", packageName, simpleClassName + builderSuffix); // Extract generics from the annotated type via mapper (stream-based) JavaLangMapper.map2GenericParameterDtos(annotatedType, context).forEach(result::addGeneric); @@ -351,6 +352,8 @@ private static void addAdditionalHelperMethodsForField( "Set.of(%s)", new TypeNameArray(innerTypes.get(0), true), builderType); + builderType, + context); setMethodAccessModifier(method, methodAccessModifier); field.addMethod(method); } @@ -369,7 +372,8 @@ private static void addAdditionalHelperMethodsForField( fieldJavaDoc, "Map.ofEntries(%s)", mapEntryType, - builderType); + builderType, + context); setMethodAccessModifier(method, methodAccessModifier); field.addMethod(method); } @@ -386,7 +390,8 @@ private static void addAdditionalHelperMethodsForField( fieldJavaDoc, "Optional.ofNullable(%s)", innerTypes.get(0), - builderType); + builderType, + context); setMethodAccessModifier(method, methodAccessModifier); field.addMethod(method); } @@ -889,7 +894,14 @@ private static MethodDto createFieldSetterWithTransform( TypeName fieldType, TypeName builderType) { return createFieldSetterWithTransform( - fieldName, fieldNameInBuilder, fieldJavadoc, transform, fieldType, List.of(), builderType); + fieldName, + fieldNameInBuilder, + fieldJavadoc, + transform, + fieldType, + List.of(), + builderType, + context); } /** @@ -909,14 +921,15 @@ private static MethodDto createFieldSetterWithTransform( String transform, TypeName fieldType, List annotations, - TypeName builderType) { + TypeName builderType, + ProcessingContext context) { MethodParameterDto parameter = new MethodParameterDto(); parameter.setParameterName(fieldName); parameter.setParameterTypeName(fieldType); // Add annotations to the parameter annotations.forEach(parameter::addAnnotation); MethodDto methodDto = new MethodDto(); - methodDto.setMethodName(fieldName); + methodDto.setMethodName(applySetterSuffix(fieldName, context)); methodDto.setReturnType(builderType); methodDto.addParameter(parameter); // Modifier is controlled by configuration, not set here @@ -949,13 +962,18 @@ private static MethodDto createFieldSetterWithTransform( } private static MethodDto createFieldConsumer( - String fieldName, String fieldJavadoc, TypeName fieldType, TypeName builderType) { + String fieldName, + String fieldNameInBuilder, + String fieldJavadoc, + TypeName fieldType, + TypeName builderType, + ProcessingContext context) { TypeNameGeneric consumerType = new TypeNameGeneric(map2TypeName(Consumer.class), fieldType); MethodParameterDto parameter = new MethodParameterDto(); parameter.setParameterName(fieldName + SUFFIX_CONSUMER); parameter.setParameterTypeName(consumerType); MethodDto methodDto = new MethodDto(); - methodDto.setMethodName(fieldName); + methodDto.setMethodName(applySetterSuffix(fieldName, context)); methodDto.setReturnType(builderType); methodDto.addParameter(parameter); // Modifier is controlled by configuration, not set here @@ -966,7 +984,7 @@ private static MethodDto createFieldConsumer( this.$fieldName:N = $builderFieldWrapper:T.changedValue(consumer); return this; """); - methodDto.addArgument(ARG_FIELD_NAME, fieldName); + methodDto.addArgument(ARG_FIELD_NAME, fieldNameInBuilder); methodDto.addArgument(ARG_DTO_METHOD_PARAM, parameter.getParameterName()); methodDto.addArgument(ARG_HELPER_TYPE, fieldType); methodDto.addArgument(ARG_BUILDER_FIELD_WRAPPER, TRACKED_VALUE_TYPE); @@ -983,7 +1001,12 @@ private static MethodDto createFieldConsumer( } private static MethodDto createStringBuilderConsumer( - String fieldName, String fieldJavadoc, String transform, TypeName builderType) { + String fieldName, + String fieldNameInBuilder, + String fieldJavadoc, + String transform, + TypeName builderType, + ProcessingContext context) { TypeName stringBuilderType = map2TypeName(StringBuilder.class); TypeNameGeneric consumerType = new TypeNameGeneric(map2TypeName(Consumer.class), stringBuilderType); @@ -991,7 +1014,7 @@ private static MethodDto createStringBuilderConsumer( parameter.setParameterName(fieldName + "StringBuilderConsumer"); parameter.setParameterTypeName(consumerType); MethodDto methodDto = new MethodDto(); - methodDto.setMethodName(fieldName); + methodDto.setMethodName(applySetterSuffix(fieldName, context)); methodDto.addParameter(parameter); // Modifier is controlled by configuration, not set here methodDto.setCode( @@ -1001,7 +1024,7 @@ private static MethodDto createStringBuilderConsumer( this.$fieldName:N = $builderFieldWrapper:T.changedValue($transform:N); return this; """); - methodDto.addArgument(ARG_FIELD_NAME, fieldName); + methodDto.addArgument(ARG_FIELD_NAME, fieldNameInBuilder); methodDto.addArgument(ARG_DTO_METHOD_PARAM, parameter.getParameterName()); methodDto.addArgument("transform", transform); methodDto.addArgument(ARG_BUILDER_FIELD_WRAPPER, TRACKED_VALUE_TYPE); @@ -1023,18 +1046,20 @@ private static MethodDto createFieldConsumerWithBuilder( String fieldJavadoc, TypeName consumerBuilderType, TypeName builderTargetType, - TypeName returnBuilderType) { + TypeName returnBuilderType, + ProcessingContext context) { TypeNameGeneric builderTypeGeneric = new TypeNameGeneric(consumerBuilderType, builderTargetType); return BuilderDefinitionCreator.createFieldConsumerWithBuilder( - fieldName, fieldJavadoc, builderTypeGeneric, returnBuilderType); + fieldName, fieldJavadoc, builderTypeGeneric, returnBuilderType, context); } private static MethodDto createFieldConsumerWithBuilder( String fieldName, String fieldJavaDoc, TypeName consumerBuilderType, - TypeName returnBuilderType) { + TypeName returnBuilderType, + ProcessingContext context) { return createFieldConsumerWithBuilder( fieldName, fieldJavaDoc, @@ -1042,7 +1067,8 @@ private static MethodDto createFieldConsumerWithBuilder( "this.$fieldName:N.value()", "", Map.of(), - returnBuilderType); + returnBuilderType, + context); } /** @@ -1054,7 +1080,8 @@ private static MethodDto createFieldConsumerWithElementBuilders( String fieldJavaDoc, TypeName collectionBuilderType, TypeName elementBuilderType, - TypeName returnBuilderType) { + TypeName returnBuilderType, + ProcessingContext context) { return createFieldConsumerWithBuilder( fieldName, fieldJavaDoc, @@ -1062,7 +1089,8 @@ private static MethodDto createFieldConsumerWithElementBuilders( "this.$fieldName:N.value(), $elementBuilderType:T::create", "$elementBuilderType:T::create", Map.of("elementBuilderType", elementBuilderType), - returnBuilderType); + returnBuilderType, + context); } /** @@ -1075,6 +1103,7 @@ private static MethodDto createFieldConsumerWithElementBuilders( * @param constructorArgsEmpty constructor arguments when field is empty * @param additionalArguments additional template arguments to add to the method (must be TypeName * values) + * @param context processing context */ private static MethodDto createFieldConsumerWithBuilder( String fieldName, @@ -1083,14 +1112,15 @@ private static MethodDto createFieldConsumerWithBuilder( String constructorArgsWithValue, String additionalConstructorArgs, Map additionalArguments, - TypeName returnBuilderType) { + TypeName returnBuilderType, + ProcessingContext context) { TypeNameGeneric consumerType = new TypeNameGeneric(map2TypeName(Consumer.class), consumerBuilderType); MethodParameterDto parameter = new MethodParameterDto(); parameter.setParameterName(fieldName + BUILDER_SUFFIX + SUFFIX_CONSUMER); parameter.setParameterTypeName(consumerType); MethodDto methodDto = new MethodDto(); - methodDto.setMethodName(fieldName); + methodDto.setMethodName(applySetterSuffix(fieldName, context)); methodDto.setReturnType(returnBuilderType); methodDto.addParameter(parameter); // Modifier is controlled by configuration, not set here @@ -1124,13 +1154,14 @@ private static MethodDto createFieldSupplier( String fieldNameInBuilder, String fieldJavaDoc, TypeName fieldType, - TypeName builderType) { + TypeName builderType, + ProcessingContext context) { TypeNameGeneric supplierType = new TypeNameGeneric(map2TypeName(Supplier.class), fieldType); MethodParameterDto parameter = new MethodParameterDto(); parameter.setParameterName(fieldName + SUFFIX_SUPPLIER); parameter.setParameterTypeName(supplierType); MethodDto methodDto = new MethodDto(); - methodDto.setMethodName(fieldName); + methodDto.setMethodName(applySetterSuffix(fieldName, context)); methodDto.setReturnType(builderType); methodDto.addParameter(parameter); // Modifier is controlled by configuration, not set here @@ -1160,7 +1191,8 @@ private static MethodDto createStringFormatMethodWithTransform( String fieldJavadoc, String transform, List annotations, - TypeName builderType) { + TypeName builderType, + ProcessingContext context) { TypeName stringType = map2TypeName(String.class); MethodParameterDto formatParam = new MethodParameterDto(); @@ -1174,7 +1206,7 @@ private static MethodDto createStringFormatMethodWithTransform( argsParam.setParameterTypeName(new TypeNameArray(TypeName.of(Object.class), false)); MethodDto methodDto = new MethodDto(); - methodDto.setMethodName(fieldName); + methodDto.setMethodName(applySetterSuffix(fieldName, context)); methodDto.setReturnType(builderType); methodDto.addParameter(formatParam); methodDto.addParameter(argsParam); @@ -1219,13 +1251,14 @@ private static MethodDto createFieldSetterForArrayFromList( String fieldNameInBuilder, TypeName listType, TypeName elementType, - TypeName builderType) { + TypeName builderType, + ProcessingContext context) { MethodParameterDto parameter = new MethodParameterDto(); parameter.setParameterName(fieldName); parameter.setParameterTypeName(listType); MethodDto methodDto = new MethodDto(); - methodDto.setMethodName(fieldName); + methodDto.setMethodName(applySetterSuffix(fieldName, context)); methodDto.setReturnType(builderType); methodDto.addParameter(parameter); // Modifier is controlled by configuration, not set here @@ -1259,7 +1292,8 @@ private static MethodDto createFieldConsumerWithArrayBuilder( String fieldNameInBuilder, TypeName collectionBuilderType, TypeName elementType, - TypeName returnBuilderType) { + TypeName returnBuilderType, + ProcessingContext context) { TypeNameGeneric builderTypeGeneric = new TypeNameGeneric(collectionBuilderType, elementType); TypeNameGeneric consumerType = new TypeNameGeneric(map2TypeName(Consumer.class), builderTypeGeneric); @@ -1269,7 +1303,7 @@ private static MethodDto createFieldConsumerWithArrayBuilder( parameter.setParameterTypeName(consumerType); MethodDto methodDto = new MethodDto(); - methodDto.setMethodName(fieldName); + methodDto.setMethodName(applySetterSuffix(fieldName, context)); methodDto.setReturnType(returnBuilderType); methodDto.addParameter(parameter); // Modifier is controlled by configuration, not set here @@ -1386,10 +1420,11 @@ private static Optional resolveBuilderTypeFromTypeElement( String packageName = context.getPackageName(typeElement); String simpleClassName = typeElement.getSimpleName().toString(); + String builderSuffix = context.getBuilderConfigurationForElement().getBuilderSuffix(); context.debug( - " -> Found @SimpleBuilder on type %s.%s, will use %sBuilder", - packageName, simpleClassName, simpleClassName); - return Optional.of(new TypeName(packageName, simpleClassName + BUILDER_SUFFIX)); + " -> Found @SimpleBuilder on type %s.%s, will use %s%s", + packageName, simpleClassName, simpleClassName, builderSuffix); + return Optional.of(new TypeName(packageName, simpleClassName + builderSuffix)); } /** @@ -1528,6 +1563,32 @@ private static MethodDto createWithBuilderMethod(BuilderDefinitionDto builderDef return method; } + /** + * Applies the setter suffix to a field name to create the method name. + * + *

    If the suffix is empty, returns the fieldName as-is. If the suffix is set, capitalizes the + * first letter of fieldName and prepends the suffix. + * + *

    Examples: + * + *

      + *
    • fieldName="name", suffix="" → "name" + *
    • fieldName="name", suffix="with" → "withName" + *
    • fieldName="age", suffix="set" → "setAge" + *
    + * + * @param fieldName the field name + * @param context the processing context containing the configuration with the suffix + * @return the method name with suffix applied + */ + private static String applySetterSuffix(String fieldName, ProcessingContext context) { + String suffix = context.getBuilderConfigurationForElement().getSetterSuffix(); + if (suffix == null || suffix.isEmpty()) { + return fieldName; + } + return suffix + StringUtils.capitalize(fieldName); + } + /** * Gets the method access modifier from the builder configuration. * diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java index dc18170b..1df13687 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java @@ -168,6 +168,7 @@ public BuilderConfiguration readBuilderConfiguration() { readOptionState(CompilerArgumentsEnum.USING_BUILDER_IMPLEMENTATION_ANNOTATION)) .implementsBuilderBase(readOptionState(CompilerArgumentsEnum.IMPLEMENTS_BUILDER_BASE)) .generateWithInterface(readOptionState(CompilerArgumentsEnum.GENERATE_WITH_INTERFACE)) + .builderSuffix(readValue(CompilerArgumentsEnum.BUILDER_SUFFIX)) .build(); } } diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java index 8f6a9f86..361c847d 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java @@ -166,7 +166,7 @@ public Address() {} public void setAddress(Address address) { this.address = address; } """); - // When: Compile with ALL compiler arguments disabled + // When: Compile with ALL compiler arguments disabled and custom builder suffix Compilation compilation = Compiler.javac() .withProcessors(new BuilderProcessor()) @@ -189,68 +189,70 @@ public Address() {} "-Asimplebuilder.usingGeneratedAnnotation=false", "-Asimplebuilder.usingBuilderImplementationAnnotation=false", "-Asimplebuilder.implementsBuilderBase=false", - "-Asimplebuilder.generateWithInterface=false") + "-Asimplebuilder.generateWithInterface=false", + "-Asimplebuilder.builderSuffix=CustomBuilder") .compile(nestedDto, addressDto, source); // Then: Compilation should succeed assertThat(compilation).succeeded(); // And: Generated builder should contain only basic functionality - String generatedCode = ProcessorTestUtils.loadGeneratedSource(compilation, "MinimalDtoBuilder"); + String generatedCode = + ProcessorTestUtils.loadGeneratedSource(compilation, "MinimalDtoCustomBuilder"); // With generateFieldSupplier=false, NO supplier methods should be generated ProcessorAsserts.assertNotContaining( generatedCode, - "public MinimalDtoBuilder name(Supplier nameSupplier)", - "public MinimalDtoBuilder items(Supplier> itemsSupplier)", - "public MinimalDtoBuilder properties(Supplier> propertiesSupplier)", - "public MinimalDtoBuilder description(Supplier> descriptionSupplier)", - "public MinimalDtoBuilder tags(Supplier> tagsSupplier)", - "public MinimalDtoBuilder nested(Supplier nestedSupplier)", - "public MinimalDtoBuilder address(Supplier
    addressSupplier)"); + "public MinimalDtoCustomBuilder name(Supplier nameSupplier)", + "public MinimalDtoCustomBuilder items(Supplier> itemsSupplier)", + "public MinimalDtoCustomBuilder properties(Supplier> propertiesSupplier)", + "public MinimalDtoCustomBuilder description(Supplier> descriptionSupplier)", + "public MinimalDtoCustomBuilder tags(Supplier> tagsSupplier)", + "public MinimalDtoCustomBuilder nested(Supplier nestedSupplier)", + "public MinimalDtoCustomBuilder address(Supplier
    addressSupplier)"); // With generateFieldConsumer=false, NO field consumer methods should be generated // Field consumer = Consumer where T is a custom type with empty constructor ProcessorAsserts.assertNotContaining( generatedCode, - "public MinimalDtoBuilder address(Consumer
    addressConsumer)", - "public MinimalDtoBuilder address(Consumer
    addressConsumer)", - "public MinimalDtoBuilder nested(Consumer nestedConsumer)", - "public MinimalDtoBuilder items(Consumer> itemsConsumer)", - "public MinimalDtoBuilder tags(Consumer> tagsConsumer)", - "public MinimalDtoBuilder properties(Consumer> propertiesConsumer)"); + "public MinimalDtoCustomBuilder address(Consumer
    addressConsumer)", + "public MinimalDtoCustomBuilder address(Consumer
    addressConsumer)", + "public MinimalDtoCustomBuilder nested(Consumer nestedConsumer)", + "public MinimalDtoCustomBuilder items(Consumer> itemsConsumer)", + "public MinimalDtoCustomBuilder tags(Consumer> tagsConsumer)", + "public MinimalDtoCustomBuilder properties(Consumer> propertiesConsumer)"); // With generateBuilderProvider=false, NO builder consumer methods should be generated // Builder consumers include: StringBuilder, collection builders, nested DTO builders ProcessorAsserts.assertNotContaining( generatedCode, - "public MinimalDtoBuilder nested(Consumer nestedBuilderConsumer)", - "public MinimalDtoBuilder name(Consumer nameStringBuilderConsumer)", - "public MinimalDtoBuilder description(Consumer descriptionStringBuilderConsumer)"); + "public MinimalDtoCustomBuilder nested(Consumer nestedBuilderConsumer)", + "public MinimalDtoCustomBuilder name(Consumer nameStringBuilderConsumer)", + "public MinimalDtoCustomBuilder description(Consumer descriptionStringBuilderConsumer)"); // With generateConditionalHelper=false, NO conditional methods ProcessorAsserts.assertNotContaining( - generatedCode, "public MinimalDtoBuilder conditional(BooleanSupplier condition"); + generatedCode, "public MinimalDtoCustomBuilder conditional(BooleanSupplier condition"); // With generateVarArgsHelpers=false, NO VarArgs helpers should be generated ProcessorAsserts.assertNotContaining( generatedCode, - "public MinimalDtoBuilder items(String... items)", - "public MinimalDtoBuilder properties(Map.Entry... properties)", - "public MinimalDtoBuilder tags(String... tags)"); + "public MinimalDtoCustomBuilder items(String... items)", + "public MinimalDtoCustomBuilder properties(Map.Entry... properties)", + "public MinimalDtoCustomBuilder tags(String... tags)"); // With generateWithInterface=false, NO With interface ProcessorAsserts.assertNotContaining(generatedCode, "public interface With"); // With generateUnboxedOptional=false, NO unboxed optional methods should be generated ProcessorAsserts.assertNotContaining( - generatedCode, "public MinimalDtoBuilder description(String description)"); + generatedCode, "public MinimalDtoCustomBuilder description(String description)"); // With generateStringFormatHelpers=false, NO String format methods should be generated ProcessorAsserts.assertNotContaining( generatedCode, - "public MinimalDtoBuilder name(String format, Object... args)", - "public MinimalDtoBuilder description(String format, Object... args)"); + "public MinimalDtoCustomBuilder name(String format, Object... args)", + "public MinimalDtoCustomBuilder description(String format, Object... args)"); // With usingGeneratedAnnotation=false, NO @Generated annotation should be used ProcessorAsserts.assertNotContaining(generatedCode, "@Generated("); @@ -264,59 +266,59 @@ public Address() {} generatedCode, "implements IBuilderBase", "@Override public MinimalDto build()"); // With builderAccess=PACKAGE_PRIVATE, builder class should NOT have public modifier - ProcessorAsserts.assertNotContaining(generatedCode, "public class MinimalDtoBuilder"); + ProcessorAsserts.assertNotContaining(generatedCode, "public class MinimalDtoCustomBuilder"); // But package-private class should exist - ProcessorAsserts.assertContaining(generatedCode, "class MinimalDtoBuilder"); + ProcessorAsserts.assertContaining(generatedCode, "class MinimalDtoCustomBuilder"); // With methodAccess=PACKAGE_PRIVATE, methods should NOT have public modifier ProcessorAsserts.assertNotContaining( generatedCode, - "public MinimalDtoBuilder name(String name)", + "public MinimalDtoCustomBuilder name(String name)", "public MinimalDto build()", - "public static MinimalDtoBuilder create()"); + "public static MinimalDtoCustomBuilder create()"); // But package-private methods should exist ProcessorAsserts.assertContaining( generatedCode, - "MinimalDtoBuilder name(String name)", + "MinimalDtoCustomBuilder name(String name)", "MinimalDto build()", - "static MinimalDtoBuilder create()"); + "static MinimalDtoCustomBuilder create()"); // With usingArrayListBuilder=false AND generateBuilderProvider=false, NO ArrayListBuilder // should be used ProcessorAsserts.assertNotContaining( generatedCode, - "MinimalDtoBuilder items(Consumer> itemsBuilderConsumer)"); + "MinimalDtoCustomBuilder items(Consumer> itemsBuilderConsumer)"); // With usingHashSetBuilder=false AND generateBuilderProvider=false, NO HashSetBuilder should be // used ProcessorAsserts.assertNotContaining( generatedCode, - "MinimalDtoBuilder tags(Consumer> tagsBuilderConsumer)"); + "MinimalDtoCustomBuilder tags(Consumer> tagsBuilderConsumer)"); // With usingHashMapBuilder=false AND generateBuilderProvider=false, NO HashMapBuilder should be // used ProcessorAsserts.assertNotContaining( generatedCode, - "MinimalDtoBuilder properties(Consumer> propertiesBuilderConsumer)"); + "MinimalDtoCustomBuilder properties(Consumer> propertiesBuilderConsumer)"); // Still generates: basic setters and build method (package-private methods, private // constructors) ProcessorAsserts.assertContaining( generatedCode, - "class MinimalDtoBuilder", - "private MinimalDtoBuilder()", - "private MinimalDtoBuilder(MinimalDto instance)", - "MinimalDtoBuilder name(String name)", - "MinimalDtoBuilder items(List items)", - "MinimalDtoBuilder properties(Map properties)", - "MinimalDtoBuilder description(Optional description)", - "MinimalDtoBuilder tags(Set tags)", - "MinimalDtoBuilder nested(NestedDto nested)", - "MinimalDtoBuilder address(Address address)", + "class MinimalDtoCustomBuilder", + "private MinimalDtoCustomBuilder()", + "private MinimalDtoCustomBuilder(MinimalDto instance)", + "MinimalDtoCustomBuilder name(String name)", + "MinimalDtoCustomBuilder items(List items)", + "MinimalDtoCustomBuilder properties(Map properties)", + "MinimalDtoCustomBuilder description(Optional description)", + "MinimalDtoCustomBuilder tags(Set tags)", + "MinimalDtoCustomBuilder nested(NestedDto nested)", + "MinimalDtoCustomBuilder address(Address address)", "MinimalDto build()", - "static MinimalDtoBuilder create()"); + "static MinimalDtoCustomBuilder create()"); } /** @@ -448,4 +450,86 @@ void configurationMerge_Chain_ShouldApplyInOrder() { finalConfig.generateConditionalHelper(), "Default should remain when not overridden"); } + + /** + * Custom builder suffix with nested DTOs test. + * + *

    Verifies that when using a custom builderSuffix, the builder correctly recognizes nested DTO + * builders with the same custom suffix. + * + *

    For example, if PersonDto has an AddressDto field, and both use suffix "Factory", then + * PersonDtoFactory should have a method accepting Consumer<AddressDtoFactory>. + */ + @Test + void builderSuffix_WithNestedDto_ShouldRecognizeNestedBuilder() { + // Given: A nested DTO with @SimpleBuilder annotation + JavaFileObject nestedDto = + ProcessorTestUtils.simpleBuilderClass( + "com.example", + "AddressDto", + """ + private String street; + private String city; + + public String getStreet() { return street; } + public void setStreet(String street) { this.street = street; } + + public String getCity() { return city; } + public void setCity(String city) { this.city = city; } + """); + + // And: A parent DTO with @SimpleBuilder annotation that has the nested DTO as a field + JavaFileObject parentDto = + ProcessorTestUtils.simpleBuilderClass( + "com.example", + "PersonDto", + """ + private String name; + private AddressDto address; + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + + public AddressDto getAddress() { return address; } + public void setAddress(AddressDto address) { this.address = address; } + """); + + // When: Compile with custom builderSuffix + Compilation compilation = + Compiler.javac() + .withProcessors(new BuilderProcessor()) + .withOptions("-Asimplebuilder.builderSuffix=Factory") + .compile(nestedDto, parentDto); + + // Then: Compilation should succeed + assertThat(compilation).succeeded(); + + // And: Parent builder should be generated with custom suffix + String parentBuilderCode = + ProcessorTestUtils.loadGeneratedSource(compilation, "PersonDtoFactory"); + assertNotNull(parentBuilderCode, "PersonDtoFactory should be generated"); + + // And: Nested builder should be generated with custom suffix + String nestedBuilderCode = + ProcessorTestUtils.loadGeneratedSource(compilation, "AddressDtoFactory"); + assertNotNull(nestedBuilderCode, "AddressDtoFactory should be generated"); + + // And: Parent builder should reference nested builder with custom suffix + ProcessorAsserts.assertContaining( + parentBuilderCode, + "class PersonDtoFactory", + "PersonDtoFactory address(AddressDto address)", + "PersonDtoFactory address(Consumer addressBuilderConsumer)"); + + // And: Parent builder's create method should use custom suffix + ProcessorAsserts.assertContaining(parentBuilderCode, "static PersonDtoFactory create()"); + + // And: Nested builder should also use custom suffix in its methods + ProcessorAsserts.assertContaining( + nestedBuilderCode, + "class AddressDtoFactory", + "AddressDtoFactory street(String street)", + "AddressDtoFactory city(String city)", + "static AddressDtoFactory create()"); + } } From b40cf52b9dbb20c8d3f235e593c10d6f3fb609a8 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 2 Nov 2025 02:08:40 +0100 Subject: [PATCH 33/63] Adding a new configuration: setterSuffix --- .../processor/dtos/BuilderConfiguration.java | 21 +++ .../enums/CompilerArgumentsEnum.java | 3 + .../util/BuilderConfigurationReader.java | 2 + .../util/BuilderDefinitionCreator.java | 121 +++++++++++++----- .../util/CompilerArgumentsReader.java | 1 + .../ConfigurationProcessingTest.java | 78 +++++------ 6 files changed, 157 insertions(+), 69 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java index 2a084df1..61abd104 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java @@ -59,6 +59,7 @@ * @param implementsBuilderBase Implement IBuilderBase interface * @param generateWithInterface Generate With interface * @param builderSuffix Suffix for builder class name + * @param setterSuffix Suffix for setter method names */ public record BuilderConfiguration( OptionState generateFieldSupplier, @@ -81,6 +82,7 @@ public record BuilderConfiguration( OptionState implementsBuilderBase, OptionState generateWithInterface, String builderSuffix, + String setterSuffix) { public static final BuilderConfiguration DEFAULT = builder() @@ -104,6 +106,7 @@ public record BuilderConfiguration( .implementsBuilderBase(ENABLED) .generateWithInterface(ENABLED) .builderSuffix("Builder") + .setterSuffix("") .build(); // === Convenience accessors with 'is' prefix for boolean properties === @@ -188,6 +191,10 @@ public String getBuilderSuffix() { return builderSuffix; } + public String getSetterSuffix() { + return setterSuffix; + } + /** * Merges this configuration with another configuration. * @@ -282,6 +289,10 @@ public BuilderConfiguration merge(BuilderConfiguration other) { other.builderSuffix != null && !other.builderSuffix.isEmpty() ? other.builderSuffix : this.builderSuffix) + .setterSuffix( + other.setterSuffix != null && !other.setterSuffix.isEmpty() + ? other.setterSuffix + : this.setterSuffix) .build(); } @@ -333,6 +344,9 @@ public String toString() { if (builderSuffix != null && !builderSuffix.equals("Builder")) { builder.append("builderSuffix", builderSuffix); } + if (setterSuffix != null && !setterSuffix.isEmpty()) { + builder.append("setterSuffix", setterSuffix); + } return builder.toString(); } @@ -373,6 +387,7 @@ public static class Builder { // === Naming === private String builderSuffix = "Builder"; + private String setterSuffix = ""; // === Setters === public Builder generateSupplier(OptionState value) { @@ -570,6 +585,11 @@ public Builder builderSuffix(String value) { return this; } + public Builder setterSuffix(String value) { + this.setterSuffix = value; + return this; + } + public BuilderConfiguration build() { return new BuilderConfiguration( generateFieldSupplier, @@ -592,6 +612,7 @@ public BuilderConfiguration build() { implementsBuilderBase, generateWithInterface, builderSuffix, + setterSuffix); } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java index 59092632..449590e3 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java @@ -105,6 +105,9 @@ public enum CompilerArgumentsEnum { /** Option for builder class name suffix. */ BUILDER_SUFFIX("builderSuffix"), + /** Option for setter method name suffix. */ + SETTER_SUFFIX("setterSuffix"), + // === Logging === /** Option for verbose logging output. */ VERBOSE("verbose"); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java index 5142c49e..d18c6c1f 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java @@ -95,6 +95,7 @@ public BuilderConfiguration readFromOptions(Element element) { .implementsBuilderBase(options.implementsBuilderBase()) .generateWithInterface(options.generateWithInterface()) .builderSuffix(options.builderSuffix()) + .setterSuffix(options.setterSuffix()) .build(); } @@ -151,6 +152,7 @@ public BuilderConfiguration readFromTemplate(Element element) { .implementsBuilderBase(options.implementsBuilderBase()) .generateWithInterface(options.generateWithInterface()) .builderSuffix(options.builderSuffix()) + .setterSuffix(options.setterSuffix()) .build(); } } catch (ClassNotFoundException e) { diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java index ea1e4d09..028417b6 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java @@ -291,7 +291,8 @@ private static void addAdditionalHelperMethodsForField( fieldJavaDoc, "String.format(format, args)", annotations, - builderType); + builderType, + context); setMethodAccessModifier(method, methodAccessModifier); field.addMethod(method); } @@ -304,7 +305,7 @@ private static void addAdditionalHelperMethodsForField( String fieldName = field.getFieldNameEstimated(); MethodDto method1 = createFieldSetterForArrayFromList( - fieldName, fieldNameInBuilder, listType, elementType, builderType); + fieldName, fieldNameInBuilder, listType, elementType, builderType, context); setMethodAccessModifier(method1, methodAccessModifier); field.addMethod(method1); @@ -312,7 +313,12 @@ private static void addAdditionalHelperMethodsForField( TypeName collectionBuilderType = map2TypeName(ArrayListBuilder.class); MethodDto method2 = createFieldConsumerWithArrayBuilder( - fieldName, fieldNameInBuilder, collectionBuilderType, elementType, builderType); + fieldName, + fieldNameInBuilder, + collectionBuilderType, + elementType, + builderType, + context); setMethodAccessModifier(method2, methodAccessModifier); field.addMethod(method2); return; @@ -336,7 +342,8 @@ private static void addAdditionalHelperMethodsForField( fieldJavaDoc, "List.of(%s)", new TypeNameArray(innerTypes.get(0), false), - builderType); + builderType, + context); setMethodAccessModifier(method, methodAccessModifier); field.addMethod(method); } @@ -351,7 +358,6 @@ private static void addAdditionalHelperMethodsForField( fieldJavaDoc, "Set.of(%s)", new TypeNameArray(innerTypes.get(0), true), - builderType); builderType, context); setMethodAccessModifier(method, methodAccessModifier); @@ -407,7 +413,8 @@ private static void addAdditionalHelperMethodsForField( fieldJavaDoc, "Optional.of(String.format(format, args))", List.of(), - builderType); + builderType, + context); setMethodAccessModifier(method, methodAccessModifier); field.addMethod(method); } @@ -453,7 +460,12 @@ private static boolean tryAddBuilderConsumer( TypeName fieldBuilderType = fieldBuilderOpt.get(); MethodDto method = BuilderDefinitionCreator.createFieldConsumerWithBuilder( - field.getFieldName(), field.getJavaDoc(), fieldBuilderType, builderType); + field.getFieldName(), + field.getFieldName(), + field.getJavaDoc(), + fieldBuilderType, + builderType, + context); setMethodAccessModifier(method, getMethodAccessModifier(context)); field.addMethod(method); return true; @@ -479,7 +491,12 @@ && hasEmptyConstructor(fieldTypeElement, context)) { // Only generate a Consumer for concrete classes with an accessible empty constructor MethodDto method = createFieldConsumer( - field.getFieldName(), field.getJavaDoc(), field.getFieldType(), builderType); + field.getFieldName(), + field.getFieldName(), + field.getJavaDoc(), + field.getFieldType(), + builderType, + context); setMethodAccessModifier(method, getMethodAccessModifier(context)); field.addMethod(method); return true; @@ -501,7 +518,12 @@ private static boolean tryAddStringBuilderConsumer( : "builder.toString()"; MethodDto method = createStringBuilderConsumer( - field.getFieldName(), field.getJavaDoc(), transform, builderType); + field.getFieldName(), + field.getFieldName(), + field.getJavaDoc(), + transform, + builderType, + context); setMethodAccessModifier(method, getMethodAccessModifier(context)); field.addMethod(method); return true; @@ -542,11 +564,13 @@ private static boolean tryAddListConsumer( elementBuilderType.get()); MethodDto method = createFieldConsumerWithElementBuilders( + field.getFieldName(), field.getFieldName(), field.getJavaDoc(), collectionBuilderType, elementBuilderType.get(), - builderType); + builderType, + context); setMethodAccessModifier(method, getMethodAccessModifier(context)); field.addMethod(method); } else if (context.getBuilderConfigurationForElement().shouldUseArrayListBuilder()) { @@ -554,11 +578,13 @@ private static boolean tryAddListConsumer( TypeName collectionBuilderType = map2TypeName(ArrayListBuilder.class); MethodDto method = createFieldConsumerWithBuilder( + field.getFieldName(), field.getFieldName(), field.getJavaDoc(), collectionBuilderType, elementType, - builderType); + builderType, + context); setMethodAccessModifier(method, getMethodAccessModifier(context)); field.addMethod(method); } else { @@ -587,7 +613,12 @@ private static boolean tryAddMapConsumer( fieldTypeGeneric.getInnerTypeArguments().get(1)); MethodDto mapConsumerWithBuilder = BuilderDefinitionCreator.createFieldConsumerWithBuilder( - field.getFieldName(), field.getJavaDoc(), builderTargetTypeName, builderType); + field.getFieldName(), + field.getFieldName(), + field.getJavaDoc(), + builderTargetTypeName, + builderType, + context); setMethodAccessModifier(mapConsumerWithBuilder, getMethodAccessModifier(context)); field.addMethod(mapConsumerWithBuilder); return true; @@ -626,11 +657,13 @@ private static boolean tryAddSetConsumer( elementBuilderType.get()); MethodDto method = createFieldConsumerWithElementBuilders( + field.getFieldName(), field.getFieldName(), field.getJavaDoc(), collectionBuilderType, elementBuilderType.get(), - builderType); + builderType, + context); setMethodAccessModifier(method, getMethodAccessModifier(context)); field.addMethod(method); } else if (context.getBuilderConfigurationForElement().shouldUseHashSetBuilder()) { @@ -638,11 +671,13 @@ private static boolean tryAddSetConsumer( TypeName collectionBuilderType = map2TypeName(HashSetBuilder.class); MethodDto method = createFieldConsumerWithBuilder( + field.getFieldName(), field.getFieldName(), field.getJavaDoc(), collectionBuilderType, elementType, - builderType); + builderType, + context); setMethodAccessModifier(method, getMethodAccessModifier(context)); field.addMethod(method); } else { @@ -669,7 +704,12 @@ private static void addSupplierMethodsForField( String fieldNameInBuilder = field.getFieldName(); MethodDto method = createFieldSupplier( - fieldName, fieldNameInBuilder, field.getJavaDoc(), field.getFieldType(), builderType); + fieldName, + fieldNameInBuilder, + field.getJavaDoc(), + field.getFieldType(), + builderType, + context); setMethodAccessModifier(method, getMethodAccessModifier(context)); field.addMethod(method); } @@ -864,7 +904,14 @@ private static Optional createFieldDto( // Add basic setter method with annotations - use ORIGINAL field name for method name MethodDto method = createFieldSetterWithTransform( - fieldName, fieldNameInBuilder, javaDoc, null, fieldType, annotations, builderType); + fieldName, + fieldNameInBuilder, + javaDoc, + null, + fieldType, + annotations, + builderType, + context); setMethodAccessModifier(method, getMethodAccessModifier(context)); field.addMethod(method); @@ -892,7 +939,8 @@ private static MethodDto createFieldSetterWithTransform( String fieldJavadoc, String transform, TypeName fieldType, - TypeName builderType) { + TypeName builderType, + ProcessingContext context) { return createFieldSetterWithTransform( fieldName, fieldNameInBuilder, @@ -929,7 +977,7 @@ private static MethodDto createFieldSetterWithTransform( // Add annotations to the parameter annotations.forEach(parameter::addAnnotation); MethodDto methodDto = new MethodDto(); - methodDto.setMethodName(applySetterSuffix(fieldName, context)); + methodDto.setMethodName(generateSetterName(fieldName, context)); methodDto.setReturnType(builderType); methodDto.addParameter(parameter); // Modifier is controlled by configuration, not set here @@ -973,7 +1021,7 @@ private static MethodDto createFieldConsumer( parameter.setParameterName(fieldName + SUFFIX_CONSUMER); parameter.setParameterTypeName(consumerType); MethodDto methodDto = new MethodDto(); - methodDto.setMethodName(applySetterSuffix(fieldName, context)); + methodDto.setMethodName(generateSetterName(fieldName, context)); methodDto.setReturnType(builderType); methodDto.addParameter(parameter); // Modifier is controlled by configuration, not set here @@ -1014,7 +1062,7 @@ private static MethodDto createStringBuilderConsumer( parameter.setParameterName(fieldName + "StringBuilderConsumer"); parameter.setParameterTypeName(consumerType); MethodDto methodDto = new MethodDto(); - methodDto.setMethodName(applySetterSuffix(fieldName, context)); + methodDto.setMethodName(generateSetterName(fieldName, context)); methodDto.addParameter(parameter); // Modifier is controlled by configuration, not set here methodDto.setCode( @@ -1043,6 +1091,7 @@ private static MethodDto createStringBuilderConsumer( private static MethodDto createFieldConsumerWithBuilder( String fieldName, + String fieldNameInBuilder, String fieldJavadoc, TypeName consumerBuilderType, TypeName builderTargetType, @@ -1051,17 +1100,24 @@ private static MethodDto createFieldConsumerWithBuilder( TypeNameGeneric builderTypeGeneric = new TypeNameGeneric(consumerBuilderType, builderTargetType); return BuilderDefinitionCreator.createFieldConsumerWithBuilder( - fieldName, fieldJavadoc, builderTypeGeneric, returnBuilderType, context); + fieldName, + fieldNameInBuilder, + fieldJavadoc, + builderTypeGeneric, + returnBuilderType, + context); } private static MethodDto createFieldConsumerWithBuilder( String fieldName, + String fieldNameInBuilder, String fieldJavaDoc, TypeName consumerBuilderType, TypeName returnBuilderType, ProcessingContext context) { return createFieldConsumerWithBuilder( fieldName, + fieldNameInBuilder, fieldJavaDoc, consumerBuilderType, "this.$fieldName:N.value()", @@ -1077,6 +1133,7 @@ private static MethodDto createFieldConsumerWithBuilder( */ private static MethodDto createFieldConsumerWithElementBuilders( String fieldName, + String fieldNameInBuilder, String fieldJavaDoc, TypeName collectionBuilderType, TypeName elementBuilderType, @@ -1084,6 +1141,7 @@ private static MethodDto createFieldConsumerWithElementBuilders( ProcessingContext context) { return createFieldConsumerWithBuilder( fieldName, + fieldNameInBuilder, fieldJavaDoc, collectionBuilderType, "this.$fieldName:N.value(), $elementBuilderType:T::create", @@ -1107,6 +1165,7 @@ private static MethodDto createFieldConsumerWithElementBuilders( */ private static MethodDto createFieldConsumerWithBuilder( String fieldName, + String fieldNameInBuilder, String fieldJavaDoc, TypeName consumerBuilderType, String constructorArgsWithValue, @@ -1120,7 +1179,7 @@ private static MethodDto createFieldConsumerWithBuilder( parameter.setParameterName(fieldName + BUILDER_SUFFIX + SUFFIX_CONSUMER); parameter.setParameterTypeName(consumerType); MethodDto methodDto = new MethodDto(); - methodDto.setMethodName(applySetterSuffix(fieldName, context)); + methodDto.setMethodName(generateSetterName(fieldName, context)); methodDto.setReturnType(returnBuilderType); methodDto.addParameter(parameter); // Modifier is controlled by configuration, not set here @@ -1132,7 +1191,7 @@ private static MethodDto createFieldConsumerWithBuilder( return this; """ .formatted(constructorArgsWithValue, additionalConstructorArgs)); - methodDto.addArgument(ARG_FIELD_NAME, fieldName); + methodDto.addArgument(ARG_FIELD_NAME, fieldNameInBuilder); methodDto.addArgument(ARG_DTO_METHOD_PARAM, parameter.getParameterName()); methodDto.addArgument(ARG_HELPER_TYPE, consumerBuilderType); additionalArguments.forEach(methodDto::addArgument); @@ -1161,7 +1220,7 @@ private static MethodDto createFieldSupplier( parameter.setParameterName(fieldName + SUFFIX_SUPPLIER); parameter.setParameterTypeName(supplierType); MethodDto methodDto = new MethodDto(); - methodDto.setMethodName(applySetterSuffix(fieldName, context)); + methodDto.setMethodName(generateSetterName(fieldName, context)); methodDto.setReturnType(builderType); methodDto.addParameter(parameter); // Modifier is controlled by configuration, not set here @@ -1206,7 +1265,7 @@ private static MethodDto createStringFormatMethodWithTransform( argsParam.setParameterTypeName(new TypeNameArray(TypeName.of(Object.class), false)); MethodDto methodDto = new MethodDto(); - methodDto.setMethodName(applySetterSuffix(fieldName, context)); + methodDto.setMethodName(generateSetterName(fieldName, context)); methodDto.setReturnType(builderType); methodDto.addParameter(formatParam); methodDto.addParameter(argsParam); @@ -1258,7 +1317,7 @@ private static MethodDto createFieldSetterForArrayFromList( parameter.setParameterTypeName(listType); MethodDto methodDto = new MethodDto(); - methodDto.setMethodName(applySetterSuffix(fieldName, context)); + methodDto.setMethodName(generateSetterName(fieldName, context)); methodDto.setReturnType(builderType); methodDto.addParameter(parameter); // Modifier is controlled by configuration, not set here @@ -1303,7 +1362,7 @@ private static MethodDto createFieldConsumerWithArrayBuilder( parameter.setParameterTypeName(consumerType); MethodDto methodDto = new MethodDto(); - methodDto.setMethodName(applySetterSuffix(fieldName, context)); + methodDto.setMethodName(generateSetterName(fieldName, context)); methodDto.setReturnType(returnBuilderType); methodDto.addParameter(parameter); // Modifier is controlled by configuration, not set here @@ -1564,7 +1623,7 @@ private static MethodDto createWithBuilderMethod(BuilderDefinitionDto builderDef } /** - * Applies the setter suffix to a field name to create the method name. + * Generates the name of setters on the builder according to configuration and field name. * *

    If the suffix is empty, returns the fieldName as-is. If the suffix is set, capitalizes the * first letter of fieldName and prepends the suffix. @@ -1581,12 +1640,12 @@ private static MethodDto createWithBuilderMethod(BuilderDefinitionDto builderDef * @param context the processing context containing the configuration with the suffix * @return the method name with suffix applied */ - private static String applySetterSuffix(String fieldName, ProcessingContext context) { + private static String generateSetterName(String fieldName, ProcessingContext context) { String suffix = context.getBuilderConfigurationForElement().getSetterSuffix(); - if (suffix == null || suffix.isEmpty()) { + if (StringUtils.isBlank(suffix)) { return fieldName; } - return suffix + StringUtils.capitalize(fieldName); + return StringUtils.trim(suffix) + StringUtils.capitalize(fieldName); } /** diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java index 1df13687..1dc3e99f 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java @@ -169,6 +169,7 @@ public BuilderConfiguration readBuilderConfiguration() { .implementsBuilderBase(readOptionState(CompilerArgumentsEnum.IMPLEMENTS_BUILDER_BASE)) .generateWithInterface(readOptionState(CompilerArgumentsEnum.GENERATE_WITH_INTERFACE)) .builderSuffix(readValue(CompilerArgumentsEnum.BUILDER_SUFFIX)) + .setterSuffix(readValue(CompilerArgumentsEnum.SETTER_SUFFIX)) .build(); } } diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java index 361c847d..8387d4a5 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java @@ -71,6 +71,8 @@ void allConfigurationOptions_MustBeSettableViaBuilder() { .usingHashMapBuilder(OptionState.ENABLED) // Integration .generateWithInterface(OptionState.ENABLED) + // Naming + .setterSuffix("") .build(); // Verify all options are accessible (this will fail to compile if accessors are missing) @@ -190,7 +192,8 @@ public Address() {} "-Asimplebuilder.usingBuilderImplementationAnnotation=false", "-Asimplebuilder.implementsBuilderBase=false", "-Asimplebuilder.generateWithInterface=false", - "-Asimplebuilder.builderSuffix=CustomBuilder") + "-Asimplebuilder.builderSuffix=CustomBuilder", + "-Asimplebuilder.setterSuffix=with") .compile(nestedDto, addressDto, source); // Then: Compilation should succeed @@ -203,32 +206,31 @@ public Address() {} // With generateFieldSupplier=false, NO supplier methods should be generated ProcessorAsserts.assertNotContaining( generatedCode, - "public MinimalDtoCustomBuilder name(Supplier nameSupplier)", - "public MinimalDtoCustomBuilder items(Supplier> itemsSupplier)", - "public MinimalDtoCustomBuilder properties(Supplier> propertiesSupplier)", - "public MinimalDtoCustomBuilder description(Supplier> descriptionSupplier)", - "public MinimalDtoCustomBuilder tags(Supplier> tagsSupplier)", - "public MinimalDtoCustomBuilder nested(Supplier nestedSupplier)", - "public MinimalDtoCustomBuilder address(Supplier

    addressSupplier)"); + "public MinimalDtoCustomBuilder withName(Supplier nameSupplier)", + "public MinimalDtoCustomBuilder withItems(Supplier> itemsSupplier)", + "public MinimalDtoCustomBuilder withProperties(Supplier> propertiesSupplier)", + "public MinimalDtoCustomBuilder withDescription(Supplier> descriptionSupplier)", + "public MinimalDtoCustomBuilder withTags(Supplier> tagsSupplier)", + "public MinimalDtoCustomBuilder withNested(Supplier nestedSupplier)", + "public MinimalDtoCustomBuilder withAddress(Supplier
    addressSupplier)"); // With generateFieldConsumer=false, NO field consumer methods should be generated // Field consumer = Consumer where T is a custom type with empty constructor ProcessorAsserts.assertNotContaining( generatedCode, - "public MinimalDtoCustomBuilder address(Consumer
    addressConsumer)", - "public MinimalDtoCustomBuilder address(Consumer
    addressConsumer)", - "public MinimalDtoCustomBuilder nested(Consumer nestedConsumer)", - "public MinimalDtoCustomBuilder items(Consumer> itemsConsumer)", - "public MinimalDtoCustomBuilder tags(Consumer> tagsConsumer)", - "public MinimalDtoCustomBuilder properties(Consumer> propertiesConsumer)"); + "public MinimalDtoCustomBuilder withAddress(Consumer
    addressConsumer)", + "public MinimalDtoCustomBuilder withNested(Consumer nestedConsumer)", + "public MinimalDtoCustomBuilder withItems(Consumer> itemsConsumer)", + "public MinimalDtoCustomBuilder withTags(Consumer> tagsConsumer)", + "public MinimalDtoCustomBuilder withProperties(Consumer> propertiesConsumer)"); // With generateBuilderProvider=false, NO builder consumer methods should be generated // Builder consumers include: StringBuilder, collection builders, nested DTO builders ProcessorAsserts.assertNotContaining( generatedCode, - "public MinimalDtoCustomBuilder nested(Consumer nestedBuilderConsumer)", - "public MinimalDtoCustomBuilder name(Consumer nameStringBuilderConsumer)", - "public MinimalDtoCustomBuilder description(Consumer descriptionStringBuilderConsumer)"); + "public MinimalDtoCustomBuilder withNested(Consumer nestedBuilderConsumer)", + "public MinimalDtoCustomBuilder withName(Consumer nameStringBuilderConsumer)", + "public MinimalDtoCustomBuilder withDescription(Consumer descriptionStringBuilderConsumer)"); // With generateConditionalHelper=false, NO conditional methods ProcessorAsserts.assertNotContaining( @@ -237,22 +239,22 @@ public Address() {} // With generateVarArgsHelpers=false, NO VarArgs helpers should be generated ProcessorAsserts.assertNotContaining( generatedCode, - "public MinimalDtoCustomBuilder items(String... items)", - "public MinimalDtoCustomBuilder properties(Map.Entry... properties)", - "public MinimalDtoCustomBuilder tags(String... tags)"); + "public MinimalDtoCustomBuilder withItems(String... items)", + "public MinimalDtoCustomBuilder withProperties(Map.Entry... properties)", + "public MinimalDtoCustomBuilder withTags(String... tags)"); // With generateWithInterface=false, NO With interface ProcessorAsserts.assertNotContaining(generatedCode, "public interface With"); // With generateUnboxedOptional=false, NO unboxed optional methods should be generated ProcessorAsserts.assertNotContaining( - generatedCode, "public MinimalDtoCustomBuilder description(String description)"); + generatedCode, "public MinimalDtoCustomBuilder withDescription(String description)"); // With generateStringFormatHelpers=false, NO String format methods should be generated ProcessorAsserts.assertNotContaining( generatedCode, - "public MinimalDtoCustomBuilder name(String format, Object... args)", - "public MinimalDtoCustomBuilder description(String format, Object... args)"); + "public MinimalDtoCustomBuilder withName(String format, Object... args)", + "public MinimalDtoCustomBuilder withDescription(String format, Object... args)"); // With usingGeneratedAnnotation=false, NO @Generated annotation should be used ProcessorAsserts.assertNotContaining(generatedCode, "@Generated("); @@ -274,14 +276,14 @@ public Address() {} // With methodAccess=PACKAGE_PRIVATE, methods should NOT have public modifier ProcessorAsserts.assertNotContaining( generatedCode, - "public MinimalDtoCustomBuilder name(String name)", + "public MinimalDtoCustomBuilder withName(String name)", "public MinimalDto build()", "public static MinimalDtoCustomBuilder create()"); - // But package-private methods should exist + // But package-private methods should exist - with setterSuffix="with", methods are prefixed ProcessorAsserts.assertContaining( generatedCode, - "MinimalDtoCustomBuilder name(String name)", + "MinimalDtoCustomBuilder withName(String name)", "MinimalDto build()", "static MinimalDtoCustomBuilder create()"); @@ -289,34 +291,34 @@ public Address() {} // should be used ProcessorAsserts.assertNotContaining( generatedCode, - "MinimalDtoCustomBuilder items(Consumer> itemsBuilderConsumer)"); + "MinimalDtoCustomBuilder withItems(Consumer> itemsBuilderConsumer)"); // With usingHashSetBuilder=false AND generateBuilderProvider=false, NO HashSetBuilder should be // used ProcessorAsserts.assertNotContaining( generatedCode, - "MinimalDtoCustomBuilder tags(Consumer> tagsBuilderConsumer)"); + "MinimalDtoCustomBuilder withTags(Consumer> tagsBuilderConsumer)"); // With usingHashMapBuilder=false AND generateBuilderProvider=false, NO HashMapBuilder should be // used ProcessorAsserts.assertNotContaining( generatedCode, - "MinimalDtoCustomBuilder properties(Consumer> propertiesBuilderConsumer)"); + "MinimalDtoCustomBuilder withProperties(Consumer> propertiesBuilderConsumer)"); - // Still generates: basic setters and build method (package-private methods, private - // constructors) + // Still generates: basic setters and build method + // With setterSuffix="with", all setter methods should be prefixed with "with" and capitalized ProcessorAsserts.assertContaining( generatedCode, "class MinimalDtoCustomBuilder", "private MinimalDtoCustomBuilder()", "private MinimalDtoCustomBuilder(MinimalDto instance)", - "MinimalDtoCustomBuilder name(String name)", - "MinimalDtoCustomBuilder items(List items)", - "MinimalDtoCustomBuilder properties(Map properties)", - "MinimalDtoCustomBuilder description(Optional description)", - "MinimalDtoCustomBuilder tags(Set tags)", - "MinimalDtoCustomBuilder nested(NestedDto nested)", - "MinimalDtoCustomBuilder address(Address address)", + "MinimalDtoCustomBuilder withName(String name)", + "MinimalDtoCustomBuilder withItems(List items)", + "MinimalDtoCustomBuilder withProperties(Map properties)", + "MinimalDtoCustomBuilder withDescription(Optional description)", + "MinimalDtoCustomBuilder withTags(Set tags)", + "MinimalDtoCustomBuilder withNested(NestedDto nested)", + "MinimalDtoCustomBuilder withAddress(Address address)", "MinimalDto build()", "static MinimalDtoCustomBuilder create()"); } From f0482e49c311ae527e07ed4a39f3cd4a9dc4f6e2 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 2 Nov 2025 02:19:19 +0100 Subject: [PATCH 34/63] Extending tests of ConfigurationProcessing --- .../ConfigurationProcessingTest.java | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java index 8387d4a5..8d2a44bb 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java @@ -61,17 +61,26 @@ void allConfigurationOptions_MustBeSettableViaBuilder() { .generateConditionalLogic(OptionState.ENABLED) // Access control .builderAccess(AccessModifier.PACKAGE_PRIVATE) + .builderConstructorAccess(AccessModifier.PRIVATE) .methodAccess(AccessModifier.PACKAGE_PRIVATE) - // Collection options + // Helper method generation .generateVarArgsHelpers(OptionState.ENABLED) + .generateStringFormatHelpers(OptionState.ENABLED) + .generateUnboxedOptional(OptionState.ENABLED) + // Collection builder options .usingArrayListBuilder(OptionState.ENABLED) .usingArrayListBuilderWithElementBuilders(OptionState.ENABLED) .usingHashSetBuilder(OptionState.ENABLED) .usingHashSetBuilderWithElementBuilders(OptionState.ENABLED) .usingHashMapBuilder(OptionState.ENABLED) + // Annotations + .usingGeneratedAnnotation(OptionState.ENABLED) + .usingBuilderImplementationAnnotation(OptionState.ENABLED) // Integration + .implementsBuilderBase(OptionState.ENABLED) .generateWithInterface(OptionState.ENABLED) // Naming + .builderSuffix("Builder") .setterSuffix("") .build(); @@ -82,14 +91,22 @@ void allConfigurationOptions_MustBeSettableViaBuilder() { assertEquals(OptionState.ENABLED, config.generateBuilderProvider()); assertEquals(OptionState.ENABLED, config.generateConditionalHelper()); assertEquals(AccessModifier.PACKAGE_PRIVATE, config.getBuilderAccess()); + assertEquals(AccessModifier.PRIVATE, config.getBuilderConstructorAccess()); assertEquals(AccessModifier.PACKAGE_PRIVATE, config.getMethodAccess()); assertEquals(OptionState.ENABLED, config.generateVarArgsHelpers()); + assertEquals(OptionState.ENABLED, config.generateStringFormatHelpers()); + assertEquals(OptionState.ENABLED, config.generateUnboxedOptional()); assertEquals(OptionState.ENABLED, config.usingArrayListBuilder()); assertEquals(OptionState.ENABLED, config.usingArrayListBuilderWithElementBuilders()); assertEquals(OptionState.ENABLED, config.usingHashSetBuilder()); assertEquals(OptionState.ENABLED, config.usingHashSetBuilderWithElementBuilders()); assertEquals(OptionState.ENABLED, config.usingHashMapBuilder()); + assertEquals(OptionState.ENABLED, config.usingGeneratedAnnotation()); + assertEquals(OptionState.ENABLED, config.usingBuilderImplementationAnnotation()); + assertEquals(OptionState.ENABLED, config.implementsBuilderBase()); assertEquals(OptionState.ENABLED, config.generateWithInterface()); + assertEquals("Builder", config.getBuilderSuffix()); + assertEquals("", config.getSetterSuffix()); } /** @@ -305,7 +322,7 @@ public Address() {} generatedCode, "MinimalDtoCustomBuilder withProperties(Consumer> propertiesBuilderConsumer)"); - // Still generates: basic setters and build method + // Still generates: basic setters and build method // With setterSuffix="with", all setter methods should be prefixed with "with" and capitalized ProcessorAsserts.assertContaining( generatedCode, @@ -336,6 +353,7 @@ void configurationMerge_MustRespectPriority() { .generateSupplier(OptionState.ENABLED) .generateConsumer(OptionState.ENABLED) .builderAccess(AccessModifier.PUBLIC) + .setterSuffix("") .build(); // When: Merge with override configuration @@ -343,6 +361,7 @@ void configurationMerge_MustRespectPriority() { BuilderConfiguration.builder() .generateSupplier(OptionState.DISABLED) // Override .generateBuilderProvider(OptionState.DISABLED) // New value + .setterSuffix("with") // Override setterSuffix // generateConsumer not set, should keep base value .build(); @@ -363,6 +382,7 @@ void configurationMerge_MustRespectPriority() { AccessModifier.PUBLIC, merged.getBuilderAccess(), "Base value should be kept when override is DEFAULT"); + assertEquals("with", merged.getSetterSuffix(), "Override should win for setterSuffix"); } /** @@ -378,6 +398,7 @@ void configurationToString_MustBeHumanReadable() { .generateConsumer(OptionState.ENABLED) .builderAccess(AccessModifier.PRIVATE) .methodAccess(AccessModifier.PROTECTED) + .setterSuffix("with") .build(); String configString = config.toString(); @@ -390,6 +411,10 @@ void configurationToString_MustBeHumanReadable() { assertTrue(configString.contains("ENABLED"), "toString should show enum values"); assertTrue(configString.contains("PRIVATE"), "toString should show AccessModifier values"); assertTrue(configString.contains("PROTECTED"), "toString should show AccessModifier values"); + assertTrue( + configString.contains("setterSuffix"), + "toString should mention setterSuffix when non-default"); + assertTrue(configString.contains("with"), "toString should show setterSuffix value"); // Verify it's not just a hash code assertTrue(configString.length() > 100, "toString should be detailed, not just class@hashcode"); From 05b9875d8771f9b1bde3dd5f6ed92959ffc1e63e Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 2 Nov 2025 02:37:17 +0100 Subject: [PATCH 35/63] Updating documentation --- .../core/annotations/SimpleBuilder.java | 265 ++++++++++++++++-- 1 file changed, 239 insertions(+), 26 deletions(-) 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 e7c0ae7c..589deaa0 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 @@ -77,7 +77,19 @@ * Generate a supplier method by which the user of this builder could define a function, which * supplies the value for this field.
    * The generated method has the parameter-type {@code Supplier} with T being the type of the - * field. Default: ENABLED Compiler option: -Asimplebuilder.generateFieldSupplier + * field. + * + *

    Example: + * + *

    {@code
    +     * PersonDto person = PersonDtoBuilder.create()
    +     *     .name(() -> fetchNameFromDatabase())
    +     *     .age(() -> calculateAge())
    +     *     .build();
    +     * }
    + * + * Default: ENABLED
    + * Compiler option: -Asimplebuilder.generateFieldSupplier */ OptionState generateFieldSupplier() default OptionState.UNSET; @@ -85,8 +97,21 @@ * Generate a consumer method with parameter-type {@code Consumer} with T being the type of * the field.
    * This is only done for complex field types, so that users could use setter to change the - * properties of that parameter. Default: ENABLED Compiler option: - * -Asimplebuilder.generateFieldConsumer + * properties of that parameter. + * + *

    Example: + * + *

    {@code
    +     * PersonDto person = PersonDtoBuilder.create()
    +     *     .address(addr -> {
    +     *         addr.setStreet("Main St");
    +     *         addr.setCity("Berlin");
    +     *     })
    +     *     .build();
    +     * }
    + * + * Default: ENABLED
    + * Compiler option: -Asimplebuilder.generateFieldConsumer */ OptionState generateFieldConsumer() default OptionState.UNSET; @@ -94,14 +119,40 @@ * Generate a builder provider method with parameter-type {@code Provider>} with T * being the type of the field
    * This is only done for complex field types, which have a recognized builder so that users - * could use the chained builder methods to set the value of this complex field.
    - * Default: ENABLED Compiler option: -Asimplebuilder.generateBuilderProvider + * could use the chained builder methods to set the value of this complex field. + * + *

    Example: + * + *

    {@code
    +     * PersonDto person = PersonDtoBuilder.create()
    +     *     .address(ab -> ab
    +     *         .street("Main St")
    +     *         .city("Berlin")
    +     *         .zipCode("10115"))
    +     *     .build();
    +     * }
    + * + * Default: ENABLED
    + * Compiler option: -Asimplebuilder.generateBuilderProvider */ OptionState generateBuilderProvider() default OptionState.UNSET; /** * Generate conditional logic method (conditional)
    - * Default: ENABLED Compiler option: -Asimplebuilder.generateConditionalHelper + * Allows conditional execution of builder methods based on a boolean supplier. + * + *

    Example: + * + *

    {@code
    +     * PersonDto person = PersonDtoBuilder.create()
    +     *     .name("John")
    +     *     .conditional(() -> includeEmail, b -> b.email("john@example.com"))
    +     *     .conditional(() -> isPremium, b -> b.memberLevel("GOLD"))
    +     *     .build();
    +     * }
    + * + * Default: ENABLED
    + * Compiler option: -Asimplebuilder.generateConditionalHelper */ OptionState generateConditionalHelper() default OptionState.UNSET; @@ -109,19 +160,51 @@ /** * Access level for generated builder class. * + *

    Available values: + * + *

      + *
    • PUBLIC - For public APIs (default) + *
    • PACKAGE_PRIVATE - For internal use within a package + *
    • PRIVATE - When using only static factory methods + *
    + * + *

    Example: + * + *

    {@code
    +     * @SimpleBuilder(options = @SimpleBuilder.Options(
    +     *     builderAccess = AccessModifier.PACKAGE_PRIVATE
    +     * ))
    +     * public class PersonDto {
    +     *     // Generates: class PersonDtoBuilder (package-private)
    +     * }
    +     * }
    + * *

    Default: {@link AccessModifier#PUBLIC PUBLIC} * - *

    Compiler option: -Asimplebuilder.builderAccess (values: PUBLIC, PROTECTED, - * PACKAGE_PRIVATE, PRIVATE) + *

    Compiler option: -Asimplebuilder.builderAccess (values: PUBLIC, PACKAGE_PRIVATE, PRIVATE) */ AccessModifier builderAccess() default AccessModifier.PUBLIC; /** * Access level for generated builder constructors. * + *

    Common pattern: Use PRIVATE constructors with PUBLIC static factory methods (create()). + * + *

    Example: + * + *

    {@code
    +     * @SimpleBuilder(options = @SimpleBuilder.Options(
    +     *     builderConstructorAccess = AccessModifier.PRIVATE
    +     * ))
    +     * public class PersonDto {
    +     *     // Generates: private PersonDtoBuilder() and private PersonDtoBuilder(PersonDto)
    +     *     // Use via: PersonDtoBuilder.create() or PersonDtoBuilder.from(instance)
    +     * }
    +     * }
    + * *

    Default: {@link AccessModifier#PUBLIC PUBLIC} * - *

    Compiler option: -Asimplebuilder.builderConstructorAccess (values: PUBLIC, PROTECTED, + *

    Compiler option: -Asimplebuilder.builderConstructorAccess (values: PUBLIC, * PACKAGE_PRIVATE, PRIVATE) */ AccessModifier builderConstructorAccess() default AccessModifier.PUBLIC; @@ -129,23 +212,58 @@ /** * Access level for generated builder methods. * + *

    Typically matches builder class access. Use PACKAGE_PRIVATE for internal APIs. + * + *

    Example: + * + *

    {@code
    +     * @SimpleBuilder(options = @SimpleBuilder.Options(
    +     *     methodAccess = AccessModifier.PACKAGE_PRIVATE
    +     * ))
    +     * public class PersonDto {
    +     *     // Generates: PersonDtoBuilder name(String name) (package-private)
    +     * }
    +     * }
    + * *

    Default: {@link AccessModifier#PUBLIC PUBLIC} * - *

    Compiler option: -Asimplebuilder.methodAccess (values: PUBLIC, PROTECTED, PACKAGE_PRIVATE, - * PRIVATE) + *

    Compiler option: -Asimplebuilder.methodAccess (values: PUBLIC, PACKAGE_PRIVATE, PRIVATE) */ AccessModifier methodAccess() default AccessModifier.PUBLIC; // === Collection Options === /** * Generate helper methods with VarArgs for Lists and Sets.
    - * Default: ENABLED Compiler option: -Asimplebuilder.generateVarArgsHelpers + * Allows passing multiple elements directly instead of creating a list/set. + * + *

    Example: + * + *

    {@code
    +     * PersonDto person = PersonDtoBuilder.create()
    +     *     .hobbies("Reading", "Gaming", "Cooking") // VarArgs instead of List.of(...)
    +     *     .build();
    +     * }
    + * + * Default: ENABLED
    + * Compiler option: -Asimplebuilder.generateVarArgsHelpers */ OptionState generateVarArgsHelpers() default OptionState.UNSET; /** * Generate String format helper methods for String fields.
    - * Default: ENABLED Compiler option: -Asimplebuilder.generateStringFormatHelpers + * Allows using String.format() style for setting string values. + * + *

    Example: + * + *

    {@code
    +     * PersonDto person = PersonDtoBuilder.create()
    +     *     .name("Hello %s %s", firstName, lastName)
    +     *     .description("Age: %d, City: %s", age, city)
    +     *     .build();
    +     * }
    + * + * Default: ENABLED
    + * Compiler option: -Asimplebuilder.generateStringFormatHelpers */ OptionState generateStringFormatHelpers() default OptionState.UNSET; @@ -153,8 +271,19 @@ * Generate unboxed optional methods that accept the inner type T directly instead of * Optional<T>.
    * For Optional fields, this generates a setter that accepts T and wraps it with - * Optional.ofNullable().
    - * Default: ENABLED Compiler option: -Asimplebuilder.generateUnboxedOptional + * Optional.ofNullable(). + * + *

    Example: + * + *

    {@code
    +     * // Field: Optional email
    +     * PersonDto person = PersonDtoBuilder.create()
    +     *     .email("john@example.com") // String instead of Optional.of("john@example.com")
    +     *     .build();
    +     * }
    + * + * Default: ENABLED
    + * Compiler option: -Asimplebuilder.generateUnboxedOptional */ OptionState generateUnboxedOptional() default OptionState.UNSET; @@ -307,26 +436,81 @@ // === Annotations === /** * Use {@code @Generated} annotation on the generated builder class.
    - * Default: ENABLED Compiler option: -Asimplebuilder.usingGeneratedAnnotation + * Marks the builder as generated code for tooling and analysis. + * + *

    Example: + * + *

    {@code
    +     * @Generated("org.javahelpers.simple.builders.processor.BuilderProcessor")
    +     * public class PersonDtoBuilder {
    +     *     // ...
    +     * }
    +     * }
    + * + * Default: ENABLED
    + * Compiler option: -Asimplebuilder.usingGeneratedAnnotation */ OptionState usingGeneratedAnnotation() default OptionState.UNSET; /** * Use {@code @BuilderImplementation} annotation on the generated builder class.
    - * Default: ENABLED Compiler option: -Asimplebuilder.usingBuilderImplementationAnnotation + * Links the generated builder back to the original DTO class. + * + *

    Example: + * + *

    {@code
    +     * @BuilderImplementation(PersonDto.class)
    +     * public class PersonDtoBuilder {
    +     *     // ...
    +     * }
    +     * }
    + * + * Default: ENABLED
    + * Compiler option: -Asimplebuilder.usingBuilderImplementationAnnotation */ OptionState usingBuilderImplementationAnnotation() default OptionState.UNSET; // === Integration === /** * Implement {@code IBuilderBase} interface in the generated builder class.
    - * Default: ENABLED Compiler option: -Asimplebuilder.implementsBuilderBase + * Provides a common base interface for all generated builders. + * + *

    Example: + * + *

    {@code
    +     * public class PersonDtoBuilder implements IBuilderBase {
    +     *     @Override
    +     *     public PersonDto build() {
    +     *         // ...
    +     *     }
    +     * }
    +     * }
    + * + * Default: ENABLED
    + * Compiler option: -Asimplebuilder.implementsBuilderBase */ OptionState implementsBuilderBase() default OptionState.UNSET; /** * Generate With interface for integrating builder into DTOs.
    - * Default: ENABLED Compiler option: -Asimplebuilder.generateWithInterface + * Creates a nested interface that can be implemented by the DTO for fluent updates. + * + *

    Example: + * + *

    {@code
    +     * PersonDto updated = person.with(b -> b
    +     *     .name("New Name")
    +     *     .age(30));
    +     * }
    +     *
    +     * // Generated:
    +     * public interface WithPersonDto {
    +     *     default PersonDto with(Consumer updater) { ... }
    +     * }
    +     * }
    + * + * Default: ENABLED
    + * Compiler option: -Asimplebuilder.generateWithInterface */ OptionState generateWithInterface() default OptionState.UNSET; @@ -334,16 +518,46 @@ /** * Suffix to append to the DTO name to generate the builder class name.
    * For example, with suffix "Builder", a DTO named "PersonDto" will generate "PersonDtoBuilder". - *
    - * Default: "Builder" Compiler option: -Asimplebuilder.builderSuffix + * + *

    Example: + * + *

    {@code
    +     * @SimpleBuilder(options = @SimpleBuilder.Options(
    +     *     builderSuffix = "Factory"
    +     * ))
    +     * public class PersonDto {
    +     *     // Generates: PersonDtoFactory instead of PersonDtoBuilder
    +     * }
    +     * }
    + * + * Default: "Builder"
    + * Compiler option: -Asimplebuilder.builderSuffix */ String builderSuffix() default "Builder"; /** * Suffix to append to setter method names in the generated builder.
    * For example, with suffix "with", a field named "name" will generate "withName()".
    - * When a suffix is set, the field name is capitalized after the suffix.
    - * Default: "" (empty - no suffix) Compiler option: -Asimplebuilder.setterSuffix + * When a suffix is set, the field name is capitalized after the suffix. + * + *

    Example: + * + *

    {@code
    +     * @SimpleBuilder(options = @SimpleBuilder.Options(
    +     *     setterSuffix = "with"
    +     * ))
    +     * public class PersonDto {
    +     *     // Generates: withName(String) instead of name(String)
    +     * }
    +     *
    +     * PersonDto person = PersonDtoBuilder.create()
    +     *     .withName("John")
    +     *     .withAge(25)
    +     *     .build();
    +     * }
    + * + * Default: "" (empty - no suffix)
    + * Compiler option: -Asimplebuilder.setterSuffix */ String setterSuffix() default ""; } @@ -359,9 +573,8 @@ * *
    {@code
        * @SimpleBuilder.Template(options = @SimpleBuilder.Options(
    -   *     generateSupplier = true,
    -   *     generateProvider = true,
    -   *     generateToString = true
    +   *     generateFieldSupplier = true,
    +   *     generateFieldConsumer = true
        * ))
        * @Retention(RetentionPolicy.CLASS)
        * @Target(ElementType.TYPE)
    
    From 13de0f1c7ca7064cee921ab2d7fc6f6d13e88c36 Mon Sep 17 00:00:00 2001
    From: AndreasIgel 
    Date: Sun, 2 Nov 2025 02:37:36 +0100
    Subject: [PATCH 36/63] Removing unused functionality
    
    ---
     .../builders/core/enums/AccessModifier.java   |   6 +-
     .../util/AnnotationDefaultReader.java         | 119 ------------------
     .../util/CompilerArgumentsReader.java         |   2 -
     .../processor/util/JavapoetMapper.java        |   1 -
     .../ConfigurationProcessingTest.java          |   9 +-
     5 files changed, 8 insertions(+), 129 deletions(-)
     delete mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/util/AnnotationDefaultReader.java
    
    diff --git a/core/src/main/java/org/javahelpers/simple/builders/core/enums/AccessModifier.java b/core/src/main/java/org/javahelpers/simple/builders/core/enums/AccessModifier.java
    index 309c0471..602ff159 100644
    --- a/core/src/main/java/org/javahelpers/simple/builders/core/enums/AccessModifier.java
    +++ b/core/src/main/java/org/javahelpers/simple/builders/core/enums/AccessModifier.java
    @@ -29,6 +29,9 @@
      *
      * 

    This enum is used to control the visibility of generated builders and their methods through * the {@link org.javahelpers.simple.builders.core.annotations.SimpleBuilder.Options} annotation. + * + *

    Note: PROTECTED is intentionally not included as builders follow the Builder pattern, not + * inheritance, and should not be extended. */ public enum AccessModifier { @@ -38,9 +41,6 @@ public enum AccessModifier { /** Public access - accessible from anywhere */ PUBLIC("public"), - /** Protected access - accessible within the same package and subclasses */ - PROTECTED("protected"), - /** Package-private access (default) - accessible only within the same package */ PACKAGE_PRIVATE(""), diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/AnnotationDefaultReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/AnnotationDefaultReader.java deleted file mode 100644 index 26d9e5f4..00000000 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/AnnotationDefaultReader.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2025 Andreas Igel - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package org.javahelpers.simple.builders.processor.util; - -import java.lang.reflect.Method; - -/** - * Utility class for reading default values from annotations via reflection. - * - *

    This class provides methods to extract default values from annotation methods, which is useful - * for maintaining a single source of truth for configuration defaults. - */ -public final class AnnotationDefaultReader { - - private AnnotationDefaultReader() { - // Utility class - prevent instantiation - } - - /** - * Read a boolean default value from an annotation method. - * - * @param annotationClass The annotation class containing the method - * @param methodName The annotation method name - * @param fallback Fallback value if reflection fails - * @return The default value from the annotation, or fallback if not found - */ - public static boolean getBooleanDefault( - Class annotationClass, String methodName, boolean fallback) { - try { - Method method = annotationClass.getMethod(methodName); - Object defaultValue = method.getDefaultValue(); - return defaultValue != null ? (Boolean) defaultValue : fallback; - } catch (Exception e) { - // Fallback to provided value if reflection fails - return fallback; - } - } - - /** - * Read an enum default value from an annotation method and convert to String. - * - * @param annotationClass The annotation class containing the method - * @param methodName The annotation method name - * @param fallback Fallback value if reflection fails - * @return The enum name as String, or fallback if not found - */ - public static String getEnumDefaultAsString( - Class annotationClass, String methodName, String fallback) { - try { - Method method = annotationClass.getMethod(methodName); - Object defaultValue = method.getDefaultValue(); - return defaultValue != null ? ((Enum) defaultValue).name() : fallback; - } catch (Exception e) { - // Fallback to provided value if reflection fails - return fallback; - } - } - - /** - * Read a String default value from an annotation method. - * - * @param annotationClass The annotation class containing the method - * @param methodName The annotation method name - * @param fallback Fallback value if reflection fails - * @return The default value from the annotation, or fallback if not found - */ - public static String getStringDefault( - Class annotationClass, String methodName, String fallback) { - try { - Method method = annotationClass.getMethod(methodName); - Object defaultValue = method.getDefaultValue(); - return defaultValue != null ? (String) defaultValue : fallback; - } catch (Exception e) { - // Fallback to provided value if reflection fails - return fallback; - } - } - - /** - * Read an integer default value from an annotation method. - * - * @param annotationClass The annotation class containing the method - * @param methodName The annotation method name - * @param fallback Fallback value if reflection fails - * @return The default value from the annotation, or fallback if not found - */ - public static int getIntDefault(Class annotationClass, String methodName, int fallback) { - try { - Method method = annotationClass.getMethod(methodName); - Object defaultValue = method.getDefaultValue(); - return defaultValue != null ? (Integer) defaultValue : fallback; - } catch (Exception e) { - // Fallback to provided value if reflection fails - return fallback; - } - } -} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java index 1dc3e99f..1eda6701 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java @@ -119,8 +119,6 @@ public AccessModifier readAccessModifier(CompilerArgumentsEnum argument) { return AccessModifier.PRIVATE; } else if (Strings.CI.equalsAny(value, "package-private", "package_private")) { return AccessModifier.PACKAGE_PRIVATE; - } else if (Strings.CI.equals(value, "protected")) { - return AccessModifier.PROTECTED; } else { return AccessModifier.DEFAULT; } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavapoetMapper.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavapoetMapper.java index baa6ff38..7f4b5a60 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavapoetMapper.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavapoetMapper.java @@ -217,7 +217,6 @@ public static List map2AnnotationSpecs(List annot public static javax.lang.model.element.Modifier map2Modifier(AccessModifier accessModifier) { return switch (accessModifier) { case PUBLIC, DEFAULT -> javax.lang.model.element.Modifier.PUBLIC; - case PROTECTED -> javax.lang.model.element.Modifier.PROTECTED; case PRIVATE -> javax.lang.model.element.Modifier.PRIVATE; case PACKAGE_PRIVATE -> null; // Package-private has no explicit modifier }; diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java index 8d2a44bb..bf4e0ab3 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java @@ -397,7 +397,7 @@ void configurationToString_MustBeHumanReadable() { .generateSupplier(OptionState.DISABLED) .generateConsumer(OptionState.ENABLED) .builderAccess(AccessModifier.PRIVATE) - .methodAccess(AccessModifier.PROTECTED) + .methodAccess(AccessModifier.PACKAGE_PRIVATE) .setterSuffix("with") .build(); @@ -410,7 +410,8 @@ void configurationToString_MustBeHumanReadable() { assertTrue(configString.contains("DISABLED"), "toString should show enum values"); assertTrue(configString.contains("ENABLED"), "toString should show enum values"); assertTrue(configString.contains("PRIVATE"), "toString should show AccessModifier values"); - assertTrue(configString.contains("PROTECTED"), "toString should show AccessModifier values"); + assertTrue( + configString.contains("PACKAGE_PRIVATE"), "toString should show AccessModifier values"); assertTrue( configString.contains("setterSuffix"), "toString should mention setterSuffix when non-default"); @@ -445,7 +446,7 @@ void configurationMerge_Chain_ShouldApplyInOrder() { BuilderConfiguration compilerArgs = BuilderConfiguration.builder() .generateSupplier(OptionState.DISABLED) - .builderAccess(AccessModifier.PROTECTED) + .builderAccess(AccessModifier.PACKAGE_PRIVATE) .build(); // Layer 3: Template (override some compiler args) @@ -469,7 +470,7 @@ void configurationMerge_Chain_ShouldApplyInOrder() { finalConfig.generateFieldConsumer(), "Options should override all others"); assertEquals( - AccessModifier.PROTECTED, + AccessModifier.PACKAGE_PRIVATE, finalConfig.getBuilderAccess(), "Compiler args should override defaults"); assertEquals( From bb42dd1769d89cbe387fb50bfa3f3e880c2c4edf Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 2 Nov 2025 02:43:26 +0100 Subject: [PATCH 37/63] Renaming generateBuilderProvider to generateBuilderConsumer --- .../core/annotations/SimpleBuilder.java | 8 ++--- .../processor/dtos/BuilderConfiguration.java | 34 +++++++++---------- .../enums/CompilerArgumentsEnum.java | 4 +-- .../util/BuilderConfigurationReader.java | 4 +-- .../util/BuilderDefinitionCreator.java | 8 ++--- .../util/CompilerArgumentsReader.java | 2 +- .../ConfigurationProcessingTest.java | 18 +++++----- 7 files changed, 39 insertions(+), 39 deletions(-) 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 589deaa0..f25b0605 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 @@ -41,7 +41,7 @@ *

    Available configuration options: * *

      - *
    • Field Setters: generateFieldSupplier, generateFieldProvider, generateBuilderProvider + *
    • Field Setters: generateFieldSupplier, generateFieldConsumer, generateBuilderConsumer * (all default: true) *
    • Conditional Logic: generateConditionalHelper (default: true) *
    • Access Control: builderAccess, methodAccess (default: PUBLIC) @@ -116,7 +116,7 @@ OptionState generateFieldConsumer() default OptionState.UNSET; /** - * Generate a builder provider method with parameter-type {@code Provider>} with T + * Generate a builder consumer method with parameter-type {@code Consumer>} with T * being the type of the field
      * This is only done for complex field types, which have a recognized builder so that users * could use the chained builder methods to set the value of this complex field. @@ -133,9 +133,9 @@ * }
    * * Default: ENABLED
    - * Compiler option: -Asimplebuilder.generateBuilderProvider + * Compiler option: -Asimplebuilder.generateBuilderConsumer */ - OptionState generateBuilderProvider() default OptionState.UNSET; + OptionState generateBuilderConsumer() default OptionState.UNSET; /** * Generate conditional logic method (conditional)
    diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java index 61abd104..37d74619 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java @@ -41,7 +41,7 @@ * * @param generateFieldSupplier Generate field supplier methods * @param generateFieldConsumer Generate field consumer methods - * @param generateBuilderProvider Generate builder provider methods + * @param generateBuilderConsumer Generate builder consumer methods * @param generateConditionalHelper Generate conditional logic methods * @param builderAccess Access level for builder class * @param builderConstructorAccess Access level for builder constructors @@ -64,7 +64,7 @@ public record BuilderConfiguration( OptionState generateFieldSupplier, OptionState generateFieldConsumer, - OptionState generateBuilderProvider, + OptionState generateBuilderConsumer, OptionState generateConditionalHelper, AccessModifier builderAccess, AccessModifier builderConstructorAccess, @@ -88,7 +88,7 @@ public record BuilderConfiguration( builder() .generateSupplier(ENABLED) .generateConsumer(ENABLED) - .generateBuilderProvider(ENABLED) + .generateBuilderConsumer(ENABLED) .generateConditionalLogic(ENABLED) .builderAccess(PUBLIC) .builderConstructorAccess(PUBLIC) @@ -118,8 +118,8 @@ public boolean shouldGenerateFieldConsumer() { return generateFieldConsumer == ENABLED; } - public boolean shouldGenerateBuilderProvider() { - return generateBuilderProvider == ENABLED; + public boolean shouldGenerateBuilderConsumer() { + return generateBuilderConsumer == ENABLED; } public boolean shouldGenerateConditionalLogic() { @@ -219,10 +219,10 @@ public BuilderConfiguration merge(BuilderConfiguration other) { other.generateFieldConsumer != UNSET ? other.generateFieldConsumer : this.generateFieldConsumer) - .generateBuilderProvider( - other.generateBuilderProvider != UNSET - ? other.generateBuilderProvider - : this.generateBuilderProvider) + .generateBuilderConsumer( + other.generateBuilderConsumer != UNSET + ? other.generateBuilderConsumer + : this.generateBuilderConsumer) .generateConditionalLogic( other.generateConditionalHelper != UNSET ? other.generateConditionalHelper @@ -306,8 +306,8 @@ public String toString() { if (generateFieldConsumer != UNSET) { builder.append("generateFieldConsumer", generateFieldConsumer); } - if (generateBuilderProvider != UNSET) { - builder.append("generateBuilderProvider", generateBuilderProvider); + if (generateBuilderConsumer != UNSET) { + builder.append("generateBuilderConsumer", generateBuilderConsumer); } if (generateConditionalHelper != UNSET) { builder.append("generateConditionalHelper", generateConditionalHelper); @@ -357,7 +357,7 @@ public static class Builder { // === Field Setter Generation === private OptionState generateFieldSupplier = OptionState.UNSET; private OptionState generateFieldConsumer = OptionState.UNSET; - private OptionState generateBuilderProvider = OptionState.UNSET; + private OptionState generateBuilderConsumer = OptionState.UNSET; // === Conditional Logic === private OptionState generateConditionalHelper = OptionState.UNSET; @@ -410,13 +410,13 @@ public Builder generateConsumer(boolean value) { return this; } - public Builder generateBuilderProvider(OptionState value) { - this.generateBuilderProvider = value; + public Builder generateBuilderConsumer(OptionState value) { + this.generateBuilderConsumer = value; return this; } - public Builder generateBuilderProvider(boolean value) { - this.generateBuilderProvider = value ? ENABLED : DISABLED; + public Builder generateBuilderConsumer(boolean value) { + this.generateBuilderConsumer = value ? ENABLED : DISABLED; return this; } @@ -594,7 +594,7 @@ public BuilderConfiguration build() { return new BuilderConfiguration( generateFieldSupplier, generateFieldConsumer, - generateBuilderProvider, + generateBuilderConsumer, generateConditionalHelper, builderAccess, builderConstructorAccess, diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java index 449590e3..bda79262 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java @@ -45,8 +45,8 @@ public enum CompilerArgumentsEnum { /** Option for field consumer generation. */ GENERATE_FIELD_CONSUMER("generateFieldConsumer"), - /** Option for builder provider generation. */ - GENERATE_BUILDER_PROVIDER("generateBuilderProvider"), + /** Option for builder consumer generation. */ + GENERATE_BUILDER_CONSUMER("generateBuilderConsumer"), // === Conditional Logic === /** Option for conditional helper generation. */ diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java index d18c6c1f..d052ccc1 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java @@ -76,7 +76,7 @@ public BuilderConfiguration readFromOptions(Element element) { return BuilderConfiguration.builder() .generateSupplier(options.generateFieldSupplier()) .generateConsumer(options.generateFieldConsumer()) - .generateBuilderProvider(options.generateBuilderProvider()) + .generateBuilderConsumer(options.generateBuilderConsumer()) .generateConditionalLogic(options.generateConditionalHelper()) .builderAccess(options.builderAccess()) .builderConstructorAccess(options.builderConstructorAccess()) @@ -132,7 +132,7 @@ public BuilderConfiguration readFromTemplate(Element element) { return BuilderConfiguration.builder() .generateSupplier(options.generateFieldSupplier()) .generateConsumer(options.generateFieldConsumer()) - .generateBuilderProvider(options.generateBuilderProvider()) + .generateBuilderConsumer(options.generateBuilderConsumer()) .generateConditionalLogic(options.generateConditionalHelper()) .builderAccess(options.builderAccess()) .builderConstructorAccess(options.builderConstructorAccess()) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java index 028417b6..0b7c7031 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java @@ -451,8 +451,8 @@ private static boolean tryAddBuilderConsumer( VariableElement fieldParameter, TypeName builderType, ProcessingContext context) { - // Builder consumers are controlled by generateBuilderProvider - if (!context.getBuilderConfigurationForElement().shouldGenerateBuilderProvider()) { + // Builder consumers are controlled by generateBuilderConsumer + if (!context.getBuilderConfigurationForElement().shouldGenerateBuilderConsumer()) { return false; } Optional fieldBuilderOpt = resolveBuilderType(fieldParameter, context); @@ -507,8 +507,8 @@ && hasEmptyConstructor(fieldTypeElement, context)) { /** Tries to add StringBuilder-based consumer for String and Optional. */ private static boolean tryAddStringBuilderConsumer( FieldDto field, TypeName builderType, ProcessingContext context) { - // StringBuilder is a builder pattern, controlled by generateBuilderProvider - if (!context.getBuilderConfigurationForElement().shouldGenerateBuilderProvider()) { + // StringBuilder is a builder pattern, controlled by generateBuilderConsumer + if (!context.getBuilderConfigurationForElement().shouldGenerateBuilderConsumer()) { return false; } if (shouldGenerateStringBuilderConsumer(field.getFieldType())) { diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java index 1eda6701..2b6747b8 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java @@ -143,7 +143,7 @@ public BuilderConfiguration readBuilderConfiguration() { return BuilderConfiguration.builder() .generateSupplier(readOptionState(CompilerArgumentsEnum.GENERATE_FIELD_SUPPLIER)) .generateConsumer(readOptionState(CompilerArgumentsEnum.GENERATE_FIELD_CONSUMER)) - .generateBuilderProvider(readOptionState(CompilerArgumentsEnum.GENERATE_BUILDER_PROVIDER)) + .generateBuilderConsumer(readOptionState(CompilerArgumentsEnum.GENERATE_BUILDER_CONSUMER)) .generateConditionalLogic( readOptionState(CompilerArgumentsEnum.GENERATE_CONDITIONAL_HELPER)) .builderAccess(readAccessModifier(CompilerArgumentsEnum.BUILDER_ACCESS)) diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java index bf4e0ab3..3d691a5f 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java @@ -56,7 +56,7 @@ void allConfigurationOptions_MustBeSettableViaBuilder() { // Field setter generation options .generateSupplier(OptionState.ENABLED) .generateConsumer(OptionState.ENABLED) - .generateBuilderProvider(OptionState.ENABLED) + .generateBuilderConsumer(OptionState.ENABLED) // Conditional logic .generateConditionalLogic(OptionState.ENABLED) // Access control @@ -88,7 +88,7 @@ void allConfigurationOptions_MustBeSettableViaBuilder() { assertNotNull(config); assertEquals(OptionState.ENABLED, config.generateFieldSupplier()); assertEquals(OptionState.ENABLED, config.generateFieldConsumer()); - assertEquals(OptionState.ENABLED, config.generateBuilderProvider()); + assertEquals(OptionState.ENABLED, config.generateBuilderConsumer()); assertEquals(OptionState.ENABLED, config.generateConditionalHelper()); assertEquals(AccessModifier.PACKAGE_PRIVATE, config.getBuilderAccess()); assertEquals(AccessModifier.PRIVATE, config.getBuilderConstructorAccess()); @@ -192,7 +192,7 @@ public Address() {} .withOptions( "-Asimplebuilder.generateFieldSupplier=false", "-Asimplebuilder.generateFieldConsumer=false", - "-Asimplebuilder.generateBuilderProvider=false", + "-Asimplebuilder.generateBuilderConsumer=false", "-Asimplebuilder.generateConditionalHelper=false", "-Asimplebuilder.builderAccess=PACKAGE_PRIVATE", "-Asimplebuilder.builderConstructorAccess=PRIVATE", @@ -241,7 +241,7 @@ public Address() {} "public MinimalDtoCustomBuilder withTags(Consumer> tagsConsumer)", "public MinimalDtoCustomBuilder withProperties(Consumer> propertiesConsumer)"); - // With generateBuilderProvider=false, NO builder consumer methods should be generated + // With generateBuilderConsumer=false, NO builder consumer methods should be generated // Builder consumers include: StringBuilder, collection builders, nested DTO builders ProcessorAsserts.assertNotContaining( generatedCode, @@ -304,19 +304,19 @@ public Address() {} "MinimalDto build()", "static MinimalDtoCustomBuilder create()"); - // With usingArrayListBuilder=false AND generateBuilderProvider=false, NO ArrayListBuilder + // With usingArrayListBuilder=false AND generateBuilderConsumer=false, NO ArrayListBuilder // should be used ProcessorAsserts.assertNotContaining( generatedCode, "MinimalDtoCustomBuilder withItems(Consumer> itemsBuilderConsumer)"); - // With usingHashSetBuilder=false AND generateBuilderProvider=false, NO HashSetBuilder should be + // With usingHashSetBuilder=false AND generateBuilderConsumer=false, NO HashSetBuilder should be // used ProcessorAsserts.assertNotContaining( generatedCode, "MinimalDtoCustomBuilder withTags(Consumer> tagsBuilderConsumer)"); - // With usingHashMapBuilder=false AND generateBuilderProvider=false, NO HashMapBuilder should be + // With usingHashMapBuilder=false AND generateBuilderConsumer=false, NO HashMapBuilder should be // used ProcessorAsserts.assertNotContaining( generatedCode, @@ -360,7 +360,7 @@ void configurationMerge_MustRespectPriority() { BuilderConfiguration override = BuilderConfiguration.builder() .generateSupplier(OptionState.DISABLED) // Override - .generateBuilderProvider(OptionState.DISABLED) // New value + .generateBuilderConsumer(OptionState.DISABLED) // New value .setterSuffix("with") // Override setterSuffix // generateConsumer not set, should keep base value .build(); @@ -377,7 +377,7 @@ void configurationMerge_MustRespectPriority() { merged.generateFieldConsumer(), "Base value should be kept when override is UNSET"); assertEquals( - OptionState.DISABLED, merged.generateBuilderProvider(), "Override should set new value"); + OptionState.DISABLED, merged.generateBuilderConsumer(), "Override should set new value"); assertEquals( AccessModifier.PUBLIC, merged.getBuilderAccess(), From 71e0e424c2de269213a8d7456c70eba5db53737a Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Fri, 14 Nov 2025 20:38:19 +0100 Subject: [PATCH 38/63] Using helper functions of ProcessorTestUtils instead of duplicating it --- .../processor/BuilderProcessorTest.java | 13 +++-------- .../processor/ConditionalExecutionTest.java | 22 +++++-------------- .../ConfigurationProcessingTest.java | 7 ++---- .../MethodConflictResolutionTest.java | 3 +-- .../builders/processor/WithInterfaceTest.java | 17 ++++---------- 5 files changed, 15 insertions(+), 47 deletions(-) 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 86bafd00..8f115631 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 @@ -1,7 +1,6 @@ package org.javahelpers.simple.builders.processor; import static com.google.testing.compile.CompilationSubject.assertThat; -import static com.google.testing.compile.Compiler.javac; import static org.javahelpers.simple.builders.processor.testing.ProcessorAsserts.assertGenerationSucceeded; import static org.javahelpers.simple.builders.processor.testing.ProcessorAsserts.contains; import static org.javahelpers.simple.builders.processor.testing.ProcessorAsserts.notContains; @@ -59,10 +58,7 @@ void shouldLogDebugMessagesWhenVerboseModeEnabled() { // When: Compile with verbose=true to enable debug logging Compilation compilation = - javac() - .withProcessors(new BuilderProcessor()) - .withOptions("-Averbose=true") - .compile(sourceFile); + ProcessorTestUtils.createCompiler().withOptions("-Averbose=true").compile(sourceFile); // Then: Compilation succeeds and debug messages are present assertThat(compilation).succeeded(); @@ -1293,7 +1289,7 @@ public HasSupplierBuilder name(Supplier nameSupplier) { } protected Compilation compile(JavaFileObject... sourceFiles) { - return javac().withProcessors(new BuilderProcessor()).compile(sourceFiles); + return ProcessorTestUtils.createCompiler().compile(sourceFiles); } @Test @@ -1310,10 +1306,7 @@ public class ForcedOldRelease { public ForcedOldRelease() {} } // When: compile with a lower language level to simulate older Java (no production code change) Compilation compilation = - javac() - .withProcessors(new BuilderProcessor()) - .withOptions("--release", "11") - .compile(source); + ProcessorTestUtils.createCompiler().withOptions("--release", "11").compile(source); // Then: compilation must fail with the expected error assertThat(compilation).failed(); diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConditionalExecutionTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConditionalExecutionTest.java index c6e917a0..d171ed8e 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConditionalExecutionTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConditionalExecutionTest.java @@ -6,9 +6,9 @@ import static org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils.printDiagnosticsOnVerbose; import com.google.testing.compile.Compilation; -import com.google.testing.compile.JavaFileObjects; import javax.tools.JavaFileObject; import org.javahelpers.simple.builders.processor.testing.ProcessorAsserts; +import org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils; import org.junit.jupiter.api.Test; /** Tests for conditional execution feature in generated builders. */ @@ -22,11 +22,8 @@ private Compilation compileSources(JavaFileObject... sources) { @Test void conditionalMethod_generatedInBuilder() { - String packageName = "test.conditional"; - JavaFileObject person = - JavaFileObjects.forSourceString( - packageName + ".Person", + ProcessorTestUtils.forSource( """ package test.conditional; import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; @@ -83,11 +80,8 @@ public PersonBuilder conditional(BooleanSupplier condition, Consumer @Test void conditionalMethod_returnsCorrectBuilderType() { - String packageName = "test.conditional.returntype"; - JavaFileObject config = - JavaFileObjects.forSourceString( - packageName + ".Config", + ProcessorTestUtils.forSource( """ package test.conditional.returntype; import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; @@ -192,11 +183,8 @@ public ConfigBuilder conditional(BooleanSupplier condition, Consumer Date: Fri, 14 Nov 2025 20:49:55 +0100 Subject: [PATCH 39/63] Adding tests for configuration reading in context of generation --- .../BuilderConfigurationReaderTest.java | 428 ++++++++++++++++++ .../CompilerArgumentsReaderTest.java | 427 +++++++++++++++++ .../testing/ProcessingEnvironmentStub.java | 155 +++++++ 3 files changed, 1010 insertions(+) create mode 100644 processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderConfigurationReaderTest.java create mode 100644 processor/src/test/java/org/javahelpers/simple/builders/processor/CompilerArgumentsReaderTest.java create mode 100644 processor/src/test/java/org/javahelpers/simple/builders/processor/testing/ProcessingEnvironmentStub.java 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 new file mode 100644 index 00000000..f346b71a --- /dev/null +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderConfigurationReaderTest.java @@ -0,0 +1,428 @@ +/* + * MIT License + * + * Copyright (c) 2025 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.processor; + +import static com.google.testing.compile.CompilationSubject.assertThat; + +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; + +import javax.tools.JavaFileObject; +import org.javahelpers.simple.builders.processor.testing.ProcessorAsserts; +import org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils; +import org.junit.jupiter.api.Test; + +/** + * Integration tests for {@link + * org.javahelpers.simple.builders.processor.util.BuilderConfigurationReader}. + * + *

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

      + *
    • Direct {@code @SimpleBuilder.Options} annotations + *
    • Template annotations ({@code @SimpleBuilder.Template}) + *
    • Configuration resolution with proper priority chain + *
    • Compiler arguments + *
    + * + *

    These are integration tests that verify BuilderConfigurationReader by checking generated + * builder code reflects the correct configuration values. + */ +class BuilderConfigurationReaderTest { + + /** + * Test: Builder respects configuration from @SimpleBuilder.Options annotation. + * + *

    Verifies BuilderConfigurationReader.readFromOptions() correctly reads and applies all + * options. + */ + @Test + void readFromOptions_WithOptionsAnnotation_AppliesAllOptions() { + // Given: A DTO with comprehensive @SimpleBuilder.Options + JavaFileObject source = + ProcessorTestUtils.forSource( + """ + package test; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + import org.javahelpers.simple.builders.core.enums.OptionState; + import org.javahelpers.simple.builders.core.enums.AccessModifier; + + @SimpleBuilder + @SimpleBuilder.Options( + generateFieldSupplier = OptionState.DISABLED, + generateFieldConsumer = OptionState.DISABLED, + generateBuilderConsumer = OptionState.DISABLED, + generateVarArgsHelpers = OptionState.DISABLED, + builderAccess = AccessModifier.PACKAGE_PRIVATE, + methodAccess = AccessModifier.PACKAGE_PRIVATE, + builderSuffix = "Factory", + setterSuffix = "with" + ) + public class PersonDto { + private String name; + private java.util.List tags; + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public java.util.List getTags() { return tags; } + public void setTags(java.util.List tags) { this.tags = tags; } + } + """); + + // When: Compile + Compilation compilation = ProcessorTestUtils.createCompiler().compile(source); + + // Then: Generated code reflects options + assertThat(compilation).succeeded(); + + String generatedCode = ProcessorTestUtils.loadGeneratedSource(compilation, "PersonDtoFactory"); + + // Verify builderSuffix="Factory" + ProcessorAsserts.assertContaining(generatedCode, "class PersonDtoFactory"); + + // Verify setterSuffix="with" + ProcessorAsserts.assertContaining(generatedCode, "PersonDtoFactory withName(String name)"); + ProcessorAsserts.assertContaining(generatedCode, "PersonDtoFactory withTags("); + + // Verify builderAccess=PACKAGE_PRIVATE (no "public" before class) + ProcessorAsserts.assertNotContaining(generatedCode, "public class PersonDtoFactory"); + ProcessorAsserts.assertContaining(generatedCode, "class PersonDtoFactory"); + + // Verify methodAccess=PACKAGE_PRIVATE (no "public" before methods) + ProcessorAsserts.assertNotContaining(generatedCode, "public PersonDtoFactory withName"); + + // Verify generateFieldSupplier=DISABLED (no Supplier methods) + ProcessorAsserts.assertNotContaining(generatedCode, "Supplier"); + + // Verify generateFieldConsumer=DISABLED (no Consumer methods for List) + ProcessorAsserts.assertNotContaining(generatedCode, "Consumer>"); + + // Verify generateBuilderConsumer=DISABLED (no builder consumers) + ProcessorAsserts.assertNotContaining(generatedCode, "ConsumerVerifies BuilderConfigurationReader.readFromTemplate() correctly detects and applies + * template configuration. + */ + @Test + void readFromTemplate_WithTemplateAnnotation_AppliesTemplateConfiguration() { + // Given: A custom template annotation + JavaFileObject templateAnnotation = + JavaFileObjects.forSourceString( + "test.MinimalBuilder", + """ + package test; + import java.lang.annotation.*; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + import org.javahelpers.simple.builders.core.enums.OptionState; + + @SimpleBuilder.Template(options = @SimpleBuilder.Options( + generateFieldSupplier = OptionState.DISABLED, + generateFieldConsumer = OptionState.DISABLED, + generateBuilderConsumer = OptionState.DISABLED, + generateVarArgsHelpers = OptionState.DISABLED, + builderSuffix = "MiniBuilder", + setterSuffix = "set" + )) + @Retention(RetentionPolicy.CLASS) + @Target(ElementType.TYPE) + public @interface MinimalBuilder { + } + """); + + JavaFileObject dtoSource = + ProcessorTestUtils.forSource( + """ + package test; + + @MinimalBuilder + public class PersonDto { + private String name; + private int age; + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public int getAge() { return age; } + public void setAge(int age) { this.age = age; } + } + """); + + // When: Compile + Compilation compilation = + ProcessorTestUtils.createCompiler().compile(templateAnnotation, dtoSource); + + // Then: Generated code reflects template configuration + assertThat(compilation).succeeded(); + + String generatedCode = + ProcessorTestUtils.loadGeneratedSource(compilation, "PersonDtoMiniBuilder"); + + // Verify template values are applied + ProcessorAsserts.assertContaining(generatedCode, "class PersonDtoMiniBuilder"); + ProcessorAsserts.assertContaining( + generatedCode, "PersonDtoMiniBuilder setName(String name)"); + ProcessorAsserts.assertContaining(generatedCode, "PersonDtoMiniBuilder setAge(int age)"); + + // Verify disabled features + ProcessorAsserts.assertNotContaining(generatedCode, "Supplier<"); + ProcessorAsserts.assertNotContaining(generatedCode, "Consumer<"); + } + + /** + * Test: Options annotation overrides template annotation (proper priority). + * + *

    Verifies BuilderConfigurationReader.resolveConfiguration() applies correct priority: Options + * > Template > Compiler args > Defaults + */ + @Test + void resolveConfiguration_OptionsOverridesTemplate_AppliesPriorityCorrectly() { + // Given: Both template and options specified + JavaFileObject templateAnnotation = + ProcessorTestUtils.forSource( + """ + package test; + import java.lang.annotation.*; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + + @SimpleBuilder.Template(options = @SimpleBuilder.Options( + builderSuffix = "TemplateBuilder", + setterSuffix = "with" + )) + @Retention(RetentionPolicy.CLASS) + @Target(ElementType.TYPE) + public @interface CustomBuilder { + } + """); + + JavaFileObject dtoSource = + ProcessorTestUtils.forSource( + """ + package test; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + + @CustomBuilder + @SimpleBuilder.Options( + builderSuffix = "OptionsBuilder", + setterSuffix = "set" + ) + public class PersonDto { + private String name; + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + } + """); + + // When: Compile + Compilation compilation = + ProcessorTestUtils.createCompiler().compile(templateAnnotation, dtoSource); + + // Then: Options wins over template + assertThat(compilation).succeeded(); + + String generatedCode = + ProcessorTestUtils.loadGeneratedSource(compilation, "PersonDtoOptionsBuilder"); + + // Verify options values (not template values) + ProcessorAsserts.assertContaining(generatedCode, "class PersonDtoOptionsBuilder"); + ProcessorAsserts.assertContaining( + generatedCode, "PersonDtoOptionsBuilder setName(String name)"); + + // Template values should NOT be present + ProcessorAsserts.assertNotContaining(generatedCode, "TemplateBuilder"); + ProcessorAsserts.assertNotContaining(generatedCode, "withName"); + } + + /** + * Test: Compiler arguments apply when no annotation configuration present. + * + *

    Verifies BuilderConfigurationReader correctly reads and applies compiler arguments via + * CompilerArgumentsReader integration. + */ + @Test + void resolveConfiguration_WithCompilerArgsOnly_AppliesCompilerArgs() { + // Given: Simple @SimpleBuilder with no options or template + JavaFileObject source = + ProcessorTestUtils.forSource( + """ + package test; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + + @SimpleBuilder + public class PersonDto { + private String name; + private java.util.List tags; + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public java.util.List getTags() { return tags; } + public void setTags(java.util.List tags) { this.tags = tags; } + } + """); + + // When: Compile with compiler arguments + Compilation compilation = + ProcessorTestUtils.createCompiler() + .withOptions( + "-Asimplebuilder.builderSuffix=CustomBuilder", + "-Asimplebuilder.setterSuffix=set", + "-Asimplebuilder.generateVarArgsHelpers=false") + .compile(source); + + // Then: Compiler args are applied + assertThat(compilation).succeeded(); + + String generatedCode = + ProcessorTestUtils.loadGeneratedSource(compilation, "PersonDtoCustomBuilder"); + + ProcessorAsserts.assertContaining(generatedCode, "class PersonDtoCustomBuilder"); + ProcessorAsserts.assertContaining(generatedCode, "PersonDtoCustomBuilder setName(String name)"); + ProcessorAsserts.assertContaining(generatedCode, "PersonDtoCustomBuilder setTags("); + + // VarArgs disabled by compiler arg + ProcessorAsserts.assertNotContaining(generatedCode, "setTags(String... tags)"); + } + + /** Test: Options override compiler arguments (proper priority). */ + @Test + void resolveConfiguration_OptionsOverridesCompilerArgs_AppliesPriorityCorrectly() { + // Given: Both compiler args and options specified + JavaFileObject source = + ProcessorTestUtils.forSource( + """ + package test; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + + @SimpleBuilder + @SimpleBuilder.Options( + builderSuffix = "OptionsBuilder" + ) + public class PersonDto { + private String name; + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + } + """); + + // When: Compile with conflicting compiler argument + Compilation compilation = + ProcessorTestUtils.createCompiler() + .withOptions("-Asimplebuilder.builderSuffix=CompilerArgBuilder") + .compile(source); + + // Then: Options wins over compiler arg + assertThat(compilation).succeeded(); + + String generatedCode = + ProcessorTestUtils.loadGeneratedSource(compilation, "PersonDtoOptionsBuilder"); + + // Verify options value (not compiler arg value) + ProcessorAsserts.assertContaining(generatedCode, "class PersonDtoOptionsBuilder"); + ProcessorAsserts.assertNotContaining(generatedCode, "CompilerArgBuilder"); + } + + /** + * Test: All layers work together in complete priority chain. + * + *

    Verifies the complete configuration resolution: Options > Template > CompilerArgs > Defaults + */ + @Test + void resolveConfiguration_AllLayersTogether_CompleteChain() { + // Given: All configuration sources present + JavaFileObject templateAnnotation = + ProcessorTestUtils.forSource( + """ + package test; + import java.lang.annotation.*; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + import org.javahelpers.simple.builders.core.enums.OptionState; + + @SimpleBuilder.Template(options = @SimpleBuilder.Options( + generateFieldSupplier = OptionState.DISABLED, + setterSuffix = "with" + )) + @Retention(RetentionPolicy.CLASS) + @Target(ElementType.TYPE) + public @interface TemplatedBuilder { + } + """); + + JavaFileObject dtoSource = + ProcessorTestUtils.forSource( + """ + package test; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + import org.javahelpers.simple.builders.core.enums.OptionState; + + @TemplatedBuilder + @SimpleBuilder + @SimpleBuilder.Options( + generateVarArgsHelpers = OptionState.DISABLED + ) + public class PersonDto { + private String name; + private java.util.List tags; + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public java.util.List getTags() { return tags; } + public void setTags(java.util.List tags) { this.tags = tags; } + } + """); + + // When: Compile with compiler args + Compilation compilation = + ProcessorTestUtils.createCompiler() + .withOptions("-Asimplebuilder.builderSuffix=CompilerBuilder") + .compile(templateAnnotation, dtoSource); + + // Then: Layered configuration is applied correctly + assertThat(compilation).succeeded(); + + String generatedCode = + ProcessorTestUtils.loadGeneratedSource(compilation, "PersonDtoCompilerBuilder"); + + // Options wins for generateVarArgsHelpers + ProcessorAsserts.assertNotContaining(generatedCode, "withTags(String... tags)"); + + // Template wins for setterSuffix (not overridden by options) + ProcessorAsserts.assertContaining(generatedCode, "withName(String name)"); + ProcessorAsserts.assertContaining(generatedCode, "withTags("); + + // Template wins for generateFieldSupplier + ProcessorAsserts.assertNotContaining(generatedCode, "Supplier"); + + // Compiler arg wins for builderSuffix (not overridden by template or options) + ProcessorAsserts.assertContaining(generatedCode, "class PersonDtoCompilerBuilder"); + } +} diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/CompilerArgumentsReaderTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/CompilerArgumentsReaderTest.java new file mode 100644 index 00000000..08c4a338 --- /dev/null +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/CompilerArgumentsReaderTest.java @@ -0,0 +1,427 @@ +/* + * MIT License + * + * Copyright (c) 2025 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.processor; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.HashMap; +import java.util.Map; +import javax.annotation.processing.ProcessingEnvironment; +import org.javahelpers.simple.builders.core.enums.AccessModifier; +import org.javahelpers.simple.builders.core.enums.OptionState; +import org.javahelpers.simple.builders.processor.dtos.BuilderConfiguration; +import org.javahelpers.simple.builders.processor.enums.CompilerArgumentsEnum; +import org.javahelpers.simple.builders.processor.testing.ProcessingEnvironmentStub; +import org.javahelpers.simple.builders.processor.util.CompilerArgumentsReader; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +/** + * Unit tests for {@link CompilerArgumentsReader} focusing on edge cases. + * + *

    Tests null values, empty strings, case sensitivity, invalid inputs, and backward + * compatibility. + */ +class CompilerArgumentsReaderTest { + + /** Test: readValue returns null when argument not set. */ + @Test + void readValue_NotSet_ReturnsNull() { + ProcessingEnvironment env = ProcessingEnvironmentStub.createEmpty(); + CompilerArgumentsReader reader = new CompilerArgumentsReader(env); + + assertNull( + reader.readValue(CompilerArgumentsEnum.BUILDER_SUFFIX), + "Should return null when argument not set"); + } + + /** Test: readValue prefers full compiler argument name over simple option name. */ + @Test + void readValue_BothNamesSet_PrefersFullName() { + ProcessingEnvironment env = + ProcessingEnvironmentStub.builder() + .put("simplebuilder.builderSuffix", "FullName") + .put("builderSuffix", "SimpleName") + .build(); + CompilerArgumentsReader reader = new CompilerArgumentsReader(env); + + assertEquals( + "FullName", + reader.readValue(CompilerArgumentsEnum.BUILDER_SUFFIX), + "Should prefer full compiler argument name"); + } + + /** Test: readValue falls back to simple option name for backward compatibility. */ + @Test + void readValue_OnlySimpleNameSet_UsesSimpleName() { + Map options = new HashMap<>(); + options.put("builderSuffix", "SimpleName"); + + ProcessingEnvironment env = ProcessingEnvironmentStub.create(options); + CompilerArgumentsReader reader = new CompilerArgumentsReader(env); + + assertEquals( + "SimpleName", + reader.readValue(CompilerArgumentsEnum.BUILDER_SUFFIX), + "Should fall back to simple option name"); + } + + /** Test: readBooleanValue returns false when value is null. */ + @Test + void readBooleanValue_NullValue_ReturnsFalse() { + ProcessingEnvironment env = ProcessingEnvironmentStub.createEmpty(); + CompilerArgumentsReader reader = new CompilerArgumentsReader(env); + + assertFalse( + reader.readBooleanValue(CompilerArgumentsEnum.GENERATE_FIELD_SUPPLIER), + "Should return false for null value"); + } + + /** Test: readBooleanValue returns false for empty string. */ + @Test + void readBooleanValue_EmptyString_ReturnsFalse() { + Map options = new HashMap<>(); + options.put("simplebuilder.generateFieldSupplier", ""); + + ProcessingEnvironment env = ProcessingEnvironmentStub.create(options); + CompilerArgumentsReader reader = new CompilerArgumentsReader(env); + + assertFalse( + reader.readBooleanValue(CompilerArgumentsEnum.GENERATE_FIELD_SUPPLIER), + "Should return false for empty string"); + } + + /** Test: readBooleanValue handles case-insensitive "true". */ + @ParameterizedTest + @ValueSource(strings = {"true", "TRUE", "True", "TrUe"}) + void readBooleanValue_CaseInsensitiveTrue_ReturnsTrue(String value) { + Map options = new HashMap<>(); + options.put("simplebuilder.generateFieldSupplier", value); + + ProcessingEnvironment env = ProcessingEnvironmentStub.create(options); + CompilerArgumentsReader reader = new CompilerArgumentsReader(env); + + assertTrue( + reader.readBooleanValue(CompilerArgumentsEnum.GENERATE_FIELD_SUPPLIER), + "Should return true for: " + value); + } + + /** Test: readBooleanValue handles case-insensitive "enabled". */ + @ParameterizedTest + @ValueSource(strings = {"enabled", "ENABLED", "Enabled", "EnAbLeD"}) + void readBooleanValue_CaseInsensitiveEnabled_ReturnsTrue(String value) { + Map options = new HashMap<>(); + options.put("simplebuilder.generateFieldSupplier", value); + + ProcessingEnvironment env = ProcessingEnvironmentStub.create(options); + CompilerArgumentsReader reader = new CompilerArgumentsReader(env); + + assertTrue( + reader.readBooleanValue(CompilerArgumentsEnum.GENERATE_FIELD_SUPPLIER), + "Should return true for: " + value); + } + + /** Test: readBooleanValue returns false for invalid values. */ + @ParameterizedTest + @ValueSource(strings = {"false", "disabled", "yes", "1", "on", "invalid"}) + void readBooleanValue_InvalidValues_ReturnsFalse(String value) { + Map options = new HashMap<>(); + options.put("simplebuilder.generateFieldSupplier", value); + + ProcessingEnvironment env = ProcessingEnvironmentStub.create(options); + CompilerArgumentsReader reader = new CompilerArgumentsReader(env); + + assertFalse( + reader.readBooleanValue(CompilerArgumentsEnum.GENERATE_FIELD_SUPPLIER), + "Should return false for: " + value); + } + + /** Test: readOptionState returns UNSET when value is null. */ + @Test + void readOptionState_NullValue_ReturnsUnset() { + ProcessingEnvironment env = ProcessingEnvironmentStub.createEmpty(); + CompilerArgumentsReader reader = new CompilerArgumentsReader(env); + + assertEquals( + OptionState.UNSET, + reader.readOptionState(CompilerArgumentsEnum.GENERATE_FIELD_SUPPLIER), + "Should return UNSET for null value"); + } + + /** Test: readOptionState returns UNSET for empty string. */ + @Test + void readOptionState_EmptyString_ReturnsUnset() { + Map options = new HashMap<>(); + options.put("simplebuilder.generateFieldSupplier", ""); + + ProcessingEnvironment env = ProcessingEnvironmentStub.create(options); + CompilerArgumentsReader reader = new CompilerArgumentsReader(env); + + assertEquals( + OptionState.UNSET, + reader.readOptionState(CompilerArgumentsEnum.GENERATE_FIELD_SUPPLIER), + "Should return UNSET for empty string"); + } + + /** Test: readOptionState handles case-insensitive "true" and "enabled". */ + @ParameterizedTest + @ValueSource(strings = {"true", "TRUE", "enabled", "ENABLED", "Enabled"}) + void readOptionState_TrueOrEnabled_ReturnsEnabled(String value) { + Map options = new HashMap<>(); + options.put("simplebuilder.generateFieldSupplier", value); + + ProcessingEnvironment env = ProcessingEnvironmentStub.create(options); + CompilerArgumentsReader reader = new CompilerArgumentsReader(env); + + assertEquals( + OptionState.ENABLED, + reader.readOptionState(CompilerArgumentsEnum.GENERATE_FIELD_SUPPLIER), + "Should return ENABLED for: " + value); + } + + /** Test: readOptionState handles case-insensitive "false" and "disabled". */ + @ParameterizedTest + @ValueSource(strings = {"false", "FALSE", "False", "disabled", "DISABLED", "Disabled"}) + void readOptionState_FalseOrDisabled_ReturnsDisabled(String value) { + Map options = new HashMap<>(); + options.put("simplebuilder.generateFieldSupplier", value); + + ProcessingEnvironment env = ProcessingEnvironmentStub.create(options); + CompilerArgumentsReader reader = new CompilerArgumentsReader(env); + + assertEquals( + OptionState.DISABLED, + reader.readOptionState(CompilerArgumentsEnum.GENERATE_FIELD_SUPPLIER), + "Should return DISABLED for: " + value); + } + + /** Test: readOptionState returns UNSET for invalid values. */ + @ParameterizedTest + @ValueSource(strings = {"yes", "no", "1", "0", "on", "off", "invalid"}) + void readOptionState_InvalidValues_ReturnsUnset(String value) { + Map options = new HashMap<>(); + options.put("simplebuilder.generateFieldSupplier", value); + + ProcessingEnvironment env = ProcessingEnvironmentStub.create(options); + CompilerArgumentsReader reader = new CompilerArgumentsReader(env); + + assertEquals( + OptionState.UNSET, + reader.readOptionState(CompilerArgumentsEnum.GENERATE_FIELD_SUPPLIER), + "Should return UNSET for invalid value: " + value); + } + + /** Test: readAccessModifier returns DEFAULT when value is null. */ + @Test + void readAccessModifier_NullValue_ReturnsDefault() { + ProcessingEnvironment env = ProcessingEnvironmentStub.createEmpty(); + CompilerArgumentsReader reader = new CompilerArgumentsReader(env); + + assertEquals( + AccessModifier.DEFAULT, + reader.readAccessModifier(CompilerArgumentsEnum.BUILDER_ACCESS), + "Should return DEFAULT for null value"); + } + + /** Test: readAccessModifier returns DEFAULT for empty string. */ + @Test + void readAccessModifier_EmptyString_ReturnsDefault() { + Map options = new HashMap<>(); + options.put("simplebuilder.builderAccess", ""); + + ProcessingEnvironment env = ProcessingEnvironmentStub.create(options); + CompilerArgumentsReader reader = new CompilerArgumentsReader(env); + + assertEquals( + AccessModifier.DEFAULT, + reader.readAccessModifier(CompilerArgumentsEnum.BUILDER_ACCESS), + "Should return DEFAULT for empty string"); + } + + /** Test: readAccessModifier handles case-insensitive "public". */ + @ParameterizedTest + @ValueSource(strings = {"public", "PUBLIC", "Public", "PuBlIc"}) + void readAccessModifier_CaseInsensitivePublic_ReturnsPublic(String value) { + Map options = new HashMap<>(); + options.put("simplebuilder.builderAccess", value); + + ProcessingEnvironment env = ProcessingEnvironmentStub.create(options); + CompilerArgumentsReader reader = new CompilerArgumentsReader(env); + + assertEquals( + AccessModifier.PUBLIC, + reader.readAccessModifier(CompilerArgumentsEnum.BUILDER_ACCESS), + "Should return PUBLIC for: " + value); + } + + /** Test: readAccessModifier handles case-insensitive "private". */ + @ParameterizedTest + @ValueSource(strings = {"private", "PRIVATE", "Private", "PrIvAtE"}) + void readAccessModifier_CaseInsensitivePrivate_ReturnsPrivate(String value) { + Map options = new HashMap<>(); + options.put("simplebuilder.builderAccess", value); + + ProcessingEnvironment env = ProcessingEnvironmentStub.create(options); + CompilerArgumentsReader reader = new CompilerArgumentsReader(env); + + assertEquals( + AccessModifier.PRIVATE, + reader.readAccessModifier(CompilerArgumentsEnum.BUILDER_ACCESS), + "Should return PRIVATE for: " + value); + } + + /** Test: readAccessModifier handles both "package-private" and "package_private". */ + @ParameterizedTest + @ValueSource( + strings = { + "package-private", + "PACKAGE-PRIVATE", + "Package-Private", + "package_private", + "PACKAGE_PRIVATE", + "Package_Private" + }) + void readAccessModifier_PackagePrivateVariants_ReturnsPackagePrivate(String value) { + Map options = new HashMap<>(); + options.put("simplebuilder.builderAccess", value); + + ProcessingEnvironment env = ProcessingEnvironmentStub.create(options); + CompilerArgumentsReader reader = new CompilerArgumentsReader(env); + + assertEquals( + AccessModifier.PACKAGE_PRIVATE, + reader.readAccessModifier(CompilerArgumentsEnum.BUILDER_ACCESS), + "Should return PACKAGE_PRIVATE for: " + value); + } + + /** Test: readAccessModifier returns DEFAULT for invalid values. */ + @ParameterizedTest + @ValueSource(strings = {"protected", "default", "package", "invalid", "123"}) + void readAccessModifier_InvalidValues_ReturnsDefault(String value) { + Map options = new HashMap<>(); + options.put("simplebuilder.builderAccess", value); + + ProcessingEnvironment env = ProcessingEnvironmentStub.create(options); + CompilerArgumentsReader reader = new CompilerArgumentsReader(env); + + assertEquals( + AccessModifier.DEFAULT, + reader.readAccessModifier(CompilerArgumentsEnum.BUILDER_ACCESS), + "Should return DEFAULT for invalid value: " + value); + } + + /** Test: readBuilderConfiguration with no arguments returns all UNSET/DEFAULT values. */ + @Test + void readBuilderConfiguration_NoArguments_ReturnsDefaults() { + ProcessingEnvironment env = ProcessingEnvironmentStub.createEmpty(); + CompilerArgumentsReader reader = new CompilerArgumentsReader(env); + + BuilderConfiguration config = reader.readBuilderConfiguration(); + + assertNotNull(config, "Configuration should not be null"); + assertEquals(OptionState.UNSET, config.generateFieldSupplier()); + assertEquals(OptionState.UNSET, config.generateFieldConsumer()); + assertEquals(OptionState.UNSET, config.generateBuilderConsumer()); + assertEquals(AccessModifier.DEFAULT, config.getBuilderAccess()); + assertEquals(AccessModifier.DEFAULT, config.getBuilderConstructorAccess()); + assertEquals(AccessModifier.DEFAULT, config.getMethodAccess()); + assertNull(config.getBuilderSuffix(), "Builder suffix should be null when not set"); + assertNull(config.getSetterSuffix(), "Setter suffix should be null when not set"); + } + + /** Test: readBuilderConfiguration reads all options correctly. */ + @Test + void readBuilderConfiguration_AllOptionsSet_ReadsCorrectly() { + ProcessingEnvironment env = + ProcessingEnvironmentStub.builder() + .put("simplebuilder.generateFieldSupplier", "true") + .put("simplebuilder.generateFieldConsumer", "false") + .put("simplebuilder.generateBuilderConsumer", "enabled") + .put("simplebuilder.builderAccess", "public") + .put("simplebuilder.builderConstructorAccess", "private") + .put("simplebuilder.methodAccess", "package-private") + .put("simplebuilder.generateVarArgsHelpers", "disabled") + .put("simplebuilder.builderSuffix", "Factory") + .put("simplebuilder.setterSuffix", "with") + .build(); + CompilerArgumentsReader reader = new CompilerArgumentsReader(env); + + BuilderConfiguration config = reader.readBuilderConfiguration(); + + assertEquals(OptionState.ENABLED, config.generateFieldSupplier()); + assertEquals(OptionState.DISABLED, config.generateFieldConsumer()); + assertEquals(OptionState.ENABLED, config.generateBuilderConsumer()); + assertEquals(AccessModifier.PUBLIC, config.getBuilderAccess()); + assertEquals(AccessModifier.PRIVATE, config.getBuilderConstructorAccess()); + assertEquals(AccessModifier.PACKAGE_PRIVATE, config.getMethodAccess()); + assertEquals(OptionState.DISABLED, config.generateVarArgsHelpers()); + assertEquals("Factory", config.getBuilderSuffix()); + assertEquals("with", config.getSetterSuffix()); + } + + /** Test: readBuilderConfiguration handles mixed valid and invalid values. */ + @Test + void readBuilderConfiguration_MixedValidInvalid_HandlesGracefully() { + ProcessingEnvironment env = + ProcessingEnvironmentStub.builder() + .put("simplebuilder.generateFieldSupplier", "invalid") + .put("simplebuilder.builderAccess", "protected") // invalid - no PROTECTED anymore + .put("simplebuilder.generateFieldConsumer", "true") + .build(); + CompilerArgumentsReader reader = new CompilerArgumentsReader(env); + + BuilderConfiguration config = reader.readBuilderConfiguration(); + + assertEquals( + OptionState.UNSET, config.generateFieldSupplier(), "Invalid option should be UNSET"); + assertEquals( + AccessModifier.DEFAULT, + config.getBuilderAccess(), + "Invalid access modifier should be DEFAULT"); + assertEquals( + OptionState.ENABLED, config.generateFieldConsumer(), "Valid option should be ENABLED"); + } + + /** Test: readBuilderConfiguration with empty string values. */ + @Test + void readBuilderConfiguration_EmptyStringValues_HandlesGracefully() { + ProcessingEnvironment env = + ProcessingEnvironmentStub.builder() + .put("simplebuilder.generateFieldSupplier", "") + .put("simplebuilder.builderAccess", "") + .put("simplebuilder.builderSuffix", "") + .put("simplebuilder.setterSuffix", "") + .build(); + CompilerArgumentsReader reader = new CompilerArgumentsReader(env); + + BuilderConfiguration config = reader.readBuilderConfiguration(); + + assertEquals(OptionState.UNSET, config.generateFieldSupplier(), "Empty should be UNSET"); + assertEquals(AccessModifier.DEFAULT, config.getBuilderAccess(), "Empty should be DEFAULT"); + assertEquals("", config.getBuilderSuffix(), "Empty string should be preserved for suffix"); + assertEquals("", config.getSetterSuffix(), "Empty string should be preserved for suffix"); + } +} diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/testing/ProcessingEnvironmentStub.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/testing/ProcessingEnvironmentStub.java new file mode 100644 index 00000000..86277c33 --- /dev/null +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/testing/ProcessingEnvironmentStub.java @@ -0,0 +1,155 @@ +/* + * MIT License + * + * Copyright (c) 2025 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.processor.testing; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import javax.annotation.processing.Filer; +import javax.annotation.processing.Messager; +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.SourceVersion; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; + +/** + * Utility class providing stub implementations of {@link ProcessingEnvironment} for unit testing. + * + *

    This class provides utility methods to create minimal ProcessingEnvironment stubs with + * configurable options, useful for testing annotation processors without requiring full compilation + * infrastructure. + */ +public final class ProcessingEnvironmentStub { + + private ProcessingEnvironmentStub() { + // Utility class - prevent instantiation + } + + /** + * Creates a stub ProcessingEnvironment with the specified compiler options. + * + *

    All other methods (getMessager, getFiler, etc.) return null or default values. Use this for + * testing scenarios where only the options map is needed. + * + * @param options the compiler options map to return from {@code getOptions()} + * @return a minimal ProcessingEnvironment stub + */ + public static ProcessingEnvironment create(Map options) { + return new ProcessingEnvironment() { + @Override + public Map getOptions() { + return options != null ? options : Collections.emptyMap(); + } + + @Override + public Messager getMessager() { + return null; + } + + @Override + public Filer getFiler() { + return null; + } + + @Override + public Elements getElementUtils() { + return null; + } + + @Override + public Types getTypeUtils() { + return null; + } + + @Override + public SourceVersion getSourceVersion() { + return SourceVersion.latestSupported(); + } + + @Override + public Locale getLocale() { + return Locale.getDefault(); + } + }; + } + + /** + * Creates a stub ProcessingEnvironment with an empty options map. + * + * @return a minimal ProcessingEnvironment stub with no options + */ + public static ProcessingEnvironment createEmpty() { + return create(Collections.emptyMap()); + } + + /** + * Creates a new builder for constructing a ProcessingEnvironment stub with options. + * + * @return a new Builder instance + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder for creating ProcessingEnvironment stubs with a fluent API. + * + *

    Example usage: + * + *

    {@code
    +   * ProcessingEnvironment env = ProcessingEnvironmentStub.builder()
    +   *     .put("simplebuilder.builderSuffix", "Builder")
    +   *     .put("simplebuilder.generateFieldSupplier", "true")
    +   *     .build();
    +   * }
    + */ + public static final class Builder { + private final Map options = new HashMap<>(); + + private Builder() {} + + /** + * Adds an option to the ProcessingEnvironment stub. + * + * @param name the option name + * @param value the option value + * @return this builder for method chaining + */ + public Builder put(String name, String value) { + options.put(name, value); + return this; + } + + /** + * Builds and returns the ProcessingEnvironment stub with the configured options. + * + * @return a ProcessingEnvironment stub + */ + public ProcessingEnvironment build() { + return create(new HashMap<>(options)); + } + } +} From b3b6db38d908f9b58e6aab616c22161a4dc16669 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Fri, 14 Nov 2025 20:50:12 +0100 Subject: [PATCH 40/63] Removing unused setter for configuration on ProcessingContext --- .../builders/processor/util/ProcessingContext.java | 9 --------- 1 file changed, 9 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ProcessingContext.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ProcessingContext.java index 70ce896d..23d6ead7 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ProcessingContext.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ProcessingContext.java @@ -79,15 +79,6 @@ public BuilderConfiguration getBuilderConfigurationForElement() { return builderConfigurationForElement; } - /** - * Set the builder configuration for the current element being processed. - * - * @param configuration the builder configuration - */ - public void setBuilderConfigurationForElement(BuilderConfiguration configuration) { - this.builderConfigurationForElement = configuration; - } - /** * Get a type element by its fully qualified class name. * From 8345c5232fb0d1ba1b662a083f2e1ecaebcc4a44 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Fri, 14 Nov 2025 21:14:15 +0100 Subject: [PATCH 41/63] Fixing tests and documentation --- .../processor/dtos/BuilderConfiguration.java | 3 +- .../util/BuilderDefinitionCreator.java | 42 +++++++++++++------ .../processor/util/JavaCodeGenerator.java | 10 ++--- .../BuilderConfigurationReaderTest.java | 15 +++++-- .../ConfigurationProcessingTest.java | 5 +-- 5 files changed, 47 insertions(+), 28 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java index 37d74619..3cad63af 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java @@ -45,7 +45,8 @@ * @param generateConditionalHelper Generate conditional logic methods * @param builderAccess Access level for builder class * @param builderConstructorAccess Access level for builder constructors - * @param methodAccess Access level for builder methods + * @param methodAccess Access level for setter/fluent methods (NOT build() or create() which are + * always public) * @param generateVarArgsHelpers Generate varargs helper methods * @param generateStringFormatHelpers Generate string format helper methods * @param generateUnboxedOptional Generate unboxed optional methods diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java index 0b7c7031..53b6c14b 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java @@ -309,18 +309,20 @@ private static void addAdditionalHelperMethodsForField( setMethodAccessModifier(method1, methodAccessModifier); field.addMethod(method1); - // Add Consumer> method - TypeName collectionBuilderType = map2TypeName(ArrayListBuilder.class); - MethodDto method2 = - createFieldConsumerWithArrayBuilder( - fieldName, - fieldNameInBuilder, - collectionBuilderType, - elementType, - builderType, - context); - setMethodAccessModifier(method2, methodAccessModifier); - field.addMethod(method2); + // Add Consumer> method only if builder consumers are enabled + if (context.getBuilderConfigurationForElement().shouldGenerateBuilderConsumer()) { + TypeName collectionBuilderType = map2TypeName(ArrayListBuilder.class); + MethodDto method2 = + createFieldConsumerWithArrayBuilder( + fieldName, + fieldNameInBuilder, + collectionBuilderType, + elementType, + builderType, + context); + setMethodAccessModifier(method2, methodAccessModifier); + field.addMethod(method2); + } return; } @@ -427,7 +429,7 @@ private static void addConsumerMethodsForField( TypeElement fieldTypeElement, TypeName builderType, ProcessingContext context) { - // Do not generate supplier methods for generic type variables (e.g., T) + // Do not generate consumer methods for generic type variables (e.g., T) if (field.getFieldType() instanceof TypeNameVariable) { return; } @@ -552,6 +554,11 @@ private static boolean tryAddListConsumer( Optional elementBuilderType = resolveBuilderType(elementType, elementTypeMirror, context); + // Only generate builder consumer methods if enabled + if (!context.getBuilderConfigurationForElement().shouldGenerateBuilderConsumer()) { + return false; + } + if (elementBuilderType.isPresent() && context .getBuilderConfigurationForElement() @@ -596,6 +603,10 @@ private static boolean tryAddListConsumer( /** Tries to add Map-specific consumer methods. Returns true if handled. */ private static boolean tryAddMapConsumer( FieldDto field, TypeName builderType, ProcessingContext context) { + // Check if builder consumers are enabled + if (!context.getBuilderConfigurationForElement().shouldGenerateBuilderConsumer()) { + return false; + } // Check if HashMapBuilder is enabled if (!context.getBuilderConfigurationForElement().shouldUseHashMapBuilder()) { return false; @@ -645,6 +656,11 @@ private static boolean tryAddSetConsumer( Optional elementBuilderType = resolveBuilderType(elementType, elementTypeMirror, context); + // Only generate builder consumer methods if enabled + if (!context.getBuilderConfigurationForElement().shouldGenerateBuilderConsumer()) { + return false; + } + if (elementBuilderType.isPresent() && context .getBuilderConfigurationForElement() diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java index e9e429cb..e92d89c4 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java @@ -170,6 +170,8 @@ public void generateBuilder(BuilderDefinitionDto builderDef) throws BuilderExcep } // Adding builder-specific methods + // Note: build() and create() are always PUBLIC for usability and to satisfy interface contracts + // (e.g., IBuilderBase). The methodAccess configuration only applies to setter/fluent methods. classBuilder.addMethod( createMethodBuild( dtoBaseClass, @@ -178,14 +180,10 @@ public void generateBuilder(BuilderDefinitionDto builderDef) throws BuilderExcep builderDef.getSetterFieldsForBuilder(), builderDef.getGenerics(), builderDef.getConfiguration().shouldImplementBuilderBase(), - methodAccessModifier)); + PUBLIC)); classBuilder.addMethod( createMethodStaticCreate( - builderBaseClass, - builderTypeName, - dtoBaseClass, - builderDef.getGenerics(), - methodAccessModifier)); + builderBaseClass, builderTypeName, dtoBaseClass, builderDef.getGenerics(), PUBLIC)); // Add conditional methods only if enabled in configuration if (builderDef.getConfiguration().shouldGenerateConditionalLogic()) { 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 f346b71a..c3116f19 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 @@ -28,7 +28,6 @@ import com.google.testing.compile.Compilation; import com.google.testing.compile.JavaFileObjects; - import javax.tools.JavaFileObject; import org.javahelpers.simple.builders.processor.testing.ProcessorAsserts; import org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils; @@ -102,6 +101,9 @@ public class PersonDto { // Verify builderSuffix="Factory" ProcessorAsserts.assertContaining(generatedCode, "class PersonDtoFactory"); + // Verify class access is PACKAGE_PRIVATE + ProcessorAsserts.assertNotContaining(generatedCode, "public class PersonDtoFactory"); + // Verify setterSuffix="with" ProcessorAsserts.assertContaining(generatedCode, "PersonDtoFactory withName(String name)"); ProcessorAsserts.assertContaining(generatedCode, "PersonDtoFactory withTags("); @@ -110,8 +112,14 @@ public class PersonDto { ProcessorAsserts.assertNotContaining(generatedCode, "public class PersonDtoFactory"); ProcessorAsserts.assertContaining(generatedCode, "class PersonDtoFactory"); - // Verify methodAccess=PACKAGE_PRIVATE (no "public" before methods) + // Verify methodAccess=PACKAGE_PRIVATE (no "public" before setter methods) ProcessorAsserts.assertNotContaining(generatedCode, "public PersonDtoFactory withName"); + ProcessorAsserts.assertContaining(generatedCode, "PersonDtoFactory withName(String name)"); + + // Verify builder methods have public access even with builderAccess=PACKAGE_PRIVATE being + // package private + ProcessorAsserts.assertContaining(generatedCode, "public PersonDto build()"); + ProcessorAsserts.assertContaining(generatedCode, "public static PersonDtoFactory create()"); // Verify generateFieldSupplier=DISABLED (no Supplier methods) ProcessorAsserts.assertNotContaining(generatedCode, "Supplier"); @@ -188,8 +196,7 @@ public class PersonDto { // Verify template values are applied ProcessorAsserts.assertContaining(generatedCode, "class PersonDtoMiniBuilder"); - ProcessorAsserts.assertContaining( - generatedCode, "PersonDtoMiniBuilder setName(String name)"); + ProcessorAsserts.assertContaining(generatedCode, "PersonDtoMiniBuilder setName(String name)"); ProcessorAsserts.assertContaining(generatedCode, "PersonDtoMiniBuilder setAge(int age)"); // Verify disabled features diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java index 3ebf11f0..1c284424 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java @@ -290,10 +290,7 @@ public Address() {} // With methodAccess=PACKAGE_PRIVATE, methods should NOT have public modifier ProcessorAsserts.assertNotContaining( - generatedCode, - "public MinimalDtoCustomBuilder withName(String name)", - "public MinimalDto build()", - "public static MinimalDtoCustomBuilder create()"); + generatedCode, "public MinimalDtoCustomBuilder withName(String name)"); // But package-private methods should exist - with setterSuffix="with", methods are prefixed ProcessorAsserts.assertContaining( From b46faa1c8e3629e356174708745fc9ade1a00a5e Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Fri, 14 Nov 2025 21:57:45 +0100 Subject: [PATCH 42/63] Supporting processing of annotation-interfaces in ProcessorTestUtils --- .../simple/builders/processor/testing/ProcessorTestUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/testing/ProcessorTestUtils.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/testing/ProcessorTestUtils.java index b1810942..ab10e6e0 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/testing/ProcessorTestUtils.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/testing/ProcessorTestUtils.java @@ -193,7 +193,7 @@ private static String extractPackageName(String source) { private static String extractTopLevelTypeName(String source) { Matcher m = Pattern.compile( - "(?m)^\\s*(?:public|protected|private)?(?:\\s+(?:abstract|final|static|sealed|non-sealed|strictfp))*\\s*(?:class|interface|enum|record)\\s+([A-Za-z_]\\w*)\\b") + "(?m)^\\s*(?:public|protected|private)?(?:\\s+(?:abstract|final|static|sealed|non-sealed|strictfp))*\\s*(?:@?interface|class|enum|record)\\s+([A-Za-z_]\\w*)\\b") .matcher(source); return m.find() ? m.group(1) : null; } From da4f0e4f69b1168c18450d2add01f93c4b38548e Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Fri, 14 Nov 2025 22:03:52 +0100 Subject: [PATCH 43/63] Switching from @SimpleBuilder.Options to @SimpleBuilder(options = ...) and adepting test --- .../core/annotations/SimpleBuilder.java | 22 ++++++++-- .../BuilderConfigurationReaderTest.java | 43 +++++++++---------- 2 files changed, 40 insertions(+), 25 deletions(-) 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 f25b0605..c0d682ce 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,14 +63,30 @@ /** * Configuration options for builder generation. * - *

    Allows fine-grained control over what gets generated in the builder class. Can be used with - * {@link SimpleBuilder} or as part of {@link Template}. + *

    Allows inline configuration of builder generation options: + * + *

    {@code
    +   * @SimpleBuilder(options = @SimpleBuilder.Options(
    +   *     builderAccess = AccessModifier.PACKAGE_PRIVATE,
    +   *     generateFieldSupplier = OptionState.DISABLED
    +   * ))
    +   * public class PersonDto { ... }
    +   * }
    + * + * @return the configuration options, or default (all UNSET) if not specified + */ + Options options() default @Options(); + + /** + * Configuration options for builder generation. + * + *

    Allows fine-grained control over what gets generated in the builder class. Used inline + * within {@link SimpleBuilder} or as part of {@link Template}. * *

    All options have sensible defaults and can be overridden via compiler options using {@code * -A} flag. */ @Retention(RetentionPolicy.CLASS) - @Target(ElementType.TYPE) @interface Options { // === Generation Options === /** 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 c3116f19..4799c30f 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 @@ -59,7 +59,7 @@ class BuilderConfigurationReaderTest { */ @Test void readFromOptions_WithOptionsAnnotation_AppliesAllOptions() { - // Given: A DTO with comprehensive @SimpleBuilder.Options + // Given: A DTO with comprehensive inline @SimpleBuilder options JavaFileObject source = ProcessorTestUtils.forSource( """ @@ -68,8 +68,7 @@ void readFromOptions_WithOptionsAnnotation_AppliesAllOptions() { import org.javahelpers.simple.builders.core.enums.OptionState; import org.javahelpers.simple.builders.core.enums.AccessModifier; - @SimpleBuilder - @SimpleBuilder.Options( + @SimpleBuilder(options = @SimpleBuilder.Options( generateFieldSupplier = OptionState.DISABLED, generateFieldConsumer = OptionState.DISABLED, generateBuilderConsumer = OptionState.DISABLED, @@ -78,7 +77,7 @@ void readFromOptions_WithOptionsAnnotation_AppliesAllOptions() { methodAccess = AccessModifier.PACKAGE_PRIVATE, builderSuffix = "Factory", setterSuffix = "with" - ) + )) public class PersonDto { private String name; private java.util.List tags; @@ -237,10 +236,10 @@ void resolveConfiguration_OptionsOverridesTemplate_AppliesPriorityCorrectly() { import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; @CustomBuilder - @SimpleBuilder.Options( + @SimpleBuilder(options = @SimpleBuilder.Options( builderSuffix = "OptionsBuilder", setterSuffix = "set" - ) + )) public class PersonDto { private String name; @@ -329,10 +328,9 @@ void resolveConfiguration_OptionsOverridesCompilerArgs_AppliesPriorityCorrectly( package test; import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; - @SimpleBuilder - @SimpleBuilder.Options( + @SimpleBuilder(options = @SimpleBuilder.Options( builderSuffix = "OptionsBuilder" - ) + )) public class PersonDto { private String name; @@ -359,9 +357,11 @@ public class PersonDto { } /** - * Test: All layers work together in complete priority chain. + * Test: @SimpleBuilder presence overrides template, then compiler args apply. * - *

    Verifies the complete configuration resolution: Options > Template > CompilerArgs > Defaults + *

    Verifies the complete configuration resolution: When @SimpleBuilder is present (even with + * partial options), custom templates are completely ignored. Priority: @SimpleBuilder inline + * options > CompilerArgs > Defaults */ @Test void resolveConfiguration_AllLayersTogether_CompleteChain() { @@ -375,8 +375,7 @@ void resolveConfiguration_AllLayersTogether_CompleteChain() { import org.javahelpers.simple.builders.core.enums.OptionState; @SimpleBuilder.Template(options = @SimpleBuilder.Options( - generateFieldSupplier = OptionState.DISABLED, - setterSuffix = "with" + generateVarArgsHelpers = OptionState.DISABLED )) @Retention(RetentionPolicy.CLASS) @Target(ElementType.TYPE) @@ -392,10 +391,10 @@ void resolveConfiguration_AllLayersTogether_CompleteChain() { import org.javahelpers.simple.builders.core.enums.OptionState; @TemplatedBuilder - @SimpleBuilder - @SimpleBuilder.Options( - generateVarArgsHelpers = OptionState.DISABLED - ) + @SimpleBuilder(options = @SimpleBuilder.Options( + generateFieldSupplier = OptionState.DISABLED, + setterSuffix = "with" + )) public class PersonDto { private String name; private java.util.List tags; @@ -413,23 +412,23 @@ public class PersonDto { .withOptions("-Asimplebuilder.builderSuffix=CompilerBuilder") .compile(templateAnnotation, dtoSource); - // Then: Layered configuration is applied correctly + // Then: Configuration is applied correctly assertThat(compilation).succeeded(); String generatedCode = ProcessorTestUtils.loadGeneratedSource(compilation, "PersonDtoCompilerBuilder"); - // Options wins for generateVarArgsHelpers + // Inline options wins for generateVarArgsHelpers ProcessorAsserts.assertNotContaining(generatedCode, "withTags(String... tags)"); - // Template wins for setterSuffix (not overridden by options) + // Inline options wins for setterSuffix (not overridden by template options) ProcessorAsserts.assertContaining(generatedCode, "withName(String name)"); ProcessorAsserts.assertContaining(generatedCode, "withTags("); - // Template wins for generateFieldSupplier + // Inline options wins for generateFieldSupplier ProcessorAsserts.assertNotContaining(generatedCode, "Supplier"); - // Compiler arg wins for builderSuffix (not overridden by template or options) + // Compiler args wins for builderSuffix (not overridden by inline options or template options) ProcessorAsserts.assertContaining(generatedCode, "class PersonDtoCompilerBuilder"); } } From 536ee8981e619cf3c39a81d2d18d834cb606deb4 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 7 Dec 2025 19:59:22 +0100 Subject: [PATCH 44/63] Testing with existing annotation in test-context (not with annotation which needs to be compiled before) --- .../BuilderConfigurationReaderTest.java | 78 +++---------------- .../testing/MyBuliderForTestAnnotation.java | 18 +++++ 2 files changed, 27 insertions(+), 69 deletions(-) create mode 100644 processor/src/test/java/org/javahelpers/simple/builders/processor/testing/MyBuliderForTestAnnotation.java 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 4799c30f..e604629d 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 @@ -27,7 +27,6 @@ import static com.google.testing.compile.CompilationSubject.assertThat; import com.google.testing.compile.Compilation; -import com.google.testing.compile.JavaFileObjects; import javax.tools.JavaFileObject; import org.javahelpers.simple.builders.processor.testing.ProcessorAsserts; import org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils; @@ -142,36 +141,13 @@ public class PersonDto { */ @Test void readFromTemplate_WithTemplateAnnotation_AppliesTemplateConfiguration() { - // Given: A custom template annotation - JavaFileObject templateAnnotation = - JavaFileObjects.forSourceString( - "test.MinimalBuilder", - """ - package test; - import java.lang.annotation.*; - import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; - import org.javahelpers.simple.builders.core.enums.OptionState; - - @SimpleBuilder.Template(options = @SimpleBuilder.Options( - generateFieldSupplier = OptionState.DISABLED, - generateFieldConsumer = OptionState.DISABLED, - generateBuilderConsumer = OptionState.DISABLED, - generateVarArgsHelpers = OptionState.DISABLED, - builderSuffix = "MiniBuilder", - setterSuffix = "set" - )) - @Retention(RetentionPolicy.CLASS) - @Target(ElementType.TYPE) - public @interface MinimalBuilder { - } - """); - JavaFileObject dtoSource = ProcessorTestUtils.forSource( """ package test; + import org.javahelpers.simple.builders.processor.testing.MyBuliderForTestAnnotation; - @MinimalBuilder + @MyBuliderForTestAnnotation public class PersonDto { private String name; private int age; @@ -184,8 +160,7 @@ public class PersonDto { """); // When: Compile - Compilation compilation = - ProcessorTestUtils.createCompiler().compile(templateAnnotation, dtoSource); + Compilation compilation = ProcessorTestUtils.createCompiler().compile(dtoSource); // Then: Generated code reflects template configuration assertThat(compilation).succeeded(); @@ -211,31 +186,14 @@ public class PersonDto { */ @Test void resolveConfiguration_OptionsOverridesTemplate_AppliesPriorityCorrectly() { - // Given: Both template and options specified - JavaFileObject templateAnnotation = - ProcessorTestUtils.forSource( - """ - package test; - import java.lang.annotation.*; - import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; - - @SimpleBuilder.Template(options = @SimpleBuilder.Options( - builderSuffix = "TemplateBuilder", - setterSuffix = "with" - )) - @Retention(RetentionPolicy.CLASS) - @Target(ElementType.TYPE) - public @interface CustomBuilder { - } - """); - JavaFileObject dtoSource = ProcessorTestUtils.forSource( """ package test; import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + import org.javahelpers.simple.builders.processor.testing.MyBuliderForTestAnnotation; - @CustomBuilder + @MyBuliderForTestAnnotation @SimpleBuilder(options = @SimpleBuilder.Options( builderSuffix = "OptionsBuilder", setterSuffix = "set" @@ -249,8 +207,7 @@ public class PersonDto { """); // When: Compile - Compilation compilation = - ProcessorTestUtils.createCompiler().compile(templateAnnotation, dtoSource); + Compilation compilation = ProcessorTestUtils.createCompiler().compile(dtoSource); // Then: Options wins over template assertThat(compilation).succeeded(); @@ -365,32 +322,15 @@ public class PersonDto { */ @Test void resolveConfiguration_AllLayersTogether_CompleteChain() { - // Given: All configuration sources present - JavaFileObject templateAnnotation = - ProcessorTestUtils.forSource( - """ - package test; - import java.lang.annotation.*; - import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; - import org.javahelpers.simple.builders.core.enums.OptionState; - - @SimpleBuilder.Template(options = @SimpleBuilder.Options( - generateVarArgsHelpers = OptionState.DISABLED - )) - @Retention(RetentionPolicy.CLASS) - @Target(ElementType.TYPE) - public @interface TemplatedBuilder { - } - """); - JavaFileObject dtoSource = ProcessorTestUtils.forSource( """ package test; import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; import org.javahelpers.simple.builders.core.enums.OptionState; + import org.javahelpers.simple.builders.processor.testing.MyBuliderForTestAnnotation; - @TemplatedBuilder + @MyBuliderForTestAnnotation @SimpleBuilder(options = @SimpleBuilder.Options( generateFieldSupplier = OptionState.DISABLED, setterSuffix = "with" @@ -410,7 +350,7 @@ public class PersonDto { Compilation compilation = ProcessorTestUtils.createCompiler() .withOptions("-Asimplebuilder.builderSuffix=CompilerBuilder") - .compile(templateAnnotation, dtoSource); + .compile(dtoSource); // Then: Configuration is applied correctly assertThat(compilation).succeeded(); diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/testing/MyBuliderForTestAnnotation.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/testing/MyBuliderForTestAnnotation.java new file mode 100644 index 00000000..ecc1b8d7 --- /dev/null +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/testing/MyBuliderForTestAnnotation.java @@ -0,0 +1,18 @@ +package org.javahelpers.simple.builders.processor.testing; + +import java.lang.annotation.*; +import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; +import org.javahelpers.simple.builders.core.enums.OptionState; + +@SimpleBuilder.Template( + options = + @SimpleBuilder.Options( + generateFieldSupplier = OptionState.DISABLED, + generateFieldConsumer = OptionState.DISABLED, + generateBuilderConsumer = OptionState.DISABLED, + generateVarArgsHelpers = OptionState.DISABLED, + builderSuffix = "MiniBuilder", + setterSuffix = "set")) +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.TYPE) +public @interface MyBuliderForTestAnnotation {} From 5f9264345acdf791cab4a0fb718def167cb2a349 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 7 Dec 2025 20:00:38 +0100 Subject: [PATCH 45/63] Refactoring way of processing configurations from annotation --- .../builders/processor/BuilderProcessor.java | 90 +++-- .../util/BuilderConfigurationReader.java | 312 +++++++++++++----- .../util/BuilderDefinitionCreator.java | 56 ++-- .../processor/util/ProcessingContext.java | 20 +- 4 files changed, 336 insertions(+), 142 deletions(-) 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 d746dde2..aab7041c 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 @@ -27,7 +27,9 @@ import static org.javahelpers.simple.builders.processor.util.BuilderDefinitionCreator.extractFromElement; import com.google.auto.service.AutoService; +import java.util.ArrayList; import java.util.HashSet; +import java.util.List; import java.util.Set; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.ProcessingEnvironment; @@ -37,10 +39,14 @@ import javax.lang.model.SourceVersion; import javax.lang.model.element.Element; import javax.lang.model.element.TypeElement; +import org.apache.commons.lang3.tuple.Pair; +import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; +import org.javahelpers.simple.builders.core.annotations.SimpleBuilder.Template; import org.javahelpers.simple.builders.processor.dtos.BuilderConfiguration; import org.javahelpers.simple.builders.processor.dtos.BuilderDefinitionDto; import org.javahelpers.simple.builders.processor.enums.CompilerArgumentsEnum; import org.javahelpers.simple.builders.processor.exceptions.BuilderException; +import org.javahelpers.simple.builders.processor.util.BuilderConfigurationReader; import org.javahelpers.simple.builders.processor.util.CompilerArgumentsReader; import org.javahelpers.simple.builders.processor.util.JavaCodeGenerator; import org.javahelpers.simple.builders.processor.util.ProcessingContext; @@ -52,7 +58,7 @@ * javax.annotation.processing.AbstractProcessor}. */ @AutoService(Processor.class) -@SupportedAnnotationTypes("org.javahelpers.simple.builders.core.annotations.SimpleBuilder") +@SupportedAnnotationTypes("*") public class BuilderProcessor extends AbstractProcessor { private ProcessingContext context; private JavaCodeGenerator codeGenerator; @@ -88,32 +94,50 @@ public boolean process(Set annotations, RoundEnvironment // Fail fast: we already emitted an error in init(); do not attempt any processing. return false; } - // Resolve annotation as TypeElement to support environments where the Class overload - // of getElementsAnnotatedWith is unavailable. + + BuilderConfigurationReader reader = context.getConfigurationReader(); + + // Find all elements to process and their configuration: + // 1. Elements annotated with @SimpleBuilder + // 2. Elements annotated with custom annotations that have @SimpleBuilder.Template + Set> elementsToProcess = new HashSet<>(); + + // Find all @SimpleBuilder annotations TypeElement simpleBuilderAnnotation = - context.getTypeElement( - org.javahelpers.simple.builders.core.annotations.SimpleBuilder.class - .getCanonicalName()); - if (simpleBuilderAnnotation == null) { - context.error( - "Annotation org.javahelpers.simple.builders.core.annotations.SimpleBuilder is not on classpath. So nothing to do here."); + context.getTypeElement(SimpleBuilder.class.getCanonicalName()); + if (simpleBuilderAnnotation != null) { + roundEnv.getElementsAnnotatedWith(simpleBuilderAnnotation).stream() + .forEach( + element -> + elementsToProcess.add(Pair.of(element, reader.readFromInlineOptions(element)))); + } + + // Find all Annotations with @SimpleBuilder.Template + List> annotationsWithTemplate = + extractingAnnotationsWithTemplate(annotations); + for (Pair annotationWithConfigPair : + annotationsWithTemplate) { + TypeElement annotation = annotationWithConfigPair.getLeft(); + BuilderConfiguration config = annotationWithConfigPair.getRight(); + roundEnv.getElementsAnnotatedWith(annotation).stream() + .forEach(element -> elementsToProcess.add(Pair.of(element, config))); } - Set annotatedElements = - roundEnv.getElementsAnnotatedWith(simpleBuilderAnnotation); context.debug("==============================="); context.info("simple-builders: PROCESSING ROUND START"); context.debug("==============================="); context.debug( "simple-builders: Processing round started. Found %d annotated elements.", - annotatedElements.size()); + elementsToProcess.size()); - for (Element annotatedElement : annotatedElements) { + for (Pair annotatedElementWithConfig : elementsToProcess) { + Element annotatedElement = annotatedElementWithConfig.getLeft(); + BuilderConfiguration config = annotatedElementWithConfig.getRight(); try { context.debug("------------------------------------"); context.debug("simple-builders: Processing element: %s", annotatedElement.getSimpleName()); context.debug("------------------------------------"); - process(annotatedElement); + process(annotatedElement, config); context.info( "simple-builders: Successfully generated builder for: %s", annotatedElement.getSimpleName()); @@ -141,11 +165,9 @@ public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); } - private void process(Element annotatedElement) throws BuilderException { - // Initialize configuration for this element (merges DEFAULT -> compiler args -> template -> - // options) - context.initConfiguration(annotatedElement); - + private void process(Element annotatedElement, BuilderConfiguration config) + throws BuilderException { + context.initConfigurationForProcessingTarget(config); BuilderDefinitionDto builderDef = extractFromElement(annotatedElement, context); codeGenerator.generateBuilder(builderDef); } @@ -162,4 +184,34 @@ private static boolean isAtLeastJava17(SourceVersion current) { return false; } } + + private static List> extractingAnnotationsWithTemplate( + Set annotationsFound) { + List> result = new ArrayList<>(); + for (TypeElement annotation : annotationsFound) { + // Only process real annotation specifications + if (annotation.getKind() != javax.lang.model.element.ElementKind.ANNOTATION_TYPE) { + continue; + } + // Skip @SimpleBuilder annotation because we only want to find annotations with + // @SimpleBuilder.Template + if (annotation + .getQualifiedName() + .toString() + .equals(org.javahelpers.simple.builders.core.annotations.SimpleBuilder.class.getName())) { + continue; + } + Template templateAnnotation = + annotation.getAnnotation( + org.javahelpers.simple.builders.core.annotations.SimpleBuilder.Template.class); + if (templateAnnotation != null) { + result.add(Pair.of(annotation, new BuilderConfiguration())); + } + } + return result; + } + + private static BuilderConfiguration extractSimpleBuilderConfiguration(Element element) { + return new BuilderConfiguration(); + } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java index d052ccc1..90dc53ef 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java @@ -24,9 +24,15 @@ package org.javahelpers.simple.builders.processor.util; +import java.util.Map; import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +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.dtos.BuilderConfiguration; /** @@ -38,11 +44,13 @@ *

    Priority order: * *

      - *
    1. {@code @SimpleBuilder.Options} on the element (highest priority) - *
    2. {@code @SimpleBuilder.Template} referenced by the element + *
    3. {@code @SimpleBuilder(options = ...)} inline options (highest priority) + *
    4. Custom template annotations (e.g., {@code @CustomBuilder}) *
    5. Global compiler arguments *
    6. Built-in defaults (lowest priority) *
    + * + *

    Note: If {@code @SimpleBuilder} is present, custom template annotations are ignored. */ public class BuilderConfigurationReader { private final BuilderConfiguration globalConfiguration; @@ -56,23 +64,224 @@ public BuilderConfigurationReader(BuilderConfiguration globalConfiguration) { this.globalConfiguration = globalConfiguration; } + public BuilderConfiguration resolveWithDirectAnnotation(Element element) { + return BuilderConfiguration.DEFAULT + .merge(this.globalConfiguration) + .merge(this.readFromInlineOptions(element)); + } + + public BuilderConfiguration resolveWithTemplateAnnotation( + SimpleBuilder.Template templateAnnotation) { + if (templateAnnotation == null || templateAnnotation.options() == null) { + // TODO add a debug logging + return BuilderConfiguration.DEFAULT.merge(this.globalConfiguration); + } + return BuilderConfiguration.DEFAULT + .merge(this.globalConfiguration) + .merge(this.buildConfigurationFromOptions(templateAnnotation.options())); + } + /** - * Reads builder configuration from an annotated element's {@code @SimpleBuilder.Options} - * annotation. + * Reads builder configuration from {@code @SimpleBuilder(options = ...)} inline options. * - *

    Returns empty Optional if the element has no {@code @SimpleBuilder.Options} annotation. + *

    Returns null if the element has no {@code @SimpleBuilder} annotation. * * @param element the annotated element to analyze - * @return Optional containing the configuration from the annotation, or empty if not present + * @return configuration from the inline options, or null if not present */ - public BuilderConfiguration readFromOptions(Element element) { - SimpleBuilder.Options options = element.getAnnotation(SimpleBuilder.Options.class); + public BuilderConfiguration readFromInlineOptions(Element element) { + AnnotationMirror simpleBuilderMirror = + extractAnnotationMirror( + element, "org.javahelpers.simple.builders.core.annotations.SimpleBuilder"); + return extractOptionsFromAnnotationMirror(simpleBuilderMirror); + } + + private AnnotationMirror extractAnnotationMirror(Element element, String annotationName) { + for (AnnotationMirror mirror : element.getAnnotationMirrors()) { + if (mirror.getAnnotationType().toString().equals(annotationName)) { + return mirror; + } + } + return null; + } - if (options == null) { + /** + * Extracts configuration from the 'options' attribute of an annotation mirror. Used for + * inline @SimpleBuilder(options = ...) where reflection doesn't work. + * + * @param annotationMirror the annotation mirror (either @SimpleBuilder or @Template) + * @return the configuration extracted from the options attribute + */ + private BuilderConfiguration extractOptionsFromAnnotationMirror( + AnnotationMirror annotationMirror) { + if (annotationMirror == null) { return null; } - // Read raw values from annotation without merging + // Find the 'options' attribute + AnnotationMirror optionsMirror = null; + Map elementValues = + annotationMirror.getElementValues(); + + for (Map.Entry entry : + elementValues.entrySet()) { + if (entry.getKey().getSimpleName().toString().equals("options")) { + Object value = entry.getValue().getValue(); + if (value instanceof AnnotationMirror) { + optionsMirror = (AnnotationMirror) value; + } + break; + } + } + + if (optionsMirror == null) { + // No options specified, return empty configuration + return null; + } + + // Parse the options annotation using AnnotationMirror (can't use reflection here) + return parseOptionsFromMirror(optionsMirror); + } + + /** + * Parses SimpleBuilder.Options from AnnotationMirror. Only contains explicitly set values (not + * defaults). + */ + private BuilderConfiguration parseOptionsFromMirror(AnnotationMirror optionsMirror) { + Map values = + optionsMirror.getElementValues(); + + BuilderConfiguration.Builder builder = BuilderConfiguration.builder(); + + for (Map.Entry entry : + values.entrySet()) { + String name = entry.getKey().getSimpleName().toString(); + Object value = entry.getValue().getValue(); + String enumValue = extractEnumName(value); + + switch (name) { + case "generateFieldSupplier" -> builder.generateSupplier(OptionState.valueOf(enumValue)); + case "generateFieldConsumer" -> builder.generateConsumer(OptionState.valueOf(enumValue)); + case "generateBuilderConsumer" -> + builder.generateBuilderConsumer(OptionState.valueOf(enumValue)); + case "generateConditionalHelper" -> + builder.generateConditionalLogic(OptionState.valueOf(enumValue)); + case "builderAccess" -> builder.builderAccess(AccessModifier.valueOf(enumValue)); + case "builderConstructorAccess" -> + builder.builderConstructorAccess(AccessModifier.valueOf(enumValue)); + case "methodAccess" -> builder.methodAccess(AccessModifier.valueOf(enumValue)); + case "generateVarArgsHelpers" -> + builder.generateVarArgsHelpers(OptionState.valueOf(enumValue)); + case "generateStringFormatHelpers" -> + builder.generateStringFormatHelpers(OptionState.valueOf(enumValue)); + case "generateUnboxedOptional" -> + builder.generateUnboxedOptional(OptionState.valueOf(enumValue)); + case "usingArrayListBuilder" -> + builder.usingArrayListBuilder(OptionState.valueOf(enumValue)); + case "usingArrayListBuilderWithElementBuilders" -> + builder.usingArrayListBuilderWithElementBuilders(OptionState.valueOf(enumValue)); + case "usingHashSetBuilder" -> builder.usingHashSetBuilder(OptionState.valueOf(enumValue)); + case "usingHashSetBuilderWithElementBuilders" -> + builder.usingHashSetBuilderWithElementBuilders(OptionState.valueOf(enumValue)); + case "usingHashMapBuilder" -> builder.usingHashMapBuilder(OptionState.valueOf(enumValue)); + case "usingGeneratedAnnotation" -> + builder.usingGeneratedAnnotation(OptionState.valueOf(enumValue)); + case "usingBuilderImplementationAnnotation" -> + builder.usingBuilderImplementationAnnotation(OptionState.valueOf(enumValue)); + case "implementsBuilderBase" -> + builder.implementsBuilderBase(OptionState.valueOf(enumValue)); + case "generateWithInterface" -> + builder.generateWithInterface(OptionState.valueOf(enumValue)); + case "builderSuffix" -> builder.builderSuffix(value.toString()); + case "setterSuffix" -> builder.setterSuffix(value.toString()); + } + } + + return builder.build(); + } + + private String extractEnumName(Object value) { + String enumString = value.toString(); + return enumString.contains(".") + ? enumString.substring(enumString.lastIndexOf('.') + 1) + : enumString; + } + + /** + * Reads builder configuration from a custom template annotation on the element. + * + *

    Only checks for custom template annotations if {@code @SimpleBuilder} is NOT present. Looks + * for any custom annotation on the element that is itself annotated with + * {@code @SimpleBuilder.Template}. + * + *

    Returns null if no template annotation is found or if {@code @SimpleBuilder} is present. + * + * @param element the annotated element to analyze + * @param elementUtils the Elements utility for annotation processing + * @return configuration from the template annotation, or null if not present + */ + public BuilderConfiguration readFromTemplate(Element element, Elements elementUtils) { + // Check all annotations on the element to find one annotated with @SimpleBuilder.Template + for (AnnotationMirror mirror : element.getAnnotationMirrors()) { + Element annotationElement = mirror.getAnnotationType().asElement(); + + // Skip @SimpleBuilder itself (it will be handled by readFromInlineOptions) + if (annotationElement + .toString() + .equals("org.javahelpers.simple.builders.core.annotations.SimpleBuilder")) { + continue; + } + + // Use reflection on the annotation TypeElement to check for @SimpleBuilder.Template + // This works when the template annotation is already compiled + SimpleBuilder.Template template = + annotationElement.getAnnotation(SimpleBuilder.Template.class); + + if (template != null) { + // Found a template! Use reflection to read options directly + return buildConfigurationFromOptions(template.options()); + } + + // Fallback: Check using AnnotationMirror for same-round compiled templates + for (AnnotationMirror metaMirror : annotationElement.getAnnotationMirrors()) { + String metaAnnotationName = metaMirror.getAnnotationType().toString(); + if (metaAnnotationName.equals( + "org.javahelpers.simple.builders.core.annotations.SimpleBuilder.Template") + || metaAnnotationName.equals( + "org.javahelpers.simple.builders.core.annotations.SimpleBuilder$Template")) { + // Found template via mirror - extract options using AnnotationMirror parsing + return extractOptionsFromTemplateMirror(metaMirror, elementUtils); + } + } + } + + return null; + } + + /** + * Extracts configuration from @SimpleBuilder.Template(options = ...) using AnnotationMirror. + * Fallback for same-round compiled templates where reflection doesn't work. + */ + private BuilderConfiguration extractOptionsFromTemplateMirror( + AnnotationMirror templateMirror, Elements elementUtils) { + 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) { + AnnotationMirror optionsMirror = (AnnotationMirror) value; + return parseOptionsFromMirror(optionsMirror); + } + } + } + return null; + } + + /** Builds configuration directly from SimpleBuilder.Options using reflection. */ + private BuilderConfiguration buildConfigurationFromOptions(SimpleBuilder.Options options) { return BuilderConfiguration.builder() .generateSupplier(options.generateFieldSupplier()) .generateConsumer(options.generateFieldConsumer()) @@ -99,70 +308,6 @@ public BuilderConfiguration readFromOptions(Element element) { .build(); } - /** - * Reads builder configuration from a template annotation on the element. - * - *

    Looks for any custom annotation on the element that is itself annotated with - * {@code @SimpleBuilder.Template}. For example, if the element has {@code @FullFeaturedBuilder}, - * and {@code @FullFeaturedBuilder} is annotated with {@code @SimpleBuilder.Template}, this method - * reads the configuration from that template. - * - *

    Returns empty Optional if no template annotation is found. - * - * @param element the annotated element to analyze - * @return Optional containing the configuration from the template annotation, or empty if not - * present - */ - public BuilderConfiguration readFromTemplate(Element element) { - // Check all annotations on the element to find one annotated with @SimpleBuilder.Template - for (AnnotationMirror mirror : element.getAnnotationMirrors()) { - try { - // Get the annotation class - String annotationClassName = mirror.getAnnotationType().toString(); - Class annotationClass = Class.forName(annotationClassName); - - // Check if this annotation is annotated with @SimpleBuilder.Template - SimpleBuilder.Template template = - annotationClass.getAnnotation(SimpleBuilder.Template.class); - - if (template != null) { - // Found a template annotation, read its options - SimpleBuilder.Options options = template.options(); - - return BuilderConfiguration.builder() - .generateSupplier(options.generateFieldSupplier()) - .generateConsumer(options.generateFieldConsumer()) - .generateBuilderConsumer(options.generateBuilderConsumer()) - .generateConditionalLogic(options.generateConditionalHelper()) - .builderAccess(options.builderAccess()) - .builderConstructorAccess(options.builderConstructorAccess()) - .methodAccess(options.methodAccess()) - .generateVarArgsHelpers(options.generateVarArgsHelpers()) - .generateStringFormatHelpers(options.generateStringFormatHelpers()) - .generateUnboxedOptional(options.generateUnboxedOptional()) - .usingArrayListBuilder(options.usingArrayListBuilder()) - .usingArrayListBuilderWithElementBuilders( - options.usingArrayListBuilderWithElementBuilders()) - .usingHashSetBuilder(options.usingHashSetBuilder()) - .usingHashSetBuilderWithElementBuilders( - options.usingHashSetBuilderWithElementBuilders()) - .usingHashMapBuilder(options.usingHashMapBuilder()) - .usingGeneratedAnnotation(options.usingGeneratedAnnotation()) - .usingBuilderImplementationAnnotation(options.usingBuilderImplementationAnnotation()) - .implementsBuilderBase(options.implementsBuilderBase()) - .generateWithInterface(options.generateWithInterface()) - .builderSuffix(options.builderSuffix()) - .setterSuffix(options.setterSuffix()) - .build(); - } - } catch (ClassNotFoundException e) { - // Annotation class not found, skip it - } - } - - return null; - } - /** * Resolves the complete builder configuration for an element by chaining all configuration * sources in priority order. @@ -170,23 +315,28 @@ public BuilderConfiguration readFromTemplate(Element element) { *

    Priority chain (highest to lowest): * *

      - *
    1. {@code @SimpleBuilder.Options} on the element - *
    2. {@code @SimpleBuilder.Template} on a meta-annotation + *
    3. {@code @SimpleBuilder(options = ...)} inline options (highest priority) + *
    4. Custom template annotations (only if {@code @SimpleBuilder} not present) *
    5. Global compiler arguments *
    6. Built-in defaults *
    * + *

    Note: If {@code @SimpleBuilder} is present, custom template annotations are completely + * ignored. The merge chain ensures that for each field: inline options override compiler args, + * which override defaults. + * * @param element the annotated element to resolve configuration for + * @param elementUtils the Elements utility for annotation processing * @return the fully resolved configuration with all sources merged */ - public BuilderConfiguration resolveConfiguration(Element element) { + public BuilderConfiguration resolveConfiguration(Element element, Elements elementUtils) { // Start with DEFAULT as the base // Layer 2: Merge global configuration from compiler arguments - // Layer 3: Merge template configuration if present - // Layer 4: Merge options configuration if present (highest priority) + // Layer 3: Merge template configuration (only if @SimpleBuilder not present) + // Layer 4: Merge inline options from @SimpleBuilder(options = ...) (highest priority) return BuilderConfiguration.DEFAULT .merge(globalConfiguration) - .merge(readFromTemplate(element)) - .merge(readFromOptions(element)); + .merge(readFromTemplate(element, elementUtils)) + .merge(readFromInlineOptions(element)); } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java index 53b6c14b..16e0eb31 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java @@ -106,7 +106,7 @@ public static BuilderDefinitionDto extractFromElement( result.addAllFields(setterFields); // Create the With interface if enabled in configuration - if (context.getBuilderConfigurationForElement().shouldGenerateWithInterface()) { + if (context.getConfiguration().shouldGenerateWithInterface()) { NestedTypeDto withInterface = createWithInterface(result, context); result.addNestedType(withInterface); } @@ -120,10 +120,10 @@ private static BuilderDefinitionDto initializeBuilderDefinition( BuilderDefinitionDto result = new BuilderDefinitionDto(); String packageName = context.getPackageName(annotatedType); String simpleClassName = annotatedType.getSimpleName().toString(); - String builderSuffix = context.getBuilderConfigurationForElement().getBuilderSuffix(); + String builderSuffix = context.getConfiguration().getBuilderSuffix(); result.setBuilderTypeName(new TypeName(packageName, simpleClassName + builderSuffix)); result.setBuildingTargetTypeName(new TypeName(packageName, simpleClassName)); - result.setConfiguration(context.getBuilderConfigurationForElement()); + result.setConfiguration(context.getConfiguration()); context.debug( "Builder will be generated as: %s.%s", packageName, simpleClassName + builderSuffix); @@ -282,7 +282,7 @@ private static void addAdditionalHelperMethodsForField( // Check for String type (not array) and add format method if (isString(field.getFieldType()) && !(field.getFieldType() instanceof TypeNameArray) - && context.getBuilderConfigurationForElement().shouldGenerateStringFormatHelpers()) { + && context.getConfiguration().shouldGenerateStringFormatHelpers()) { String fieldName = field.getFieldNameEstimated(); MethodDto method = createStringFormatMethodWithTransform( @@ -310,7 +310,7 @@ private static void addAdditionalHelperMethodsForField( field.addMethod(method1); // Add Consumer> method only if builder consumers are enabled - if (context.getBuilderConfigurationForElement().shouldGenerateBuilderConsumer()) { + if (context.getConfiguration().shouldGenerateBuilderConsumer()) { TypeName collectionBuilderType = map2TypeName(ArrayListBuilder.class); MethodDto method2 = createFieldConsumerWithArrayBuilder( @@ -335,7 +335,7 @@ private static void addAdditionalHelperMethodsForField( int innerTypesCnt = innerTypes.size(); if (isList(field.getFieldType()) && innerTypesCnt == 1) { // Only add varargs helper if enabled in configuration - if (context.getBuilderConfigurationForElement().shouldGenerateVarArgsHelpers()) { + if (context.getConfiguration().shouldGenerateVarArgsHelpers()) { String fieldName = field.getFieldNameEstimated(); MethodDto method = createFieldSetterWithTransform( @@ -351,7 +351,7 @@ private static void addAdditionalHelperMethodsForField( } } else if (isSet(field.getFieldType()) && innerTypesCnt == 1) { // Only add varargs helper if enabled in configuration - if (context.getBuilderConfigurationForElement().shouldGenerateVarArgsHelpers()) { + if (context.getConfiguration().shouldGenerateVarArgsHelpers()) { String fieldName = field.getFieldNameEstimated(); MethodDto method = createFieldSetterWithTransform( @@ -367,7 +367,7 @@ private static void addAdditionalHelperMethodsForField( } } else if (isMap(field.getFieldType()) && innerTypesCnt == 2) { // Only add varargs helper if enabled in configuration - if (context.getBuilderConfigurationForElement().shouldGenerateVarArgsHelpers()) { + if (context.getConfiguration().shouldGenerateVarArgsHelpers()) { TypeName mapEntryType = new TypeNameArray( new TypeNameGeneric("java.util", "Map.Entry", innerTypes.get(0), innerTypes.get(1)), @@ -389,7 +389,7 @@ private static void addAdditionalHelperMethodsForField( String fieldName = field.getFieldNameEstimated(); // Only generate unboxed optional method if enabled in configuration - if (context.getBuilderConfigurationForElement().shouldGenerateUnboxedOptional()) { + if (context.getConfiguration().shouldGenerateUnboxedOptional()) { // Add setter that accepts the inner type T and wraps it in Optional.ofNullable() MethodDto method = createFieldSetterWithTransform( @@ -406,8 +406,7 @@ private static void addAdditionalHelperMethodsForField( // If Optional, add format method TypeName innerType = innerTypes.get(0); - if (isString(innerType) - && context.getBuilderConfigurationForElement().shouldGenerateStringFormatHelpers()) { + if (isString(innerType) && context.getConfiguration().shouldGenerateStringFormatHelpers()) { MethodDto method = createStringFormatMethodWithTransform( fieldName, @@ -454,7 +453,7 @@ private static boolean tryAddBuilderConsumer( TypeName builderType, ProcessingContext context) { // Builder consumers are controlled by generateBuilderConsumer - if (!context.getBuilderConfigurationForElement().shouldGenerateBuilderConsumer()) { + if (!context.getConfiguration().shouldGenerateBuilderConsumer()) { return false; } Optional fieldBuilderOpt = resolveBuilderType(fieldParameter, context); @@ -482,7 +481,7 @@ private static boolean tryAddFieldConsumer( TypeName builderType, ProcessingContext context) { // Check if field consumer generation is enabled in configuration - if (!context.getBuilderConfigurationForElement().shouldGenerateFieldConsumer()) { + if (!context.getConfiguration().shouldGenerateFieldConsumer()) { return false; } if (!isJavaClass(field.getFieldType()) @@ -510,7 +509,7 @@ && hasEmptyConstructor(fieldTypeElement, context)) { private static boolean tryAddStringBuilderConsumer( FieldDto field, TypeName builderType, ProcessingContext context) { // StringBuilder is a builder pattern, controlled by generateBuilderConsumer - if (!context.getBuilderConfigurationForElement().shouldGenerateBuilderConsumer()) { + if (!context.getConfiguration().shouldGenerateBuilderConsumer()) { return false; } if (shouldGenerateStringBuilderConsumer(field.getFieldType())) { @@ -555,14 +554,12 @@ private static boolean tryAddListConsumer( resolveBuilderType(elementType, elementTypeMirror, context); // Only generate builder consumer methods if enabled - if (!context.getBuilderConfigurationForElement().shouldGenerateBuilderConsumer()) { + if (!context.getConfiguration().shouldGenerateBuilderConsumer()) { return false; } if (elementBuilderType.isPresent() - && context - .getBuilderConfigurationForElement() - .shouldUseArrayListBuilderWithElementBuilders()) { + && context.getConfiguration().shouldUseArrayListBuilderWithElementBuilders()) { // Element type has a builder - use ArrayListBuilderWithElementBuilders if enabled TypeName collectionBuilderType = new TypeNameGeneric( @@ -580,7 +577,7 @@ private static boolean tryAddListConsumer( context); setMethodAccessModifier(method, getMethodAccessModifier(context)); field.addMethod(method); - } else if (context.getBuilderConfigurationForElement().shouldUseArrayListBuilder()) { + } else if (context.getConfiguration().shouldUseArrayListBuilder()) { // Regular ArrayListBuilder if enabled TypeName collectionBuilderType = map2TypeName(ArrayListBuilder.class); MethodDto method = @@ -604,11 +601,11 @@ private static boolean tryAddListConsumer( private static boolean tryAddMapConsumer( FieldDto field, TypeName builderType, ProcessingContext context) { // Check if builder consumers are enabled - if (!context.getBuilderConfigurationForElement().shouldGenerateBuilderConsumer()) { + if (!context.getConfiguration().shouldGenerateBuilderConsumer()) { return false; } // Check if HashMapBuilder is enabled - if (!context.getBuilderConfigurationForElement().shouldUseHashMapBuilder()) { + if (!context.getConfiguration().shouldUseHashMapBuilder()) { return false; } if (!(isMap(field.getFieldType()) @@ -657,14 +654,12 @@ private static boolean tryAddSetConsumer( resolveBuilderType(elementType, elementTypeMirror, context); // Only generate builder consumer methods if enabled - if (!context.getBuilderConfigurationForElement().shouldGenerateBuilderConsumer()) { + if (!context.getConfiguration().shouldGenerateBuilderConsumer()) { return false; } if (elementBuilderType.isPresent() - && context - .getBuilderConfigurationForElement() - .shouldUseHashSetBuilderWithElementBuilders()) { + && context.getConfiguration().shouldUseHashSetBuilderWithElementBuilders()) { // Element type has a builder - use HashSetBuilderWithElementBuilders if enabled TypeName collectionBuilderType = new TypeNameGeneric( @@ -682,7 +677,7 @@ private static boolean tryAddSetConsumer( context); setMethodAccessModifier(method, getMethodAccessModifier(context)); field.addMethod(method); - } else if (context.getBuilderConfigurationForElement().shouldUseHashSetBuilder()) { + } else if (context.getConfiguration().shouldUseHashSetBuilder()) { // Regular HashSetBuilder if enabled TypeName collectionBuilderType = map2TypeName(HashSetBuilder.class); MethodDto method = @@ -708,7 +703,7 @@ private static void addSupplierMethodsForField( TypeName builderType, ProcessingContext context) { // Check if supplier generation is enabled in configuration - if (!context.getBuilderConfigurationForElement().shouldGenerateFieldSupplier()) { + if (!context.getConfiguration().shouldGenerateFieldSupplier()) { return; } // Skip supplier generation for functional interfaces @@ -1495,7 +1490,7 @@ private static Optional resolveBuilderTypeFromTypeElement( String packageName = context.getPackageName(typeElement); String simpleClassName = typeElement.getSimpleName().toString(); - String builderSuffix = context.getBuilderConfigurationForElement().getBuilderSuffix(); + String builderSuffix = context.getConfiguration().getBuilderSuffix(); context.debug( " -> Found @SimpleBuilder on type %s.%s, will use %s%s", packageName, simpleClassName, simpleClassName, builderSuffix); @@ -1657,7 +1652,7 @@ private static MethodDto createWithBuilderMethod(BuilderDefinitionDto builderDef * @return the method name with suffix applied */ private static String generateSetterName(String fieldName, ProcessingContext context) { - String suffix = context.getBuilderConfigurationForElement().getSetterSuffix(); + String suffix = context.getConfiguration().getSetterSuffix(); if (StringUtils.isBlank(suffix)) { return fieldName; } @@ -1671,8 +1666,7 @@ private static String generateSetterName(String fieldName, ProcessingContext con * @return the Modifier for method access, or null for package-private */ private static Modifier getMethodAccessModifier(ProcessingContext context) { - return JavapoetMapper.map2Modifier( - context.getBuilderConfigurationForElement().getMethodAccess()); + return JavapoetMapper.map2Modifier(context.getConfiguration().getMethodAccess()); } /** diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ProcessingContext.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ProcessingContext.java index 23d6ead7..4cc7ad44 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ProcessingContext.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ProcessingContext.java @@ -44,7 +44,7 @@ public final class ProcessingContext { private final Types typeUtils; private final ProcessingLogger logger; private final BuilderConfigurationReader configurationReader; - private BuilderConfiguration builderConfigurationForElement; + private BuilderConfiguration configurationForProcessingTarget; /** * Creates a new processing context. @@ -65,18 +65,16 @@ public ProcessingContext( this.configurationReader = new BuilderConfigurationReader(globalConfiguration); } - public void initConfiguration(Element element) { - this.builderConfigurationForElement = configurationReader.resolveConfiguration(element); - logger.debug("Resolved builder configuration for element: {}", builderConfigurationForElement); + public void initConfigurationForProcessingTarget(BuilderConfiguration config) { + this.configurationForProcessingTarget = config; } - /** - * Get the builder configuration for the current element being processed. - * - * @return the builder configuration - */ - public BuilderConfiguration getBuilderConfigurationForElement() { - return builderConfigurationForElement; + public BuilderConfiguration getConfiguration() { + return this.configurationForProcessingTarget; + } + + public BuilderConfigurationReader getConfigurationReader() { + return configurationReader; } /** From 4d48ac2ec4fdabff6f18ba8f3d9e3a2ffbfc9a2e Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 7 Dec 2025 20:31:57 +0100 Subject: [PATCH 46/63] Adept tests and testdata for correct behavior --- .../simple/builders/processor/dtos/BuilderConfiguration.java | 4 ++-- .../builders/processor/BuilderConfigurationReaderTest.java | 1 + .../processor/testing/MyBuliderForTestAnnotation.java | 2 ++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java index 3cad63af..710485d0 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java @@ -387,8 +387,8 @@ public static class Builder { private OptionState generateWithInterface = OptionState.UNSET; // === Naming === - private String builderSuffix = "Builder"; - private String setterSuffix = ""; + private String builderSuffix = null; + private String setterSuffix = null; // === Setters === public Builder generateSupplier(OptionState value) { 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 e604629d..56279f8c 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 @@ -333,6 +333,7 @@ void resolveConfiguration_AllLayersTogether_CompleteChain() { @MyBuliderForTestAnnotation @SimpleBuilder(options = @SimpleBuilder.Options( generateFieldSupplier = OptionState.DISABLED, + generateVarArgsHelpers = OptionState.DISABLED, setterSuffix = "with" )) public class PersonDto { diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/testing/MyBuliderForTestAnnotation.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/testing/MyBuliderForTestAnnotation.java index ecc1b8d7..94e82ec2 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/testing/MyBuliderForTestAnnotation.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/testing/MyBuliderForTestAnnotation.java @@ -11,6 +11,8 @@ generateFieldConsumer = OptionState.DISABLED, generateBuilderConsumer = OptionState.DISABLED, generateVarArgsHelpers = OptionState.DISABLED, + generateConditionalHelper = OptionState.DISABLED, + generateWithInterface = OptionState.DISABLED, builderSuffix = "MiniBuilder", setterSuffix = "set")) @Retention(RetentionPolicy.CLASS) From 7383097d5b2c96ff2895203bd4d36f8d7f80e870 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 7 Dec 2025 20:49:46 +0100 Subject: [PATCH 47/63] Changing behaviour (no pre-computing of configurations anymore) --- .../builders/processor/BuilderProcessor.java | 40 +++++++------------ .../util/BuilderConfigurationReader.java | 29 ++++++-------- 2 files changed, 27 insertions(+), 42 deletions(-) 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 aab7041c..b8cf8806 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 @@ -39,7 +39,6 @@ import javax.lang.model.SourceVersion; import javax.lang.model.element.Element; import javax.lang.model.element.TypeElement; -import org.apache.commons.lang3.tuple.Pair; import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; import org.javahelpers.simple.builders.core.annotations.SimpleBuilder.Template; import org.javahelpers.simple.builders.processor.dtos.BuilderConfiguration; @@ -97,30 +96,23 @@ public boolean process(Set annotations, RoundEnvironment BuilderConfigurationReader reader = context.getConfigurationReader(); - // Find all elements to process and their configuration: + // Find all elements to process: // 1. Elements annotated with @SimpleBuilder // 2. Elements annotated with custom annotations that have @SimpleBuilder.Template - Set> elementsToProcess = new HashSet<>(); + // Configuration is resolved per-element to handle priority correctly when both exist + Set elementsToProcess = new HashSet<>(); // Find all @SimpleBuilder annotations TypeElement simpleBuilderAnnotation = context.getTypeElement(SimpleBuilder.class.getCanonicalName()); if (simpleBuilderAnnotation != null) { - roundEnv.getElementsAnnotatedWith(simpleBuilderAnnotation).stream() - .forEach( - element -> - elementsToProcess.add(Pair.of(element, reader.readFromInlineOptions(element)))); + elementsToProcess.addAll(roundEnv.getElementsAnnotatedWith(simpleBuilderAnnotation)); } // Find all Annotations with @SimpleBuilder.Template - List> annotationsWithTemplate = - extractingAnnotationsWithTemplate(annotations); - for (Pair annotationWithConfigPair : - annotationsWithTemplate) { - TypeElement annotation = annotationWithConfigPair.getLeft(); - BuilderConfiguration config = annotationWithConfigPair.getRight(); - roundEnv.getElementsAnnotatedWith(annotation).stream() - .forEach(element -> elementsToProcess.add(Pair.of(element, config))); + List annotationsWithTemplate = extractingAnnotationsWithTemplate(annotations); + for (TypeElement annotation : annotationsWithTemplate) { + elementsToProcess.addAll(roundEnv.getElementsAnnotatedWith(annotation)); } context.debug("==============================="); @@ -130,13 +122,15 @@ public boolean process(Set annotations, RoundEnvironment "simple-builders: Processing round started. Found %d annotated elements.", elementsToProcess.size()); - for (Pair annotatedElementWithConfig : elementsToProcess) { - Element annotatedElement = annotatedElementWithConfig.getLeft(); - BuilderConfiguration config = annotatedElementWithConfig.getRight(); + for (Element annotatedElement : elementsToProcess) { try { context.debug("------------------------------------"); context.debug("simple-builders: Processing element: %s", annotatedElement.getSimpleName()); context.debug("------------------------------------"); + // Resolve configuration per-element to handle all layers (defaults, global, template, + // inline) + BuilderConfiguration config = + reader.resolveConfiguration(annotatedElement, processingEnv.getElementUtils()); process(annotatedElement, config); context.info( "simple-builders: Successfully generated builder for: %s", @@ -185,9 +179,9 @@ private static boolean isAtLeastJava17(SourceVersion current) { } } - private static List> extractingAnnotationsWithTemplate( + private static List extractingAnnotationsWithTemplate( Set annotationsFound) { - List> result = new ArrayList<>(); + List result = new ArrayList<>(); for (TypeElement annotation : annotationsFound) { // Only process real annotation specifications if (annotation.getKind() != javax.lang.model.element.ElementKind.ANNOTATION_TYPE) { @@ -205,13 +199,9 @@ private static List> extractingAnnotatio annotation.getAnnotation( org.javahelpers.simple.builders.core.annotations.SimpleBuilder.Template.class); if (templateAnnotation != null) { - result.add(Pair.of(annotation, new BuilderConfiguration())); + result.add(annotation); } } return result; } - - private static BuilderConfiguration extractSimpleBuilderConfiguration(Element element) { - return new BuilderConfiguration(); - } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java index 90dc53ef..7615912b 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java @@ -221,28 +221,23 @@ private String extractEnumName(Object value) { * @return configuration from the template annotation, or null if not present */ public BuilderConfiguration readFromTemplate(Element element, Elements elementUtils) { - // Check all annotations on the element to find one annotated with @SimpleBuilder.Template + // If @SimpleBuilder is present, ignore template annotations (inline options take full + // precedence) for (AnnotationMirror mirror : element.getAnnotationMirrors()) { - Element annotationElement = mirror.getAnnotationType().asElement(); - - // Skip @SimpleBuilder itself (it will be handled by readFromInlineOptions) - if (annotationElement + if (mirror + .getAnnotationType() .toString() .equals("org.javahelpers.simple.builders.core.annotations.SimpleBuilder")) { - continue; + return null; } + } - // Use reflection on the annotation TypeElement to check for @SimpleBuilder.Template - // This works when the template annotation is already compiled - SimpleBuilder.Template template = - annotationElement.getAnnotation(SimpleBuilder.Template.class); - - if (template != null) { - // Found a template! Use reflection to read options directly - return buildConfigurationFromOptions(template.options()); - } + // Check all annotations on the element to find one annotated with @SimpleBuilder.Template + for (AnnotationMirror mirror : element.getAnnotationMirrors()) { + Element annotationElement = mirror.getAnnotationType().asElement(); - // Fallback: Check using AnnotationMirror for same-round compiled templates + // Check using AnnotationMirror for template annotations + // (this gives us only explicitly set values) for (AnnotationMirror metaMirror : annotationElement.getAnnotationMirrors()) { String metaAnnotationName = metaMirror.getAnnotationType().toString(); if (metaAnnotationName.equals( @@ -250,6 +245,7 @@ public BuilderConfiguration readFromTemplate(Element element, Elements elementUt || metaAnnotationName.equals( "org.javahelpers.simple.builders.core.annotations.SimpleBuilder$Template")) { // Found template via mirror - extract options using AnnotationMirror parsing + // This approach only gives us explicitly set values, not annotation defaults return extractOptionsFromTemplateMirror(metaMirror, elementUtils); } } @@ -333,7 +329,6 @@ public BuilderConfiguration resolveConfiguration(Element element, Elements eleme // Start with DEFAULT as the base // Layer 2: Merge global configuration from compiler arguments // Layer 3: Merge template configuration (only if @SimpleBuilder not present) - // Layer 4: Merge inline options from @SimpleBuilder(options = ...) (highest priority) return BuilderConfiguration.DEFAULT .merge(globalConfiguration) .merge(readFromTemplate(element, elementUtils)) From 50c7a51fabbd86c62fa34d114569894c342c17cb Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 7 Dec 2025 21:03:19 +0100 Subject: [PATCH 48/63] Refactoring to increase code quality --- .../builders/processor/BuilderProcessor.java | 7 +- .../util/BuilderConfigurationReader.java | 92 +++++++------------ .../processor/util/ProcessingContext.java | 3 +- 3 files changed, 37 insertions(+), 65 deletions(-) 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 b8cf8806..98054a55 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 @@ -127,10 +127,9 @@ public boolean process(Set annotations, RoundEnvironment context.debug("------------------------------------"); context.debug("simple-builders: Processing element: %s", annotatedElement.getSimpleName()); context.debug("------------------------------------"); - // Resolve configuration per-element to handle all layers (defaults, global, template, - // inline) - BuilderConfiguration config = - reader.resolveConfiguration(annotatedElement, processingEnv.getElementUtils()); + // Resolve configuration per-element to handle all layers + // (defaults, global, template, inline) + BuilderConfiguration config = reader.resolveConfiguration(annotatedElement); process(annotatedElement, config); context.info( "simple-builders: Successfully generated builder for: %s", diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java index 7615912b..68c345a9 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java @@ -54,31 +54,21 @@ */ public class BuilderConfigurationReader { private final BuilderConfiguration globalConfiguration; + private final ProcessingLogger logger; + private final Elements elementUtils; /** * Creates a new BuilderConfigurationReader. * * @param globalConfiguration the global configuration from compiler arguments + * @param logger the logger for debug output + * @param elementUtils the Elements utility for annotation processing */ - public BuilderConfigurationReader(BuilderConfiguration globalConfiguration) { + public BuilderConfigurationReader( + BuilderConfiguration globalConfiguration, ProcessingLogger logger, Elements elementUtils) { this.globalConfiguration = globalConfiguration; - } - - public BuilderConfiguration resolveWithDirectAnnotation(Element element) { - return BuilderConfiguration.DEFAULT - .merge(this.globalConfiguration) - .merge(this.readFromInlineOptions(element)); - } - - public BuilderConfiguration resolveWithTemplateAnnotation( - SimpleBuilder.Template templateAnnotation) { - if (templateAnnotation == null || templateAnnotation.options() == null) { - // TODO add a debug logging - return BuilderConfiguration.DEFAULT.merge(this.globalConfiguration); - } - return BuilderConfiguration.DEFAULT - .merge(this.globalConfiguration) - .merge(this.buildConfigurationFromOptions(templateAnnotation.options())); + this.logger = logger; + this.elementUtils = elementUtils; } /** @@ -217,10 +207,9 @@ private String extractEnumName(Object value) { *

    Returns null if no template annotation is found or if {@code @SimpleBuilder} is present. * * @param element the annotated element to analyze - * @param elementUtils the Elements utility for annotation processing * @return configuration from the template annotation, or null if not present */ - public BuilderConfiguration readFromTemplate(Element element, Elements elementUtils) { + public BuilderConfiguration readFromTemplate(Element element) { // If @SimpleBuilder is present, ignore template annotations (inline options take full // precedence) for (AnnotationMirror mirror : element.getAnnotationMirrors()) { @@ -228,6 +217,9 @@ public BuilderConfiguration readFromTemplate(Element element, Elements elementUt .getAnnotationType() .toString() .equals("org.javahelpers.simple.builders.core.annotations.SimpleBuilder")) { + logger.debug( + "Template annotations ignored for '%s' (direct @SimpleBuilder present)", + element.getSimpleName()); return null; } } @@ -246,7 +238,10 @@ public BuilderConfiguration readFromTemplate(Element element, Elements elementUt "org.javahelpers.simple.builders.core.annotations.SimpleBuilder$Template")) { // Found template via mirror - extract options using AnnotationMirror parsing // This approach only gives us explicitly set values, not annotation defaults - return extractOptionsFromTemplateMirror(metaMirror, elementUtils); + logger.debug( + "Found template annotation '%s' on '%s'", + annotationElement.getSimpleName(), element.getSimpleName()); + return extractOptionsFromTemplateMirror(metaMirror); } } } @@ -258,8 +253,7 @@ public BuilderConfiguration readFromTemplate(Element element, Elements elementUt * Extracts configuration from @SimpleBuilder.Template(options = ...) using AnnotationMirror. * Fallback for same-round compiled templates where reflection doesn't work. */ - private BuilderConfiguration extractOptionsFromTemplateMirror( - AnnotationMirror templateMirror, Elements elementUtils) { + private BuilderConfiguration extractOptionsFromTemplateMirror(AnnotationMirror templateMirror) { Map templateValues = elementUtils.getElementValuesWithDefaults(templateMirror); @@ -276,34 +270,6 @@ private BuilderConfiguration extractOptionsFromTemplateMirror( return null; } - /** Builds configuration directly from SimpleBuilder.Options using reflection. */ - private BuilderConfiguration buildConfigurationFromOptions(SimpleBuilder.Options options) { - return BuilderConfiguration.builder() - .generateSupplier(options.generateFieldSupplier()) - .generateConsumer(options.generateFieldConsumer()) - .generateBuilderConsumer(options.generateBuilderConsumer()) - .generateConditionalLogic(options.generateConditionalHelper()) - .builderAccess(options.builderAccess()) - .builderConstructorAccess(options.builderConstructorAccess()) - .methodAccess(options.methodAccess()) - .generateVarArgsHelpers(options.generateVarArgsHelpers()) - .generateStringFormatHelpers(options.generateStringFormatHelpers()) - .generateUnboxedOptional(options.generateUnboxedOptional()) - .usingArrayListBuilder(options.usingArrayListBuilder()) - .usingArrayListBuilderWithElementBuilders( - options.usingArrayListBuilderWithElementBuilders()) - .usingHashSetBuilder(options.usingHashSetBuilder()) - .usingHashSetBuilderWithElementBuilders(options.usingHashSetBuilderWithElementBuilders()) - .usingHashMapBuilder(options.usingHashMapBuilder()) - .usingGeneratedAnnotation(options.usingGeneratedAnnotation()) - .usingBuilderImplementationAnnotation(options.usingBuilderImplementationAnnotation()) - .implementsBuilderBase(options.implementsBuilderBase()) - .generateWithInterface(options.generateWithInterface()) - .builderSuffix(options.builderSuffix()) - .setterSuffix(options.setterSuffix()) - .build(); - } - /** * Resolves the complete builder configuration for an element by chaining all configuration * sources in priority order. @@ -322,16 +288,22 @@ private BuilderConfiguration buildConfigurationFromOptions(SimpleBuilder.Options * which override defaults. * * @param element the annotated element to resolve configuration for - * @param elementUtils the Elements utility for annotation processing * @return the fully resolved configuration with all sources merged */ - public BuilderConfiguration resolveConfiguration(Element element, Elements elementUtils) { - // Start with DEFAULT as the base - // Layer 2: Merge global configuration from compiler arguments - // Layer 3: Merge template configuration (only if @SimpleBuilder not present) - return BuilderConfiguration.DEFAULT - .merge(globalConfiguration) - .merge(readFromTemplate(element, elementUtils)) - .merge(readFromInlineOptions(element)); + public BuilderConfiguration resolveConfiguration(Element element) { + logger.debug("Resolving configuration for element: %s", element.getSimpleName()); + + BuilderConfiguration templateConfig = readFromTemplate(element); + BuilderConfiguration inlineConfig = readFromInlineOptions(element); + + BuilderConfiguration result = + BuilderConfiguration.DEFAULT + .merge(globalConfiguration) + .merge(templateConfig) + .merge(inlineConfig); + + logger.debug("Configuration resolved for '%s': %s", element.getSimpleName(), result.toString()); + + return result; } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ProcessingContext.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ProcessingContext.java index 4cc7ad44..2d927c70 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ProcessingContext.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ProcessingContext.java @@ -62,7 +62,8 @@ public ProcessingContext( this.elementUtils = elementUtils; this.typeUtils = typeUtils; this.logger = logger; - this.configurationReader = new BuilderConfigurationReader(globalConfiguration); + this.configurationReader = + new BuilderConfigurationReader(globalConfiguration, logger, elementUtils); } public void initConfigurationForProcessingTarget(BuilderConfiguration config) { From f112030aa53bbd2b259867653700db96d0249d1c Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 7 Dec 2025 21:14:09 +0100 Subject: [PATCH 49/63] Adding test for inline template handling --- .../BuilderConfigurationReaderTest.java | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) 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 56279f8c..1c063205 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 @@ -178,6 +178,71 @@ public class PersonDto { ProcessorAsserts.assertNotContaining(generatedCode, "Consumer<"); } + /** + * Test: Template annotation defined in same compilation round (inline). + * + *

    This tests the AnnotationMirror fallback path when the template annotation is not yet + * compiled (same-round compilation). The annotation processor must use AnnotationMirror to read + * the template configuration instead of reflection. + */ + @Test + void readFromTemplate_InlineTemplateDefinition_AppliesTemplateConfiguration() { + // Given: Source with inline template annotation definition AND usage + JavaFileObject dtoSource = + ProcessorTestUtils.forSource( + """ + package test; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + import org.javahelpers.simple.builders.core.enums.OptionState; + import java.lang.annotation.*; + + @SimpleBuilder.Template( + options = @SimpleBuilder.Options( + generateFieldSupplier = OptionState.DISABLED, + generateFieldConsumer = OptionState.DISABLED, + generateBuilderConsumer = OptionState.DISABLED, + generateVarArgsHelpers = OptionState.DISABLED, + generateConditionalHelper = OptionState.DISABLED, + generateWithInterface = OptionState.DISABLED, + builderSuffix = "InlineBuilder", + setterSuffix = "update" + )) + @Retention(RetentionPolicy.CLASS) + @Target(ElementType.TYPE) + @interface InlineTemplate {} + + @InlineTemplate + class PersonDto { + private String name; + private int age; + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public int getAge() { return age; } + public void setAge(int age) { this.age = age; } + } + """); + + // When: Compile + Compilation compilation = ProcessorTestUtils.createCompiler().compile(dtoSource); + + // Then: Generated code reflects inline template configuration + assertThat(compilation).succeeded(); + + String generatedCode = + ProcessorTestUtils.loadGeneratedSource(compilation, "PersonDtoInlineBuilder"); + + // Verify inline template values are applied + ProcessorAsserts.assertContaining(generatedCode, "class PersonDtoInlineBuilder"); + ProcessorAsserts.assertContaining( + generatedCode, "PersonDtoInlineBuilder updateName(String name)"); + ProcessorAsserts.assertContaining(generatedCode, "PersonDtoInlineBuilder updateAge(int age)"); + + // Verify disabled features (tests AnnotationMirror path extracts values correctly) + ProcessorAsserts.assertNotContaining(generatedCode, "Supplier<"); + ProcessorAsserts.assertNotContaining(generatedCode, "Consumer<"); + } + /** * Test: Options annotation overrides template annotation (proper priority). * From 178eadfccf6dbbc82b221137ef015ab478aeac39 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 7 Dec 2025 21:14:38 +0100 Subject: [PATCH 50/63] Changing test "resolveConfiguration_AllLayersTogether_CompleteChain" to have a full compare of the result --- .../BuilderConfigurationReaderTest.java | 151 +++++++++++++++--- 1 file changed, 131 insertions(+), 20 deletions(-) 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 1c063205..31a7b0d0 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 @@ -379,11 +379,21 @@ public class PersonDto { } /** - * Test: @SimpleBuilder presence overrides template, then compiler args apply. + * Test: All optional features disabled through mixed configuration layers. * - *

    Verifies the complete configuration resolution: When @SimpleBuilder is present (even with - * partial options), custom templates are completely ignored. Priority: @SimpleBuilder inline - * options > CompilerArgs > Defaults + *

    Verifies comprehensive configuration resolution by disabling ALL optional features, but + * distributing the disabling across different configuration layers (inline, compiler args, + * defaults). This ensures the complete priority chain works correctly and serves as a regression + * test that new features are properly processed. + * + *

    Configuration strategy: + * + *

      + *
    • Inline options: Disable field supplier, field consumer, builder consumer + *
    • Compiler args: Disable conditional helper, with interface, string format helpers + *
    • Inline options: Custom naming (builderSuffix, setterSuffix) + *
    • Template: Ignored (because @SimpleBuilder is present) + *
    */ @Test void resolveConfiguration_AllLayersTogether_CompleteChain() { @@ -398,7 +408,14 @@ void resolveConfiguration_AllLayersTogether_CompleteChain() { @MyBuliderForTestAnnotation @SimpleBuilder(options = @SimpleBuilder.Options( generateFieldSupplier = OptionState.DISABLED, + generateFieldConsumer = OptionState.DISABLED, + generateBuilderConsumer = OptionState.DISABLED, generateVarArgsHelpers = OptionState.DISABLED, + generateUnboxedOptional = OptionState.DISABLED, + usingArrayListBuilder = OptionState.DISABLED, + usingHashSetBuilder = OptionState.DISABLED, + usingHashMapBuilder = OptionState.DISABLED, + builderSuffix = "MinimalBuilder", setterSuffix = "with" )) public class PersonDto { @@ -412,29 +429,123 @@ public class PersonDto { } """); - // When: Compile with compiler args + // When: Compile with compiler args that disable additional features Compilation compilation = ProcessorTestUtils.createCompiler() - .withOptions("-Asimplebuilder.builderSuffix=CompilerBuilder") + .withOptions( + "-Asimplebuilder.generateConditionalHelper=DISABLED", + "-Asimplebuilder.generateWithInterface=DISABLED", + "-Asimplebuilder.generateStringFormatHelpers=DISABLED") .compile(dtoSource); // Then: Configuration is applied correctly assertThat(compilation).succeeded(); String generatedCode = - ProcessorTestUtils.loadGeneratedSource(compilation, "PersonDtoCompilerBuilder"); - - // Inline options wins for generateVarArgsHelpers - ProcessorAsserts.assertNotContaining(generatedCode, "withTags(String... tags)"); - - // Inline options wins for setterSuffix (not overridden by template options) - ProcessorAsserts.assertContaining(generatedCode, "withName(String name)"); - ProcessorAsserts.assertContaining(generatedCode, "withTags("); - - // Inline options wins for generateFieldSupplier - ProcessorAsserts.assertNotContaining(generatedCode, "Supplier"); - - // Compiler args wins for builderSuffix (not overridden by inline options or template options) - ProcessorAsserts.assertContaining(generatedCode, "class PersonDtoCompilerBuilder"); + ProcessorTestUtils.loadGeneratedSource(compilation, "PersonDtoMinimalBuilder"); + + // Assert complete generated source to ensure all configuration options are applied correctly. + // This comprehensive test validates that ALL optional features can be properly disabled + // through the configuration chain. If this test fails after adding a new feature, it means + // the feature may not be properly integrated into the configuration processing. + String expectedCode = + """ + package test; + + import static org.javahelpers.simple.builders.core.util.TrackedValue.changedValue; + import static org.javahelpers.simple.builders.core.util.TrackedValue.initialValue; + import static org.javahelpers.simple.builders.core.util.TrackedValue.unsetValue; + + import java.util.List; + import javax.annotation.processing.Generated; + import org.javahelpers.simple.builders.core.annotations.BuilderImplementation; + import org.javahelpers.simple.builders.core.interfaces.IBuilderBase; + import org.javahelpers.simple.builders.core.util.TrackedValue; + + /** + * Builder for {@code test.PersonDto}. + */ + @Generated("Generated by org.javahelpers.simple.builders.processor.BuilderProcessor") + @BuilderImplementation( + forClass = PersonDto.class + ) + public class PersonDtoMinimalBuilder implements IBuilderBase { + /** + * Tracked value for name: name. + */ + private TrackedValue name = unsetValue(); + + /** + * Tracked value for tags: tags. + */ + private TrackedValue> tags = unsetValue(); + + /** + * Initialisation of builder for {@code test.PersonDto} by a instance. + * + * @param instance object instance for initialisiation + */ + public PersonDtoMinimalBuilder(PersonDto instance) { + this.name = initialValue(instance.getName()); + this.tags = initialValue(instance.getTags()); + } + + /** + * Empty constructor of builder for {@code test.PersonDto}. + */ + public PersonDtoMinimalBuilder() { + } + + /** + * Sets the value for tags. + * + * @param tags tags + * @return current instance of builder + */ + public PersonDtoMinimalBuilder withTags(List tags) { + this.tags = changedValue(tags); + return this; + } + + /** + * Sets the value for name. + * + * @param name name + * @return current instance of builder + */ + public PersonDtoMinimalBuilder withName(String name) { + this.name = changedValue(name); + return this; + } + + @Override + public PersonDto build() { + PersonDto result = new PersonDto(); + this.name.ifSet(result::setName); + this.tags.ifSet(result::setTags); + return result; + } + + /** + * Creating a new builder for {@code test.PersonDto}. + * + * @return builder for {@code test.PersonDto} + */ + public static PersonDtoMinimalBuilder create() { + return new PersonDtoMinimalBuilder(); + } + } + """; + + // Normalize whitespace for comparison to avoid formatting issues + String normalizedExpected = expectedCode.replaceAll("\\s+", " ").trim(); + String normalizedGenerated = generatedCode.replaceAll("\\s+", " ").trim(); + + org.junit.jupiter.api.Assertions.assertEquals( + normalizedExpected, + normalizedGenerated, + "Generated code does not match expected. This comprehensive test ensures all configuration " + + "options are correctly applied. If this fails, a configuration option may have been " + + "added without proper processing support."); } } From 63e1f9de4d281de5f91ebd71c518030b932e7016 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 7 Dec 2025 21:19:29 +0100 Subject: [PATCH 51/63] Update BuilderConfigurationReaderTest.java --- .../processor/BuilderConfigurationReaderTest.java | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) 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 31a7b0d0..d603a00f 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 @@ -386,10 +386,12 @@ public class PersonDto { * defaults). This ensures the complete priority chain works correctly and serves as a regression * test that new features are properly processed. * - *

    Configuration strategy: + *

    Configuration strategy - ALL optional features disabled: * *

      - *
    • Inline options: Disable field supplier, field consumer, builder consumer + *
    • Inline options: Disable field supplier, field consumer, builder consumer, varargs + * helpers, unboxed optional, collection builders, annotations + * (@Generated, @BuilderImplementation) *
    • Compiler args: Disable conditional helper, with interface, string format helpers *
    • Inline options: Custom naming (builderSuffix, setterSuffix) *
    • Template: Ignored (because @SimpleBuilder is present) @@ -415,6 +417,8 @@ void resolveConfiguration_AllLayersTogether_CompleteChain() { usingArrayListBuilder = OptionState.DISABLED, usingHashSetBuilder = OptionState.DISABLED, usingHashMapBuilder = OptionState.DISABLED, + usingGeneratedAnnotation = OptionState.DISABLED, + usingBuilderImplementationAnnotation = OptionState.DISABLED, builderSuffix = "MinimalBuilder", setterSuffix = "with" )) @@ -457,18 +461,12 @@ public class PersonDto { import static org.javahelpers.simple.builders.core.util.TrackedValue.unsetValue; import java.util.List; - import javax.annotation.processing.Generated; - import org.javahelpers.simple.builders.core.annotations.BuilderImplementation; import org.javahelpers.simple.builders.core.interfaces.IBuilderBase; import org.javahelpers.simple.builders.core.util.TrackedValue; /** * Builder for {@code test.PersonDto}. */ - @Generated("Generated by org.javahelpers.simple.builders.processor.BuilderProcessor") - @BuilderImplementation( - forClass = PersonDto.class - ) public class PersonDtoMinimalBuilder implements IBuilderBase { /** * Tracked value for name: name. From 8d1ddaba816189b8edb31b1cf6eb05495e13148b Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 7 Dec 2025 21:33:01 +0100 Subject: [PATCH 52/63] Updating documentation --- docs/CONFIGURATION.md | 680 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 601 insertions(+), 79 deletions(-) diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index d6d2db8f..f0b9c9b4 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -52,16 +52,16 @@ Configure individual builders using `@SimpleBuilder` with `@SimpleBuilder.Option ```java @SimpleBuilder @SimpleBuilder.Options( - generateFieldSupplier = true, - generateFieldProvider = true, - generateBuilderProvider = true, - generateConditionalHelper = true, + generateFieldSupplier = OptionState.ENABLED, + generateFieldConsumer = OptionState.ENABLED, + generateBuilderConsumer = OptionState.ENABLED, + generateConditionalHelper = OptionState.ENABLED, builderAccess = AccessModifier.PUBLIC, methodAccess = AccessModifier.PUBLIC, - generateVarArgsHelpers = true, - usingArrayListBuilder = true, - usingHashMapBuilder = true, - generateWithInterface = true + generateVarArgsHelpers = OptionState.ENABLED, + usingArrayListBuilder = OptionState.ENABLED, + usingHashMapBuilder = OptionState.ENABLED, + generateWithInterface = OptionState.ENABLED ) public class PersonDto { private String name; @@ -129,10 +129,10 @@ Set project-wide defaults via compiler options. These apply to all builders unle - -Asimplebuilder.generateFieldSupplier=true - -Asimplebuilder.generateFieldProvider=true + -Asimplebuilder.generateFieldSupplier=ENABLED + -Asimplebuilder.generateFieldConsumer=ENABLED -Asimplebuilder.builderAccess=PUBLIC - -Asimplebuilder.usingArrayListBuilder=true + -Asimplebuilder.usingArrayListBuilder=ENABLED @@ -147,8 +147,8 @@ dependencies { compileJava { options.compilerArgs += [ - "-Asimplebuilder.generateFieldSupplier=true", - "-Asimplebuilder.generateFieldProvider=true", + "-Asimplebuilder.generateFieldSupplier=ENABLED", + "-Asimplebuilder.generateFieldConsumer=ENABLED", "-Asimplebuilder.builderAccess=PUBLIC" ] } @@ -164,43 +164,502 @@ compileJava { ## Configuration Options +All options use `OptionState` enum with values: `ENABLED`, `DISABLED`, or `UNSET` (uses default/compiler arg). + ### Field Setter Generation -| Option | Type | Default | Compiler Option | Description | -|--------|------|---------|------------------|-------------| -| `generateFieldSupplier` | boolean | `true` | `-Asimplebuilder.generateFieldSupplier` | Generate setter methods accepting `Supplier` for field values | -| `generateFieldProvider` | boolean | `true` | `-Asimplebuilder.generateFieldProvider` | Generate setter methods accepting `Provider` for complex field types | -| `generateBuilderProvider` | boolean | `true` | `-Asimplebuilder.generateBuilderProvider` | Generate setter methods accepting `Provider>` for buildable types | +#### `generateFieldSupplier` + +**Default**: `ENABLED` | **Compiler Option**: `-Asimplebuilder.generateFieldSupplier=ENABLED|DISABLED` + +Generates setter methods that accept `Supplier` for lazy field value initialization. + +**When ENABLED**: +```java +// Generated method +public PersonDtoBuilder name(Supplier nameSupplier) { + this.name = changedValue(nameSupplier.get()); + return this; +} + +// Usage +PersonDto person = PersonDtoBuilder.create() + .name(() -> expensiveNameComputation()) + .build(); +``` + +**When DISABLED**: No `Supplier<>` setter methods are generated. + +--- + +#### `generateFieldConsumer` + +**Default**: `ENABLED` | **Compiler Option**: `-Asimplebuilder.generateFieldConsumer=ENABLED|DISABLED` + +Generates setter methods that accept `Consumer` for String fields, allowing fluent string building. + +**When ENABLED**: +```java +// Generated method for String fields +public PersonDtoBuilder name(Consumer nameConsumer) { + StringBuilder builder = new StringBuilder(); + nameConsumer.accept(builder); + this.name = changedValue(builder.toString()); + return this; +} + +// Usage +PersonDto person = PersonDtoBuilder.create() + .name(sb -> sb.append("Dr. ").append(firstName).append(" ").append(lastName)) + .build(); +``` + +**When DISABLED**: No `Consumer` methods are generated. + +--- + +#### `generateBuilderConsumer` + +**Default**: `ENABLED` | **Compiler Option**: `-Asimplebuilder.generateBuilderConsumer=ENABLED|DISABLED` + +Generates setter methods that accept `Consumer` for complex nested objects and collections. + +**When ENABLED**: +```java +// Generated method for collection fields +public PersonDtoBuilder tags(Consumer> tagsConsumer) { + ArrayListBuilder builder = new ArrayListBuilder<>(); + tagsConsumer.accept(builder); + this.tags = changedValue(builder.build()); + return this; +} + +// Usage +PersonDto person = PersonDtoBuilder.create() + .tags(list -> list.add("java").add("kotlin").add("scala")) + .build(); +``` + +**When DISABLED**: No builder consumer methods are generated. + +--- ### Conditional Logic -| Option | Type | Default | Compiler Option | Description | -|--------|------|---------|------------------|-------------| -| `generateConditionalHelper` | boolean | `true` | `-Asimplebuilder.generateConditionalHelper` | Generate conditional/when methods for fluent conditional logic | +#### `generateConditionalHelper` + +**Default**: `ENABLED` | **Compiler Option**: `-Asimplebuilder.generateConditionalHelper=ENABLED|DISABLED` + +Generates conditional helper methods for fluent conditional logic in builder chains. + +**When ENABLED**: +```java +// Generated methods +public PersonDtoBuilder conditional(BooleanSupplier condition, + Consumer trueCase, + Consumer falseCase) { ... } + +public PersonDtoBuilder conditional(BooleanSupplier condition, + Consumer yesCondition) { ... } + +// Usage +PersonDto person = PersonDtoBuilder.create() + .name("John") + .conditional(() -> isPremiumUser, + builder -> builder.premiumFeatures(true), + builder -> builder.premiumFeatures(false)) + .build(); +``` + +**When DISABLED**: No conditional helper methods are generated. + +--- ### Access Control -| Option | Type | Default | Values | Compiler Option | Description | -|--------|------|---------|--------|------------------|-------------| -| `builderAccess` | AccessModifier | `PUBLIC` | `PUBLIC`, `PROTECTED`, `PACKAGE_PRIVATE`, `PRIVATE` | `-Asimplebuilder.builderAccess` | Visibility level for generated builder class | -| `methodAccess` | AccessModifier | `PUBLIC` | `PUBLIC`, `PROTECTED`, `PACKAGE_PRIVATE`, `PRIVATE` | `-Asimplebuilder.methodAccess` | Visibility level for generated builder methods | +#### `builderAccess` + +**Default**: `PUBLIC` | **Compiler Option**: `-Asimplebuilder.builderAccess=PUBLIC|PACKAGE_PRIVATE` + +Controls the visibility of the generated builder class. + +**Supported Values**: +- `PUBLIC` - Builder accessible from anywhere (default, recommended for public APIs) +- `PACKAGE_PRIVATE` - Builder only accessible within the same package (good for internal APIs) + +⚠️ **Warning**: `PRIVATE` is technically supported but **not recommended** as it makes the builder class completely inaccessible and therefore useless. Even static factory methods won't help since the class itself is private. + +**Example with PACKAGE_PRIVATE**: +```java +// Generated builder +class PersonDtoBuilder implements IBuilderBase { // No 'public' keyword + // ... only accessible within the same package +} +``` + +**Use case**: Use `PACKAGE_PRIVATE` for DTOs that are internal to your package and shouldn't have their builders exposed publicly. + +--- + +#### `builderConstructorAccess` + +**Default**: `PUBLIC` | **Compiler Option**: `-Asimplebuilder.builderConstructorAccess=PUBLIC|PACKAGE_PRIVATE|PRIVATE` + +Controls the visibility of the builder's constructors. + +**Supported Values**: +- `PUBLIC` - Constructors accessible from anywhere (default) +- `PACKAGE_PRIVATE` - Constructors only accessible within the same package +- `PRIVATE` - Constructors only accessible via static factory methods ✅ **Recommended pattern** + +**Example with PRIVATE** (recommended for API design): +```java +// Generated constructors +private PersonDtoBuilder() { } +private PersonDtoBuilder(PersonDto instance) { ... } + +// Usage - forced to use static factory methods +PersonDtoBuilder builder = PersonDtoBuilder.create(); // ✅ OK +new PersonDtoBuilder() // ❌ Compilation error - constructor is private +``` + +**Use case**: Use `PRIVATE` constructors to enforce using the static `create()` factory method, preventing direct instantiation and ensuring consistent builder creation patterns. + +--- + +#### `methodAccess` + +**Default**: `PUBLIC` | **Compiler Option**: `-Asimplebuilder.methodAccess=PUBLIC|PACKAGE_PRIVATE` + +Controls the visibility of all generated setter methods. + +**Supported Values**: +- `PUBLIC` - Methods accessible from anywhere (default, recommended) +- `PACKAGE_PRIVATE` - Methods only accessible within the same package + +⚠️ **Warning**: `PRIVATE` is technically supported but **not recommended** as it makes all builder setter methods private and therefore the builder completely unusable. + +**Example with PACKAGE_PRIVATE**: +```java +// Generated methods without 'public' modifier +PersonDtoBuilder name(String name) { // Package-private + this.name = changedValue(name); + return this; +} +``` + +**Use case**: Rarely needed. Consider using `PACKAGE_PRIVATE` only when the entire builder API should be internal to the package. + +--- + +### Helper Methods + +#### `generateVarArgsHelpers` + +**Default**: `ENABLED` | **Compiler Option**: `-Asimplebuilder.generateVarArgsHelpers=ENABLED|DISABLED` + +Generates varargs methods for List and Set fields for convenient multi-value initialization. + +**When ENABLED**: +```java +// Generated method +public PersonDtoBuilder tags(String... tags) { + this.tags = changedValue(Arrays.asList(tags)); + return this; +} + +// Usage +PersonDto person = PersonDtoBuilder.create() + .tags("java", "kotlin", "scala") // Varargs syntax + .build(); +``` + +**When DISABLED**: No varargs methods are generated; must use collection directly. + +--- + +#### `generateStringFormatHelpers` + +**Default**: `ENABLED` | **Compiler Option**: `-Asimplebuilder.generateStringFormatHelpers=ENABLED|DISABLED` + +Generates `String.format()` helper methods for String fields. + +**When ENABLED**: +```java +// Generated method +public PersonDtoBuilder name(String format, Object... args) { + this.name = changedValue(String.format(format, args)); + return this; +} + +// Usage +PersonDto person = PersonDtoBuilder.create() + .name("Hello, %s %s!", firstName, lastName) + .build(); +``` + +**When DISABLED**: No format helper methods are generated. + +--- + +#### `generateUnboxedOptional` + +**Default**: `ENABLED` | **Compiler Option**: `-Asimplebuilder.generateUnboxedOptional=ENABLED|DISABLED` + +Generates methods that accept `Optional` and automatically unwrap them. + +**When ENABLED**: +```java +// Generated method +public PersonDtoBuilder name(Optional nameOptional) { + nameOptional.ifPresent(value -> this.name = changedValue(value)); + return this; +} + +// Usage +Optional maybeName = findName(); +PersonDto person = PersonDtoBuilder.create() + .name(maybeName) // Automatically unwrapped + .build(); +``` + +**When DISABLED**: Must unwrap Optional manually before passing to builder. + +--- ### Collection Helpers -| Option | Type | Default | Compiler Option | Description | -|--------|------|---------|------------------|-------------| -| `generateVarArgsHelpers` | boolean | `true` | `-Asimplebuilder.generateVarArgsHelpers` | Generate varargs methods for Lists and Sets | -| `usingArrayListBuilder` | boolean | `true` | `-Asimplebuilder.usingArrayListBuilder` | Use chaining ArrayListBuilder for Lists | -| `usingArrayListBuilderWithElementBuilders` | boolean | `true` | `-Asimplebuilder.usingArrayListBuilderWithElementBuilders` | Use ArrayListBuilderWithElementBuilders for Lists of complex objects | -| `usingHashSetBuilder` | boolean | `true` | `-Asimplebuilder.usingHashSetBuilder` | Use chaining HashSetBuilder for Sets | -| `usingHashSetBuilderWithElementBuilders` | boolean | `true` | `-Asimplebuilder.usingHashSetBuilderWithElementBuilders` | Use HashSetBuilderWithElementBuilders for Sets of complex objects | -| `usingHashMapBuilder` | boolean | `true` | `-Asimplebuilder.usingHashMapBuilder` | Use chaining HashMapBuilder for Maps | +#### `usingArrayListBuilder` + +**Default**: `ENABLED` | **Compiler Option**: `-Asimplebuilder.usingArrayListBuilder=ENABLED|DISABLED` + +Generates methods using `ArrayListBuilder` for fluent List construction. + +**When ENABLED**: +```java +// Generated method +public PersonDtoBuilder tags(Consumer> consumer) { + ArrayListBuilder builder = new ArrayListBuilder<>(); + consumer.accept(builder); + this.tags = changedValue(builder.build()); + return this; +} + +// Usage +.tags(list -> list.add("tag1").add("tag2").addAll(otherTags)) +``` + +**When DISABLED**: Basic List setter only; no fluent list building. + +--- + +#### `usingArrayListBuilderWithElementBuilders` + +**Default**: `ENABLED` | **Compiler Option**: `-Asimplebuilder.usingArrayListBuilderWithElementBuilders=ENABLED|DISABLED` + +Generates methods using `ArrayListBuilderWithElementBuilders` for fluent construction of Lists containing complex objects that have their own builders. + +**When ENABLED**: +```java +// For List where PersonDto has a builder +public TeamDtoBuilder members(Consumer> consumer) { + ArrayListBuilderWithElementBuilders builder = ...; + consumer.accept(builder); + this.members = changedValue(builder.build()); + return this; +} + +// Usage - build complex nested objects inline +.members(list -> list + .add(person -> person.name("Alice").age(30)) + .add(person -> person.name("Bob").age(25))) +``` + +**When DISABLED**: No builder consumer methods for complex list elements; must construct objects separately. + +--- + +#### `usingHashSetBuilder` + +**Default**: `ENABLED` | **Compiler Option**: `-Asimplebuilder.usingHashSetBuilder=ENABLED|DISABLED` + +Generates methods using `HashSetBuilder` for fluent Set construction. + +**When ENABLED**: +```java +// Generated method +public PersonDtoBuilder tags(Consumer> consumer) { + HashSetBuilder builder = new HashSetBuilder<>(); + consumer.accept(builder); + this.tags = changedValue(builder.build()); + return this; +} + +// Usage +.tags(set -> set.add("tag1").add("tag2").addAll(otherTags)) +``` + +**When DISABLED**: Basic Set setter only; no fluent set building. + +--- + +#### `usingHashSetBuilderWithElementBuilders` + +**Default**: `ENABLED` | **Compiler Option**: `-Asimplebuilder.usingHashSetBuilderWithElementBuilders=ENABLED|DISABLED` + +Generates methods using `HashSetBuilderWithElementBuilders` for fluent construction of Sets containing complex objects that have their own builders. + +**When ENABLED**: +```java +// For Set where PersonDto has a builder +public TeamDtoBuilder uniqueMembers(Consumer> consumer) { + HashSetBuilderWithElementBuilders builder = ...; + consumer.accept(builder); + this.uniqueMembers = changedValue(builder.build()); + return this; +} + +// Usage - build complex nested objects inline +.uniqueMembers(set -> set + .add(person -> person.name("Alice").email("alice@example.com")) + .add(person -> person.name("Bob").email("bob@example.com"))) +``` + +**When DISABLED**: No builder consumer methods for complex set elements; must construct objects separately. + +--- + +#### `usingHashMapBuilder` + +**Default**: `ENABLED` | **Compiler Option**: `-Asimplebuilder.usingHashMapBuilder=ENABLED|DISABLED` + +Generates methods using `HashMapBuilder` for fluent Map construction. + +--- + +### Integration & Annotations + +#### `generateWithInterface` + +**Default**: `ENABLED` | **Compiler Option**: `-Asimplebuilder.generateWithInterface=ENABLED|DISABLED` + +Generates a `With` interface that can be implemented by your DTO to enable fluent modification methods. + +**When ENABLED**: +```java +// Generated interface inside the builder +public interface With { + default PersonDto with(Consumer modifications) { + PersonDtoBuilder builder = new PersonDtoBuilder((PersonDto) this); + modifications.accept(builder); + return builder.build(); + } + + default PersonDtoBuilder with() { + return new PersonDtoBuilder((PersonDto) this); + } +} + +// Your DTO can implement it +public class PersonDto implements PersonDtoBuilder.With { + // ... +} + +// Usage - create modified copies +PersonDto original = new PersonDto(); +PersonDto modified = original.with(p -> p.name("New Name").age(30)); +``` + +**When DISABLED**: No `With` interface is generated. DTOs cannot use the fluent modification pattern. + +--- + +#### `implementsBuilderBase` + +**Default**: `ENABLED` | **Compiler Option**: `-Asimplebuilder.implementsBuilderBase=ENABLED|DISABLED` + +Makes the generated builder implement `IBuilderBase` interface for framework integration. + +**When ENABLED**: +```java +public class PersonDtoBuilder implements IBuilderBase { + // Can be used with generic builder frameworks +} +``` + +**When DISABLED**: Builder is a standalone class without interface. + +--- + +#### `usingGeneratedAnnotation` + +**Default**: `ENABLED` | **Compiler Option**: `-Asimplebuilder.usingGeneratedAnnotation=ENABLED|DISABLED` + +Adds `@Generated` annotation to the builder class for tooling and code coverage exclusion. + +**When ENABLED**: +```java +@Generated("Generated by org.javahelpers.simple.builders.processor.BuilderProcessor") +public class PersonDtoBuilder implements IBuilderBase { + // ... +} +``` -### Integration +**When DISABLED**: No `@Generated` annotation. Useful if you want the builder counted in code coverage. -| Option | Type | Default | Compiler Option | Description | -|--------|------|---------|------------------|-------------| -| `generateWithInterface` | boolean | `true` | `-Asimplebuilder.generateWithInterface` | Generate With interface for DTO integration | +--- + +#### `usingBuilderImplementationAnnotation` + +**Default**: `ENABLED` | **Compiler Option**: `-Asimplebuilder.usingBuilderImplementationAnnotation=ENABLED|DISABLED` + +Adds `@BuilderImplementation` annotation linking the builder to its target class. + +**When ENABLED**: +```java +@BuilderImplementation(forClass = PersonDto.class) +public class PersonDtoBuilder implements IBuilderBase { + // ... +} +``` + +**When DISABLED**: No `@BuilderImplementation` annotation. + +--- + +### Naming + +#### `builderSuffix` + +**Default**: `"Builder"` | **Compiler Option**: `-Asimplebuilder.builderSuffix=CustomSuffix` + +Customizes the suffix appended to the DTO class name to create the builder class name. + +**Example**: +```java +@SimpleBuilder.Options(builderSuffix = "Factory") +public class PersonDto { } + +// Generated class name: PersonDtoFactory (instead of PersonDtoBuilder) +``` + +--- + +#### `setterSuffix` + +**Default**: `""` (empty) | **Compiler Option**: `-Asimplebuilder.setterSuffix=customPrefix` + +Adds a prefix to all setter method names. + +**Example**: +```java +@SimpleBuilder.Options(setterSuffix = "with") +public class PersonDto { + private String name; +} + +// Generated method: withName(String name) instead of name(String name) +``` ## Examples @@ -211,14 +670,14 @@ Generate only essential builder methods: ```java @SimpleBuilder @SimpleBuilder.Options( - generateFieldSupplier = false, - generateFieldProvider = false, - generateBuilderProvider = false, - generateConditionalHelper = false, - generateVarArgsHelpers = false, - usingArrayListBuilder = false, - usingHashMapBuilder = false, - generateWithInterface = false + generateFieldSupplier = OptionState.DISABLED, + generateFieldConsumer = OptionState.DISABLED, + generateBuilderConsumer = OptionState.DISABLED, + generateConditionalHelper = OptionState.DISABLED, + generateVarArgsHelpers = OptionState.DISABLED, + usingArrayListBuilder = OptionState.DISABLED, + usingHashMapBuilder = OptionState.DISABLED, + generateWithInterface = OptionState.DISABLED ) public class MinimalDto { private String name; @@ -323,16 +782,16 @@ Set sensible defaults for your entire project: - -Asimplebuilder.generateFieldSupplier=true - -Asimplebuilder.generateFieldProvider=true - -Asimplebuilder.generateBuilderProvider=true + -Asimplebuilder.generateFieldSupplier=ENABLED + -Asimplebuilder.generateFieldConsumer=ENABLED + -Asimplebuilder.generateBuilderConsumer=ENABLED -Asimplebuilder.builderAccess=PACKAGE_PRIVATE - -Asimplebuilder.usingArrayListBuilder=true - -Asimplebuilder.usingHashMapBuilder=true + -Asimplebuilder.usingArrayListBuilder=ENABLED + -Asimplebuilder.usingHashMapBuilder=ENABLED ``` @@ -452,33 +911,86 @@ Or in compiler options: 6. **Test configurations**: Verify generated code meets expectations 7. **Consider team preferences**: Choose configurations that work for everyone +### Access Modifier Best Practices + +**Recommended Combinations**: + +✅ **Public API Builder** (most common): +```java +@SimpleBuilder.Options( + builderAccess = AccessModifier.PUBLIC, // ✅ Accessible everywhere + builderConstructorAccess = AccessModifier.PRIVATE, // ✅ Forces use of create() + methodAccess = AccessModifier.PUBLIC // ✅ Accessible everywhere +) +``` + +✅ **Internal/Package-Private Builder**: +```java +@SimpleBuilder.Options( + builderAccess = AccessModifier.PACKAGE_PRIVATE, // ✅ Internal to package + builderConstructorAccess = AccessModifier.PRIVATE, // ✅ Forces use of create() + methodAccess = AccessModifier.PACKAGE_PRIVATE // ✅ Internal to package +) +``` + +❌ **Avoid These Combinations**: + +```java +// ❌ WRONG: Private builder class is completely unusable +builderAccess = AccessModifier.PRIVATE // Nobody can access the class at all! + +// ❌ WRONG: Private methods make the builder unusable +methodAccess = AccessModifier.PRIVATE // Nobody can call the setter methods! + +// ⚠️ RARELY USEFUL: Public builder with private methods +builderAccess = AccessModifier.PUBLIC, +methodAccess = AccessModifier.PRIVATE // Builder exists but is unusable! +``` + +**Why `PRIVATE` constructors are different**: +- ✅ `builderConstructorAccess = PRIVATE` **IS USEFUL** - Enforces using `create()` factory method +- ❌ `builderAccess = PRIVATE` **IS NOT USEFUL** - Makes the entire class inaccessible +- ❌ `methodAccess = PRIVATE` **IS NOT USEFUL** - Makes all methods inaccessible + ## Reference ### All Compiler Options ``` # Field Setter Generation --Asimplebuilder.generateFieldSupplier=true|false --Asimplebuilder.generateFieldProvider=true|false --Asimplebuilder.generateBuilderProvider=true|false +-Asimplebuilder.generateFieldSupplier=ENABLED|DISABLED +-Asimplebuilder.generateFieldConsumer=ENABLED|DISABLED +-Asimplebuilder.generateBuilderConsumer=ENABLED|DISABLED # Conditional Logic --Asimplebuilder.generateConditionalHelper=true|false +-Asimplebuilder.generateConditionalHelper=ENABLED|DISABLED # Access Control --Asimplebuilder.builderAccess=PUBLIC|PROTECTED|PACKAGE_PRIVATE|PRIVATE --Asimplebuilder.methodAccess=PUBLIC|PROTECTED|PACKAGE_PRIVATE|PRIVATE +-Asimplebuilder.builderAccess=PUBLIC|PACKAGE_PRIVATE # PRIVATE not recommended (unusable builder) +-Asimplebuilder.builderConstructorAccess=PUBLIC|PACKAGE_PRIVATE|PRIVATE # PRIVATE recommended for factory pattern +-Asimplebuilder.methodAccess=PUBLIC|PACKAGE_PRIVATE # PRIVATE not recommended (unusable methods) -# Collection Helpers --Asimplebuilder.generateVarArgsHelpers=true|false --Asimplebuilder.usingArrayListBuilder=true|false --Asimplebuilder.usingArrayListBuilderWithElementBuilders=true|false --Asimplebuilder.usingHashSetBuilder=true|false --Asimplebuilder.usingHashSetBuilderWithElementBuilders=true|false --Asimplebuilder.usingHashMapBuilder=true|false +# Helper Methods +-Asimplebuilder.generateVarArgsHelpers=ENABLED|DISABLED +-Asimplebuilder.generateStringFormatHelpers=ENABLED|DISABLED +-Asimplebuilder.generateUnboxedOptional=ENABLED|DISABLED -# Integration --Asimplebuilder.generateWithInterface=true|false +# Collection Helpers +-Asimplebuilder.usingArrayListBuilder=ENABLED|DISABLED +-Asimplebuilder.usingArrayListBuilderWithElementBuilders=ENABLED|DISABLED +-Asimplebuilder.usingHashSetBuilder=ENABLED|DISABLED +-Asimplebuilder.usingHashSetBuilderWithElementBuilders=ENABLED|DISABLED +-Asimplebuilder.usingHashMapBuilder=ENABLED|DISABLED + +# Integration & Annotations +-Asimplebuilder.generateWithInterface=ENABLED|DISABLED +-Asimplebuilder.implementsBuilderBase=ENABLED|DISABLED +-Asimplebuilder.usingGeneratedAnnotation=ENABLED|DISABLED +-Asimplebuilder.usingBuilderImplementationAnnotation=ENABLED|DISABLED + +# Naming +-Asimplebuilder.builderSuffix=CustomSuffix +-Asimplebuilder.setterSuffix=customPrefix ``` ### Complete Options Example @@ -487,27 +999,39 @@ Or in compiler options: @SimpleBuilder @SimpleBuilder.Options( // Field Setter Generation - generateFieldSupplier = true, - generateFieldProvider = true, - generateBuilderProvider = true, + generateFieldSupplier = OptionState.ENABLED, + generateFieldConsumer = OptionState.ENABLED, + generateBuilderConsumer = OptionState.ENABLED, // Conditional Logic - generateConditionalHelper = true, + generateConditionalHelper = OptionState.ENABLED, // Access Control builderAccess = AccessModifier.PUBLIC, + builderConstructorAccess = AccessModifier.PUBLIC, methodAccess = AccessModifier.PUBLIC, + // Helper Methods + generateVarArgsHelpers = OptionState.ENABLED, + generateStringFormatHelpers = OptionState.ENABLED, + generateUnboxedOptional = OptionState.ENABLED, + // Collection Helpers - generateVarArgsHelpers = true, - usingArrayListBuilder = true, - usingArrayListBuilderWithElementBuilders = true, - usingHashSetBuilder = true, - usingHashSetBuilderWithElementBuilders = true, - usingHashMapBuilder = true, + usingArrayListBuilder = OptionState.ENABLED, + usingArrayListBuilderWithElementBuilders = OptionState.ENABLED, + usingHashSetBuilder = OptionState.ENABLED, + usingHashSetBuilderWithElementBuilders = OptionState.ENABLED, + usingHashMapBuilder = OptionState.ENABLED, + + // Integration & Annotations + generateWithInterface = OptionState.ENABLED, + implementsBuilderBase = OptionState.ENABLED, + usingGeneratedAnnotation = OptionState.ENABLED, + usingBuilderImplementationAnnotation = OptionState.ENABLED, - // Integration - generateWithInterface = true + // Naming + builderSuffix = "Builder", + setterSuffix = "" ) public class ExampleDto { private String name; @@ -516,6 +1040,4 @@ public class ExampleDto { --- -**Last Updated**: 2025-11-01 -**Version**: 0.2.0 **Related**: [README.md](../README.md) From b8f5c97009d1f3f2e9a2da8365b47650ff2ac636 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 7 Dec 2025 21:45:42 +0100 Subject: [PATCH 53/63] Adding validation of configuration which leads to broken builders --- .../util/BuilderConfigurationReader.java | 40 ++++++- .../BuilderConfigurationReaderTest.java | 105 ++++++++++++++++++ 2 files changed, 144 insertions(+), 1 deletion(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java index 68c345a9..6b093138 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java @@ -34,6 +34,7 @@ import org.javahelpers.simple.builders.core.enums.AccessModifier; import org.javahelpers.simple.builders.core.enums.OptionState; import org.javahelpers.simple.builders.processor.dtos.BuilderConfiguration; +import org.javahelpers.simple.builders.processor.exceptions.BuilderException; /** * Reads builder configuration from annotated elements. @@ -290,7 +291,7 @@ private BuilderConfiguration extractOptionsFromTemplateMirror(AnnotationMirror t * @param element the annotated element to resolve configuration for * @return the fully resolved configuration with all sources merged */ - public BuilderConfiguration resolveConfiguration(Element element) { + public BuilderConfiguration resolveConfiguration(Element element) throws BuilderException { logger.debug("Resolving configuration for element: %s", element.getSimpleName()); BuilderConfiguration templateConfig = readFromTemplate(element); @@ -302,8 +303,45 @@ public BuilderConfiguration resolveConfiguration(Element element) { .merge(templateConfig) .merge(inlineConfig); + // Validate access modifiers and warn about problematic configurations + validateAccessModifiers(element, result); + logger.debug("Configuration resolved for '%s': %s", element.getSimpleName(), result.toString()); return result; } + + /** + * Validates access modifier configurations and throws exception for invalid settings. + * + * @param element the element being processed + * @param config the resolved configuration + * @throws BuilderException if access modifiers are invalid + */ + private void validateAccessModifiers(Element element, BuilderConfiguration config) + throws BuilderException { + String elementName = element.getSimpleName().toString(); + + // Fail on PRIVATE builder access (makes builder completely unusable and causes Java compilation + // error) + if (config.builderAccess() == AccessModifier.PRIVATE) { + throw new BuilderException( + element, + "Builder for '%s' has builderAccess=PRIVATE which makes the builder class " + + "completely inaccessible and unusable (Java does not allow private top-level classes). " + + "Use PUBLIC or PACKAGE_PRIVATE instead. " + + "Note: Only builderConstructorAccess=PRIVATE is useful (for enforcing factory methods).", + elementName); + } + + // Fail on PRIVATE method access (makes all builder methods unusable) + if (config.methodAccess() == AccessModifier.PRIVATE) { + throw new BuilderException( + element, + "Builder for '%s' has methodAccess=PRIVATE which makes all setter methods " + + "inaccessible and the builder unusable. " + + "Use PUBLIC or PACKAGE_PRIVATE instead.", + elementName); + } + } } 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 d603a00f..e49e80ea 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 @@ -546,4 +546,109 @@ public static PersonDtoMinimalBuilder create() { + "options are correctly applied. If this fails, a configuration option may have been " + "added without proper processing support."); } + + /** + * Test: PRIVATE builderAccess fails builder generation. + * + *

      Verifies that using PRIVATE for builderAccess causes builder generation to fail with a clear + * error message, while allowing other builders to be processed successfully. + */ + @Test + void resolveConfiguration_PrivateBuilderAccess_FailsGeneration() { + JavaFileObject dtoSource = + ProcessorTestUtils.forSource( + """ + package test; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + import org.javahelpers.simple.builders.core.enums.AccessModifier; + + @SimpleBuilder(options = @SimpleBuilder.Options( + builderAccess = AccessModifier.PRIVATE + )) + public class PersonDto { + private String name; + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + } + """); + + // When: Compile + Compilation compilation = ProcessorTestUtils.createCompiler().compile(dtoSource); + + // Then: Builder generation fails but compilation succeeds (no invalid Java code generated) + assertThat(compilation).succeeded(); + assertThat(compilation).hadWarningContaining("Failed to generate builder"); + assertThat(compilation).hadWarningContaining("builderAccess=PRIVATE"); + assertThat(compilation).hadWarningContaining("Java does not allow private top-level classes"); + } + + /** + * Test: PRIVATE methodAccess fails builder generation. + * + *

      Verifies that using PRIVATE for methodAccess causes builder generation to fail with a clear + * error message, while allowing other builders to be processed successfully. + */ + @Test + void resolveConfiguration_PrivateMethodAccess_FailsGeneration() { + JavaFileObject dtoSource = + ProcessorTestUtils.forSource( + """ + package test; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + import org.javahelpers.simple.builders.core.enums.AccessModifier; + + @SimpleBuilder(options = @SimpleBuilder.Options( + methodAccess = AccessModifier.PRIVATE + )) + public class PersonDto { + private String name; + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + } + """); + + // When: Compile + Compilation compilation = ProcessorTestUtils.createCompiler().compile(dtoSource); + + // Then: Builder generation fails but compilation succeeds (no invalid Java code generated) + assertThat(compilation).succeeded(); + assertThat(compilation).hadWarningContaining("Failed to generate builder"); + assertThat(compilation).hadWarningContaining("methodAccess=PRIVATE"); + assertThat(compilation).hadWarningContaining("makes all setter methods inaccessible"); + } + + /** + * Test: PRIVATE builderConstructorAccess does NOT fail generation. + * + *

      Verifies that using PRIVATE for builderConstructorAccess is acceptable and does not cause + * builder generation to fail (it's a valid pattern to enforce using factory methods). + */ + @Test + void resolveConfiguration_PrivateBuilderConstructorAccess_NoFailure() { + JavaFileObject dtoSource = + ProcessorTestUtils.forSource( + """ + package test; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + import org.javahelpers.simple.builders.core.enums.AccessModifier; + + @SimpleBuilder(options = @SimpleBuilder.Options( + builderConstructorAccess = AccessModifier.PRIVATE + )) + public class PersonDto { + private String name; + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + } + """); + + // When: Compile + Compilation compilation = ProcessorTestUtils.createCompiler().compile(dtoSource); + + // Then: Compilation succeeds without warnings about access modifiers + assertThat(compilation).succeededWithoutWarnings(); + } } From fd129a998f5769143edfefae0a1482d0a40543df Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 7 Dec 2025 21:46:29 +0100 Subject: [PATCH 54/63] Updating configuration with validation --- docs/CONFIGURATION.md | 62 ++++++++++++++++++++++++++++++++----------- 1 file changed, 47 insertions(+), 15 deletions(-) diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index f0b9c9b4..ad16e7f2 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -287,7 +287,7 @@ Controls the visibility of the generated builder class. - `PUBLIC` - Builder accessible from anywhere (default, recommended for public APIs) - `PACKAGE_PRIVATE` - Builder only accessible within the same package (good for internal APIs) -⚠️ **Warning**: `PRIVATE` is technically supported but **not recommended** as it makes the builder class completely inaccessible and therefore useless. Even static factory methods won't help since the class itself is private. +⚠️ **Error**: `PRIVATE` is **not allowed** for `builderAccess`. If you try to use it, builder generation will fail with an error message explaining that Java does not allow private top-level classes. The builder will not be generated, but other DTOs in your project will continue processing normally. **Example with PACKAGE_PRIVATE**: ```java @@ -337,7 +337,7 @@ Controls the visibility of all generated setter methods. - `PUBLIC` - Methods accessible from anywhere (default, recommended) - `PACKAGE_PRIVATE` - Methods only accessible within the same package -⚠️ **Warning**: `PRIVATE` is technically supported but **not recommended** as it makes all builder setter methods private and therefore the builder completely unusable. +⚠️ **Error**: `PRIVATE` is **not allowed** for `methodAccess`. If you try to use it, builder generation will fail with an error message explaining that all setter methods would be inaccessible. The builder will not be generated, but other DTOs in your project will continue processing normally. **Example with PACKAGE_PRIVATE**: ```java @@ -901,6 +901,26 @@ Or in compiler 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 +### Builder Not Generated - Access Modifier Errors + +If you see warnings like "Failed to generate builder" with access modifier messages: + +**Problem**: Used `PRIVATE` for `builderAccess` or `methodAccess` +```java +@SimpleBuilder.Options(builderAccess = AccessModifier.PRIVATE) // ❌ ERROR +``` + +**Solution**: Use `PUBLIC` or `PACKAGE_PRIVATE` instead +```java +@SimpleBuilder.Options(builderAccess = AccessModifier.PACKAGE_PRIVATE) // ✅ OK +``` + +**Note**: Only `builderConstructorAccess = PRIVATE` is valid - this enforces using the `create()` factory method. + +**Error Messages**: +- `builderAccess=PRIVATE` → "Java does not allow private top-level classes" +- `methodAccess=PRIVATE` → "Makes all setter methods inaccessible" + ## Best Practices 1. **Use templates for common patterns**: Define reusable templates for your project @@ -933,24 +953,36 @@ Or in compiler options: ) ``` -❌ **Avoid These Combinations**: +❌ **Invalid Combinations (Will Cause Builder Generation to Fail)**: ```java -// ❌ WRONG: Private builder class is completely unusable -builderAccess = AccessModifier.PRIVATE // Nobody can access the class at all! - -// ❌ WRONG: Private methods make the builder unusable -methodAccess = AccessModifier.PRIVATE // Nobody can call the setter methods! - -// ⚠️ RARELY USEFUL: Public builder with private methods -builderAccess = AccessModifier.PUBLIC, -methodAccess = AccessModifier.PRIVATE // Builder exists but is unusable! +// ❌ ERROR: Private builder class causes generation failure +builderAccess = AccessModifier.PRIVATE +// Error: "Java does not allow private top-level classes" +// Result: Builder NOT generated, other DTOs continue processing + +// ❌ ERROR: Private methods cause generation failure +methodAccess = AccessModifier.PRIVATE +// Error: "Makes all setter methods inaccessible" +// Result: Builder NOT generated, other DTOs continue processing + +// ❌ ERROR: Both invalid configurations +builderAccess = AccessModifier.PRIVATE, +methodAccess = AccessModifier.PRIVATE +// Result: Builder NOT generated, other DTOs continue processing ``` **Why `PRIVATE` constructors are different**: -- ✅ `builderConstructorAccess = PRIVATE` **IS USEFUL** - Enforces using `create()` factory method -- ❌ `builderAccess = PRIVATE` **IS NOT USEFUL** - Makes the entire class inaccessible -- ❌ `methodAccess = PRIVATE` **IS NOT USEFUL** - Makes all methods inaccessible +- ✅ `builderConstructorAccess = PRIVATE` **IS ALLOWED** - Enforces using `create()` factory method (recommended pattern) +- ❌ `builderAccess = PRIVATE` **CAUSES ERROR** - Java doesn't allow private top-level classes +- ❌ `methodAccess = PRIVATE` **CAUSES ERROR** - Makes all methods inaccessible and builder unusable + +**What happens when validation fails**: +1. Builder generation for that DTO is skipped +2. A clear warning message is logged explaining the problem +3. Compilation continues and succeeds +4. Other DTOs in your project still get their builders generated +5. No invalid Java code is produced ## Reference From 61b091bcec6620043ccf5643556bdb7ecaa82079 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 7 Dec 2025 22:14:09 +0100 Subject: [PATCH 55/63] Improving codequality --- .../processor/dtos/BuilderConfiguration.java | 130 ++++++++---------- .../util/BuilderConfigurationReader.java | 119 +++++++++++----- .../CompilerArgumentsReaderTest.java | 22 +-- 3 files changed, 152 insertions(+), 119 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java index 710485d0..099bcf4a 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java @@ -212,91 +212,83 @@ public BuilderConfiguration merge(BuilderConfiguration other) { } return BuilderConfiguration.builder() - .generateSupplier( - other.generateFieldSupplier != UNSET - ? other.generateFieldSupplier - : this.generateFieldSupplier) - .generateConsumer( - other.generateFieldConsumer != UNSET - ? other.generateFieldConsumer - : this.generateFieldConsumer) + .generateSupplier(mergeOptionState(other.generateFieldSupplier, this.generateFieldSupplier)) + .generateConsumer(mergeOptionState(other.generateFieldConsumer, this.generateFieldConsumer)) .generateBuilderConsumer( - other.generateBuilderConsumer != UNSET - ? other.generateBuilderConsumer - : this.generateBuilderConsumer) + mergeOptionState(other.generateBuilderConsumer, this.generateBuilderConsumer)) .generateConditionalLogic( - other.generateConditionalHelper != UNSET - ? other.generateConditionalHelper - : this.generateConditionalHelper) - .builderAccess( - other.builderAccess != AccessModifier.DEFAULT - ? other.builderAccess - : this.builderAccess) + mergeOptionState(other.generateConditionalHelper, this.generateConditionalHelper)) + .builderAccess(mergeAccessModifier(other.builderAccess, this.builderAccess)) .builderConstructorAccess( - other.builderConstructorAccess != AccessModifier.DEFAULT - ? other.builderConstructorAccess - : this.builderConstructorAccess) - .methodAccess( - other.methodAccess != AccessModifier.DEFAULT ? other.methodAccess : this.methodAccess) + mergeAccessModifier(other.builderConstructorAccess, this.builderConstructorAccess)) + .methodAccess(mergeAccessModifier(other.methodAccess, this.methodAccess)) .generateVarArgsHelpers( - other.generateVarArgsHelpers != UNSET - ? other.generateVarArgsHelpers - : this.generateVarArgsHelpers) + mergeOptionState(other.generateVarArgsHelpers, this.generateVarArgsHelpers)) .generateStringFormatHelpers( - other.generateStringFormatHelpers != UNSET - ? other.generateStringFormatHelpers - : this.generateStringFormatHelpers) + mergeOptionState(other.generateStringFormatHelpers, this.generateStringFormatHelpers)) .generateUnboxedOptional( - other.generateUnboxedOptional != UNSET - ? other.generateUnboxedOptional - : this.generateUnboxedOptional) + mergeOptionState(other.generateUnboxedOptional, this.generateUnboxedOptional)) .usingArrayListBuilder( - other.usingArrayListBuilder != UNSET - ? other.usingArrayListBuilder - : this.usingArrayListBuilder) + mergeOptionState(other.usingArrayListBuilder, this.usingArrayListBuilder)) .usingArrayListBuilderWithElementBuilders( - other.usingArrayListBuilderWithElementBuilders != UNSET - ? other.usingArrayListBuilderWithElementBuilders - : this.usingArrayListBuilderWithElementBuilders) - .usingHashSetBuilder( - other.usingHashSetBuilder != UNSET - ? other.usingHashSetBuilder - : this.usingHashSetBuilder) + mergeOptionState( + other.usingArrayListBuilderWithElementBuilders, + this.usingArrayListBuilderWithElementBuilders)) + .usingHashSetBuilder(mergeOptionState(other.usingHashSetBuilder, this.usingHashSetBuilder)) .usingHashSetBuilderWithElementBuilders( - other.usingHashSetBuilderWithElementBuilders != UNSET - ? other.usingHashSetBuilderWithElementBuilders - : this.usingHashSetBuilderWithElementBuilders) - .usingHashMapBuilder( - other.usingHashMapBuilder != UNSET - ? other.usingHashMapBuilder - : this.usingHashMapBuilder) + mergeOptionState( + other.usingHashSetBuilderWithElementBuilders, + this.usingHashSetBuilderWithElementBuilders)) + .usingHashMapBuilder(mergeOptionState(other.usingHashMapBuilder, this.usingHashMapBuilder)) .usingGeneratedAnnotation( - other.usingGeneratedAnnotation != UNSET - ? other.usingGeneratedAnnotation - : this.usingGeneratedAnnotation) + mergeOptionState(other.usingGeneratedAnnotation, this.usingGeneratedAnnotation)) .usingBuilderImplementationAnnotation( - other.usingBuilderImplementationAnnotation != UNSET - ? other.usingBuilderImplementationAnnotation - : this.usingBuilderImplementationAnnotation) + mergeOptionState( + other.usingBuilderImplementationAnnotation, + this.usingBuilderImplementationAnnotation)) .implementsBuilderBase( - other.implementsBuilderBase != UNSET - ? other.implementsBuilderBase - : this.implementsBuilderBase) + mergeOptionState(other.implementsBuilderBase, this.implementsBuilderBase)) .generateWithInterface( - other.generateWithInterface != UNSET - ? other.generateWithInterface - : this.generateWithInterface) - .builderSuffix( - other.builderSuffix != null && !other.builderSuffix.isEmpty() - ? other.builderSuffix - : this.builderSuffix) - .setterSuffix( - other.setterSuffix != null && !other.setterSuffix.isEmpty() - ? other.setterSuffix - : this.setterSuffix) + mergeOptionState(other.generateWithInterface, this.generateWithInterface)) + .builderSuffix(mergeString(other.builderSuffix, this.builderSuffix)) + .setterSuffix(mergeString(other.setterSuffix, this.setterSuffix)) .build(); } + /** + * Merges two OptionState values, preferring the other value if it's not UNSET. + * + * @param other the other value (higher priority) + * @param thisValue the current value (lower priority) + * @return the merged value + */ + private static OptionState mergeOptionState(OptionState other, OptionState thisValue) { + return other != UNSET ? other : thisValue; + } + + /** + * Merges two AccessModifier values, preferring the other value if it's not DEFAULT. + * + * @param other the other value (higher priority) + * @param thisValue the current value (lower priority) + * @return the merged value + */ + private static AccessModifier mergeAccessModifier( + AccessModifier other, AccessModifier thisValue) { + return other != AccessModifier.DEFAULT ? other : thisValue; + } + + /** + * Merges two String values, preferring the other value if it's not null or empty. + * + * @param other the other value (higher priority) + * @param thisValue the current value (lower priority) + * @return the merged value + */ + private static String mergeString(String other, String thisValue) { + return other != null && !other.isEmpty() ? other : thisValue; + } + @Override public String toString() { ToStringBuilder builder = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java index 6b093138..3969de17 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java @@ -54,6 +54,13 @@ *

      Note: If {@code @SimpleBuilder} is present, custom template annotations are ignored. */ public class BuilderConfigurationReader { + private static final String SIMPLE_BUILDER_ANNOTATION = + "org.javahelpers.simple.builders.core.annotations.SimpleBuilder"; + private static final String SIMPLE_BUILDER_TEMPLATE_ANNOTATION = + "org.javahelpers.simple.builders.core.annotations.SimpleBuilder.Template"; + private static final String SIMPLE_BUILDER_TEMPLATE_ANNOTATION_ALT = + "org.javahelpers.simple.builders.core.annotations.SimpleBuilder$Template"; + private final BuilderConfiguration globalConfiguration; private final ProcessingLogger logger; private final Elements elementUtils; @@ -82,8 +89,7 @@ public BuilderConfigurationReader( */ public BuilderConfiguration readFromInlineOptions(Element element) { AnnotationMirror simpleBuilderMirror = - extractAnnotationMirror( - element, "org.javahelpers.simple.builders.core.annotations.SimpleBuilder"); + extractAnnotationMirror(element, SIMPLE_BUILDER_ANNOTATION); return extractOptionsFromAnnotationMirror(simpleBuilderMirror); } @@ -185,6 +191,9 @@ private BuilderConfiguration parseOptionsFromMirror(AnnotationMirror optionsMirr builder.generateWithInterface(OptionState.valueOf(enumValue)); case "builderSuffix" -> builder.builderSuffix(value.toString()); case "setterSuffix" -> builder.setterSuffix(value.toString()); + default -> + logger.warning( + "Unknown configuration option '%s' with value '%s' - ignoring", name, value); } } @@ -211,45 +220,92 @@ private String extractEnumName(Object value) { * @return configuration from the template annotation, or null if not present */ public BuilderConfiguration readFromTemplate(Element element) { - // If @SimpleBuilder is present, ignore template annotations (inline options take full - // precedence) + // If @SimpleBuilder is present, ignore template annotations + if (hasSimpleBuilderAnnotation(element)) { + logger.debug( + "Template annotations ignored for '%s' (direct @SimpleBuilder present)", + element.getSimpleName()); + return null; + } + + // Check all annotations on the element to find one annotated with @SimpleBuilder.Template for (AnnotationMirror mirror : element.getAnnotationMirrors()) { - if (mirror - .getAnnotationType() - .toString() - .equals("org.javahelpers.simple.builders.core.annotations.SimpleBuilder")) { - logger.debug( - "Template annotations ignored for '%s' (direct @SimpleBuilder present)", - element.getSimpleName()); - return null; + BuilderConfiguration templateConfig = checkForTemplateAnnotation(mirror, element); + if (templateConfig != null) { + return templateConfig; } } - // Check all annotations on the element to find one annotated with @SimpleBuilder.Template + return null; + } + + /** + * Checks if the element has a direct @SimpleBuilder annotation. + * + * @param element the element to check + * @return true if @SimpleBuilder is present + */ + private boolean hasSimpleBuilderAnnotation(Element element) { for (AnnotationMirror mirror : element.getAnnotationMirrors()) { - Element annotationElement = mirror.getAnnotationType().asElement(); - - // Check using AnnotationMirror for template annotations - // (this gives us only explicitly set values) - for (AnnotationMirror metaMirror : annotationElement.getAnnotationMirrors()) { - String metaAnnotationName = metaMirror.getAnnotationType().toString(); - if (metaAnnotationName.equals( - "org.javahelpers.simple.builders.core.annotations.SimpleBuilder.Template") - || metaAnnotationName.equals( - "org.javahelpers.simple.builders.core.annotations.SimpleBuilder$Template")) { - // Found template via mirror - extract options using AnnotationMirror parsing - // This approach only gives us explicitly set values, not annotation defaults - logger.debug( - "Found template annotation '%s' on '%s'", - annotationElement.getSimpleName(), element.getSimpleName()); - return extractOptionsFromTemplateMirror(metaMirror); - } + if (isSimpleBuilderAnnotation(mirror)) { + return true; } } + return false; + } + /** + * Checks if an annotation mirror represents @SimpleBuilder. + * + * @param mirror the annotation mirror to check + * @return true if this is @SimpleBuilder + */ + private boolean isSimpleBuilderAnnotation(AnnotationMirror mirror) { + String typeName = mirror.getAnnotationType().toString(); + 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. + * + * @param metaMirror the meta-annotation mirror to check + * @return true if this is @SimpleBuilder.Template + */ + private boolean isTemplateAnnotation(AnnotationMirror metaMirror) { + String metaAnnotationName = metaMirror.getAnnotationType().toString(); + // Check both possible representations of nested annotation + if (metaAnnotationName.equals(SIMPLE_BUILDER_TEMPLATE_ANNOTATION)) { + return true; + } + if (metaAnnotationName.equals(SIMPLE_BUILDER_TEMPLATE_ANNOTATION_ALT)) { + return true; + } + return false; + } + /** * Extracts configuration from @SimpleBuilder.Template(options = ...) using AnnotationMirror. * Fallback for same-round compiled templates where reflection doesn't work. @@ -262,8 +318,7 @@ private BuilderConfiguration extractOptionsFromTemplateMirror(AnnotationMirror t templateValues.entrySet()) { if (entry.getKey().getSimpleName().toString().equals("options")) { Object value = entry.getValue().getValue(); - if (value instanceof AnnotationMirror) { - AnnotationMirror optionsMirror = (AnnotationMirror) value; + if (value instanceof AnnotationMirror optionsMirror) { return parseOptionsFromMirror(optionsMirror); } } diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/CompilerArgumentsReaderTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/CompilerArgumentsReaderTest.java index 08c4a338..0edef695 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/CompilerArgumentsReaderTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/CompilerArgumentsReaderTest.java @@ -114,25 +114,11 @@ void readBooleanValue_EmptyString_ReturnsFalse() { "Should return false for empty string"); } - /** Test: readBooleanValue handles case-insensitive "true". */ + /** Test: readBooleanValue handles case-insensitive "true" and "enabled". */ @ParameterizedTest - @ValueSource(strings = {"true", "TRUE", "True", "TrUe"}) - void readBooleanValue_CaseInsensitiveTrue_ReturnsTrue(String value) { - Map options = new HashMap<>(); - options.put("simplebuilder.generateFieldSupplier", value); - - ProcessingEnvironment env = ProcessingEnvironmentStub.create(options); - CompilerArgumentsReader reader = new CompilerArgumentsReader(env); - - assertTrue( - reader.readBooleanValue(CompilerArgumentsEnum.GENERATE_FIELD_SUPPLIER), - "Should return true for: " + value); - } - - /** Test: readBooleanValue handles case-insensitive "enabled". */ - @ParameterizedTest - @ValueSource(strings = {"enabled", "ENABLED", "Enabled", "EnAbLeD"}) - void readBooleanValue_CaseInsensitiveEnabled_ReturnsTrue(String value) { + @ValueSource( + strings = {"true", "TRUE", "True", "TrUe", "enabled", "ENABLED", "Enabled", "EnAbLeD"}) + void readBooleanValue_CaseInsensitiveTrueOrEnabled_ReturnsTrue(String value) { Map options = new HashMap<>(); options.put("simplebuilder.generateFieldSupplier", value); From e5c99d2a823622374b4e8b13c0bdeb01441bdfd1 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 7 Dec 2025 22:21:34 +0100 Subject: [PATCH 56/63] Updating javadoc in SimpleBuilder --- .../core/annotations/SimpleBuilder.java | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) 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 c0d682ce..2fcf6ea8 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 @@ -44,7 +44,8 @@ *

    • Field Setters: generateFieldSupplier, generateFieldConsumer, generateBuilderConsumer * (all default: true) *
    • Conditional Logic: generateConditionalHelper (default: true) - *
    • Access Control: builderAccess, methodAccess (default: PUBLIC) + *
    • Access Control: builderAccess, builderConstructorAccess, methodAccess (default: + * PUBLIC) *
    • Collection Helpers: generateVarArgsHelpers, usingArrayListBuilder, * usingArrayListBuilderWithElementBuilders, usingHashSetBuilder, * usingHashSetBuilderWithElementBuilders, usingHashMapBuilder (all default: true) @@ -174,16 +175,17 @@ // === Access Control === /** - * Access level for generated builder class. - * - *

      Available values: + * Access level for the generated builder class. * *

        *
      • PUBLIC - For public APIs (default) *
      • PACKAGE_PRIVATE - For internal use within a package - *
      • PRIVATE - When using only static factory methods *
      * + *

      Note: {@code PRIVATE} is not allowed for builder classes. Java does not + * allow private top-level classes, so using {@code PRIVATE} will cause builder generation to + * fail with a clear error message. Use {@code PACKAGE_PRIVATE} for internal builders instead. + * *

      Example: * *

      {@code
      @@ -197,7 +199,9 @@
            *
            * 

      Default: {@link AccessModifier#PUBLIC PUBLIC} * - *

      Compiler option: -Asimplebuilder.builderAccess (values: PUBLIC, PACKAGE_PRIVATE, PRIVATE) + *

      Compiler option: -Asimplebuilder.builderAccess (values: PUBLIC, PACKAGE_PRIVATE) + * + * @see #builderConstructorAccess() for controlling constructor visibility */ AccessModifier builderAccess() default AccessModifier.PUBLIC; @@ -230,6 +234,10 @@ * *

      Typically matches builder class access. Use PACKAGE_PRIVATE for internal APIs. * + *

      Note: {@code PRIVATE} is not allowed for builder methods. Private methods + * would make all setter methods inaccessible, rendering the builder unusable. Using {@code + * PRIVATE} will cause builder generation to fail with a clear error message. + * *

      Example: * *

      {@code
      @@ -243,7 +251,7 @@
            *
            * 

      Default: {@link AccessModifier#PUBLIC PUBLIC} * - *

      Compiler option: -Asimplebuilder.methodAccess (values: PUBLIC, PACKAGE_PRIVATE, PRIVATE) + *

      Compiler option: -Asimplebuilder.methodAccess (values: PUBLIC, PACKAGE_PRIVATE) */ AccessModifier methodAccess() default AccessModifier.PUBLIC; From fc5db921bab4a3b85c44d0961ed81e9db86f173a Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 7 Dec 2025 22:42:05 +0100 Subject: [PATCH 57/63] Refactoring code to increase code quality --- .../util/BuilderConfigurationReader.java | 11 ++---- .../util/BuilderDefinitionCreator.java | 36 +++++-------------- 2 files changed, 11 insertions(+), 36 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java index 3969de17..95e48feb 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java @@ -297,13 +297,8 @@ private BuilderConfiguration checkForTemplateAnnotation( private boolean isTemplateAnnotation(AnnotationMirror metaMirror) { String metaAnnotationName = metaMirror.getAnnotationType().toString(); // Check both possible representations of nested annotation - if (metaAnnotationName.equals(SIMPLE_BUILDER_TEMPLATE_ANNOTATION)) { - return true; - } - if (metaAnnotationName.equals(SIMPLE_BUILDER_TEMPLATE_ANNOTATION_ALT)) { - return true; - } - return false; + return metaAnnotationName.equals(SIMPLE_BUILDER_TEMPLATE_ANNOTATION) + || metaAnnotationName.equals(SIMPLE_BUILDER_TEMPLATE_ANNOTATION_ALT); } /** @@ -373,7 +368,7 @@ public BuilderConfiguration resolveConfiguration(Element element) throws Builder * @param config the resolved configuration * @throws BuilderException if access modifiers are invalid */ - private void validateAccessModifiers(Element element, BuilderConfiguration config) + private static void validateAccessModifiers(Element element, BuilderConfiguration config) throws BuilderException { String elementName = element.getSimpleName().toString(); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java index 16e0eb31..528620d9 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java @@ -277,7 +277,6 @@ private static void addAdditionalHelperMethodsForField( ProcessingContext context) { String fieldNameInBuilder = field.getFieldName(); String fieldJavaDoc = field.getJavaDoc(); - Modifier methodAccessModifier = getMethodAccessModifier(context); // Check for String type (not array) and add format method if (isString(field.getFieldType()) @@ -293,7 +292,6 @@ private static void addAdditionalHelperMethodsForField( annotations, builderType, context); - setMethodAccessModifier(method, methodAccessModifier); field.addMethod(method); } @@ -306,7 +304,6 @@ private static void addAdditionalHelperMethodsForField( MethodDto method1 = createFieldSetterForArrayFromList( fieldName, fieldNameInBuilder, listType, elementType, builderType, context); - setMethodAccessModifier(method1, methodAccessModifier); field.addMethod(method1); // Add Consumer> method only if builder consumers are enabled @@ -320,7 +317,6 @@ private static void addAdditionalHelperMethodsForField( elementType, builderType, context); - setMethodAccessModifier(method2, methodAccessModifier); field.addMethod(method2); } return; @@ -346,7 +342,6 @@ private static void addAdditionalHelperMethodsForField( new TypeNameArray(innerTypes.get(0), false), builderType, context); - setMethodAccessModifier(method, methodAccessModifier); field.addMethod(method); } } else if (isSet(field.getFieldType()) && innerTypesCnt == 1) { @@ -362,7 +357,6 @@ private static void addAdditionalHelperMethodsForField( new TypeNameArray(innerTypes.get(0), true), builderType, context); - setMethodAccessModifier(method, methodAccessModifier); field.addMethod(method); } } else if (isMap(field.getFieldType()) && innerTypesCnt == 2) { @@ -382,7 +376,6 @@ private static void addAdditionalHelperMethodsForField( mapEntryType, builderType, context); - setMethodAccessModifier(method, methodAccessModifier); field.addMethod(method); } } else if (isOptional(field.getFieldType()) && innerTypesCnt == 1) { @@ -400,7 +393,6 @@ private static void addAdditionalHelperMethodsForField( innerTypes.get(0), builderType, context); - setMethodAccessModifier(method, methodAccessModifier); field.addMethod(method); } @@ -416,7 +408,6 @@ private static void addAdditionalHelperMethodsForField( List.of(), builderType, context); - setMethodAccessModifier(method, methodAccessModifier); field.addMethod(method); } } @@ -467,7 +458,6 @@ private static boolean tryAddBuilderConsumer( fieldBuilderType, builderType, context); - setMethodAccessModifier(method, getMethodAccessModifier(context)); field.addMethod(method); return true; } @@ -498,7 +488,6 @@ && hasEmptyConstructor(fieldTypeElement, context)) { field.getFieldType(), builderType, context); - setMethodAccessModifier(method, getMethodAccessModifier(context)); field.addMethod(method); return true; } @@ -525,7 +514,6 @@ private static boolean tryAddStringBuilderConsumer( transform, builderType, context); - setMethodAccessModifier(method, getMethodAccessModifier(context)); field.addMethod(method); return true; } @@ -575,7 +563,6 @@ private static boolean tryAddListConsumer( elementBuilderType.get(), builderType, context); - setMethodAccessModifier(method, getMethodAccessModifier(context)); field.addMethod(method); } else if (context.getConfiguration().shouldUseArrayListBuilder()) { // Regular ArrayListBuilder if enabled @@ -589,7 +576,6 @@ private static boolean tryAddListConsumer( elementType, builderType, context); - setMethodAccessModifier(method, getMethodAccessModifier(context)); field.addMethod(method); } else { return false; @@ -627,7 +613,6 @@ private static boolean tryAddMapConsumer( builderTargetTypeName, builderType, context); - setMethodAccessModifier(mapConsumerWithBuilder, getMethodAccessModifier(context)); field.addMethod(mapConsumerWithBuilder); return true; } @@ -675,7 +660,6 @@ private static boolean tryAddSetConsumer( elementBuilderType.get(), builderType, context); - setMethodAccessModifier(method, getMethodAccessModifier(context)); field.addMethod(method); } else if (context.getConfiguration().shouldUseHashSetBuilder()) { // Regular HashSetBuilder if enabled @@ -689,7 +673,6 @@ private static boolean tryAddSetConsumer( elementType, builderType, context); - setMethodAccessModifier(method, getMethodAccessModifier(context)); field.addMethod(method); } else { return false; @@ -721,7 +704,6 @@ private static void addSupplierMethodsForField( field.getFieldType(), builderType, context); - setMethodAccessModifier(method, getMethodAccessModifier(context)); field.addMethod(method); } @@ -923,7 +905,6 @@ private static Optional createFieldDto( annotations, builderType, context); - setMethodAccessModifier(method, getMethodAccessModifier(context)); field.addMethod(method); // Add consumer/supplier/helper methods - use ORIGINAL field name for method names @@ -991,7 +972,7 @@ private static MethodDto createFieldSetterWithTransform( methodDto.setMethodName(generateSetterName(fieldName, context)); methodDto.setReturnType(builderType); methodDto.addParameter(parameter); - // Modifier is controlled by configuration, not set here + setMethodAccessModifier(methodDto, getMethodAccessModifier(context)); String params; if (StringUtils.isBlank(transform)) { params = parameter.getParameterName(); @@ -1035,7 +1016,7 @@ private static MethodDto createFieldConsumer( methodDto.setMethodName(generateSetterName(fieldName, context)); methodDto.setReturnType(builderType); methodDto.addParameter(parameter); - // Modifier is controlled by configuration, not set here + setMethodAccessModifier(methodDto, getMethodAccessModifier(context)); methodDto.setCode( """ $helperType:T consumer = this.$fieldName:N.isSet() ? this.$fieldName:N.value() : new $helperType:T(); @@ -1075,7 +1056,7 @@ private static MethodDto createStringBuilderConsumer( MethodDto methodDto = new MethodDto(); methodDto.setMethodName(generateSetterName(fieldName, context)); methodDto.addParameter(parameter); - // Modifier is controlled by configuration, not set here + setMethodAccessModifier(methodDto, getMethodAccessModifier(context)); methodDto.setCode( """ StringBuilder builder = new StringBuilder(); @@ -1193,7 +1174,7 @@ private static MethodDto createFieldConsumerWithBuilder( methodDto.setMethodName(generateSetterName(fieldName, context)); methodDto.setReturnType(returnBuilderType); methodDto.addParameter(parameter); - // Modifier is controlled by configuration, not set here + setMethodAccessModifier(methodDto, getMethodAccessModifier(context)); methodDto.setCode( """ $helperType:T builder = this.$fieldName:N.isSet() ? new $helperType:T(%s) : new $helperType:T(%s); @@ -1234,7 +1215,7 @@ private static MethodDto createFieldSupplier( methodDto.setMethodName(generateSetterName(fieldName, context)); methodDto.setReturnType(builderType); methodDto.addParameter(parameter); - // Modifier is controlled by configuration, not set here + setMethodAccessModifier(methodDto, getMethodAccessModifier(context)); methodDto.setCode( """ this.$fieldName:N = $builderFieldWrapper:T.changedValue($dtoMethodParam:N.get()); @@ -1280,7 +1261,7 @@ private static MethodDto createStringFormatMethodWithTransform( methodDto.setReturnType(builderType); methodDto.addParameter(formatParam); methodDto.addParameter(argsParam); - // Modifier is controlled by configuration, not set here + setMethodAccessModifier(methodDto, getMethodAccessModifier(context)); methodDto.setCode( """ this.$fieldName:N = $builderFieldWrapper:T.changedValue($transform:N); @@ -1331,7 +1312,7 @@ private static MethodDto createFieldSetterForArrayFromList( methodDto.setMethodName(generateSetterName(fieldName, context)); methodDto.setReturnType(builderType); methodDto.addParameter(parameter); - // Modifier is controlled by configuration, not set here + setMethodAccessModifier(methodDto, getMethodAccessModifier(context)); methodDto.setCode( """ this.$fieldName:N = $builderFieldWrapper:T.changedValue($dtoMethodParams:N.toArray(new $elementType:T[0])); @@ -1376,8 +1357,7 @@ private static MethodDto createFieldConsumerWithArrayBuilder( methodDto.setMethodName(generateSetterName(fieldName, context)); methodDto.setReturnType(returnBuilderType); methodDto.addParameter(parameter); - // Modifier is controlled by configuration, not set here - + setMethodAccessModifier(methodDto, getMethodAccessModifier(context)); methodDto.setCode( """ $helperType:T builder = this.$fieldName:N.isSet() ? new $helperType:T(java.util.List.of(this.$fieldName:N.value())) : new $helperType:T(); From 871ae6117c9a4dfafad96bd84fd2ad765bfbe948 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 7 Dec 2025 22:46:56 +0100 Subject: [PATCH 58/63] Fixing handling of strings in configuration, trailing and leading spaces should be removed when reading the configuration --- .../builders/processor/dtos/BuilderConfiguration.java | 10 +++++++--- .../processor/util/BuilderDefinitionCreator.java | 4 ++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java index 099bcf4a..1422e948 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java @@ -29,6 +29,7 @@ import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; +import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; import org.javahelpers.simple.builders.core.enums.AccessModifier; import org.javahelpers.simple.builders.core.enums.OptionState; @@ -279,7 +280,10 @@ private static AccessModifier mergeAccessModifier( } /** - * Merges two String values, preferring the other value if it's not null or empty. + * Merges two String values, preferring the other value if it's not null. + * + *

      Note: String values are normalized with trimToNull in the builder, so null means + * unset/blank. * * @param other the other value (higher priority) * @param thisValue the current value (lower priority) @@ -574,12 +578,12 @@ public Builder methodAccess(String value) { } public Builder builderSuffix(String value) { - this.builderSuffix = value; + this.builderSuffix = value == null ? null : value.trim(); return this; } public Builder setterSuffix(String value) { - this.setterSuffix = value; + this.setterSuffix = value == null ? null : value.trim(); return this; } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java index 528620d9..4172ff36 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java @@ -1633,10 +1633,10 @@ private static MethodDto createWithBuilderMethod(BuilderDefinitionDto builderDef */ private static String generateSetterName(String fieldName, ProcessingContext context) { String suffix = context.getConfiguration().getSetterSuffix(); - if (StringUtils.isBlank(suffix)) { + if (suffix == null || suffix.isEmpty()) { return fieldName; } - return StringUtils.trim(suffix) + StringUtils.capitalize(fieldName); + return suffix + StringUtils.capitalize(fieldName); } /** From 5497c4bc34abbfb39384966007e83f49a767744b Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sat, 20 Dec 2025 18:09:04 +0100 Subject: [PATCH 59/63] Adding an example of own Builder-Annotation in example project --- .../simple/builders/example/BookDto.java | 416 ++++++++++++++++++ .../builders/example/ElementaryBuilder.java | 53 +++ .../builders/example/BookDtoBuilderTest.java | 125 ++++++ 3 files changed, 594 insertions(+) create mode 100644 example/src/main/java/org/javahelpers/simple/builders/example/BookDto.java create mode 100644 example/src/main/java/org/javahelpers/simple/builders/example/ElementaryBuilder.java create mode 100644 example/src/test/java/org/javahelpers/simple/builders/example/BookDtoBuilderTest.java diff --git a/example/src/main/java/org/javahelpers/simple/builders/example/BookDto.java b/example/src/main/java/org/javahelpers/simple/builders/example/BookDto.java new file mode 100644 index 00000000..773d272a --- /dev/null +++ b/example/src/main/java/org/javahelpers/simple/builders/example/BookDto.java @@ -0,0 +1,416 @@ +/* + * MIT License + * + * Copyright (c) 2025 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.example; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +/** + * Elementary builder example demonstrating all fundamental Java property types. + * + *

      This DTO showcases the {@link ElementaryBuilder} annotation which generates a setter-only + * builder without advanced features like suppliers, consumers, or collection builders. + * + *

      Supported property types include: + * + *

        + *
      • Primitive types: int, double, boolean, byte, short, long, float, char + *
      • String types: title, author, isbn + *
      • BigDecimal for precise decimal values + *
      • Date/Time types: LocalDate, LocalDateTime + *
      • Optional types for nullable values + *
      • Collections: List, Set, Map + *
      • Complex objects: PersonDto + *
      + */ +@ElementaryBuilder +public class BookDto { + private String title; + private String author; + private String isbn; + private int pages; + private double price; + private BigDecimal exactPrice; + private boolean available; + private byte rating; + private short edition; + private long salesCount; + private float discount; + private char category; + private LocalDate publishDate; + private LocalDateTime lastUpdated; + private Optional subtitle; + private List tags; + private Set genres; + private Map metadata; + private PersonDto publisher; + + /** + * Gets the title of the book. + * + * @return the book title + */ + public String getTitle() { + return title; + } + + /** + * Sets the title of the book. + * + * @param title the book title to set + */ + public void setTitle(String title) { + this.title = title; + } + + /** + * Gets the author of the book. + * + * @return the book author + */ + public String getAuthor() { + return author; + } + + /** + * Sets the author of the book. + * + * @param author the book author to set + */ + public void setAuthor(String author) { + this.author = author; + } + + /** + * Gets the ISBN (International Standard Book Number) of the book. + * + * @return the ISBN + */ + public String getIsbn() { + return isbn; + } + + /** + * Sets the ISBN (International Standard Book Number) of the book. + * + * @param isbn the ISBN to set + */ + public void setIsbn(String isbn) { + this.isbn = isbn; + } + + /** + * Gets the number of pages in the book. + * + * @return the page count + */ + public int getPages() { + return pages; + } + + /** + * Sets the number of pages in the book. + * + * @param pages the page count to set + */ + public void setPages(int pages) { + this.pages = pages; + } + + /** + * Gets the price of the book as a double. + * + * @return the book price + */ + public double getPrice() { + return price; + } + + /** + * Sets the price of the book as a double. + * + * @param price the book price to set + */ + public void setPrice(double price) { + this.price = price; + } + + /** + * Gets the exact price of the book as a BigDecimal for precise decimal calculations. + * + * @return the exact book price + */ + public BigDecimal getExactPrice() { + return exactPrice; + } + + /** + * Sets the exact price of the book as a BigDecimal for precise decimal calculations. + * + * @param exactPrice the exact book price to set + */ + public void setExactPrice(BigDecimal exactPrice) { + this.exactPrice = exactPrice; + } + + /** + * Checks if the book is available for purchase or loan. + * + * @return true if available, false otherwise + */ + public boolean isAvailable() { + return available; + } + + /** + * Sets the availability status of the book. + * + * @param available true if available, false otherwise + */ + public void setAvailable(boolean available) { + this.available = available; + } + + /** + * Gets the rating of the book (typically 1-5). + * + * @return the book rating + */ + public byte getRating() { + return rating; + } + + /** + * Sets the rating of the book (typically 1-5). + * + * @param rating the book rating to set + */ + public void setRating(byte rating) { + this.rating = rating; + } + + /** + * Gets the edition number of the book. + * + * @return the edition number + */ + public short getEdition() { + return edition; + } + + /** + * Sets the edition number of the book. + * + * @param edition the edition number to set + */ + public void setEdition(short edition) { + this.edition = edition; + } + + /** + * Gets the total number of copies sold. + * + * @return the sales count + */ + public long getSalesCount() { + return salesCount; + } + + /** + * Sets the total number of copies sold. + * + * @param salesCount the sales count to set + */ + public void setSalesCount(long salesCount) { + this.salesCount = salesCount; + } + + /** + * Gets the discount percentage applied to the book (e.g., 0.15 for 15% off). + * + * @return the discount percentage + */ + public float getDiscount() { + return discount; + } + + /** + * Sets the discount percentage applied to the book (e.g., 0.15 for 15% off). + * + * @param discount the discount percentage to set + */ + public void setDiscount(float discount) { + this.discount = discount; + } + + /** + * Gets the category code of the book (e.g., 'T' for Technical, 'F' for Fiction). + * + * @return the category code + */ + public char getCategory() { + return category; + } + + /** + * Sets the category code of the book (e.g., 'T' for Technical, 'F' for Fiction). + * + * @param category the category code to set + */ + public void setCategory(char category) { + this.category = category; + } + + /** + * Gets the publication date of the book. + * + * @return the publication date + */ + public LocalDate getPublishDate() { + return publishDate; + } + + /** + * Sets the publication date of the book. + * + * @param publishDate the publication date to set + */ + public void setPublishDate(LocalDate publishDate) { + this.publishDate = publishDate; + } + + /** + * Gets the timestamp when the book information was last updated. + * + * @return the last update timestamp + */ + public LocalDateTime getLastUpdated() { + return lastUpdated; + } + + /** + * Sets the timestamp when the book information was last updated. + * + * @param lastUpdated the last update timestamp to set + */ + public void setLastUpdated(LocalDateTime lastUpdated) { + this.lastUpdated = lastUpdated; + } + + /** + * Gets the optional subtitle of the book. + * + * @return an Optional containing the subtitle, or empty if no subtitle exists + */ + public Optional getSubtitle() { + return subtitle; + } + + /** + * Sets the optional subtitle of the book. + * + * @param subtitle an Optional containing the subtitle to set + */ + public void setSubtitle(Optional subtitle) { + this.subtitle = subtitle; + } + + /** + * Gets the list of tags associated with the book (e.g., "programming", "best-practices"). + * + * @return the list of tags + */ + public List getTags() { + return tags; + } + + /** + * Sets the list of tags associated with the book. + * + * @param tags the list of tags to set + */ + public void setTags(List tags) { + this.tags = tags; + } + + /** + * Gets the set of genres the book belongs to (e.g., "Technical", "Software Engineering"). + * + * @return the set of genres + */ + public Set getGenres() { + return genres; + } + + /** + * Sets the set of genres the book belongs to. + * + * @param genres the set of genres to set + */ + public void setGenres(Set genres) { + this.genres = genres; + } + + /** + * Gets the metadata map containing additional book information (e.g., language, format). + * + * @return the metadata map + */ + public Map getMetadata() { + return metadata; + } + + /** + * Sets the metadata map containing additional book information. + * + * @param metadata the metadata map to set + */ + public void setMetadata(Map metadata) { + this.metadata = metadata; + } + + /** + * Gets the publisher information as a PersonDto. + * + * @return the publisher + */ + public PersonDto getPublisher() { + return publisher; + } + + /** + * Sets the publisher information. + * + * @param publisher the publisher to set + */ + public void setPublisher(PersonDto publisher) { + this.publisher = publisher; + } +} diff --git a/example/src/main/java/org/javahelpers/simple/builders/example/ElementaryBuilder.java b/example/src/main/java/org/javahelpers/simple/builders/example/ElementaryBuilder.java new file mode 100644 index 00000000..24d1b4d2 --- /dev/null +++ b/example/src/main/java/org/javahelpers/simple/builders/example/ElementaryBuilder.java @@ -0,0 +1,53 @@ +/* + * MIT License + * + * Copyright (c) 2025 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.example; + +import java.lang.annotation.ElementType; +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, + generateFieldConsumer = OptionState.DISABLED, + generateBuilderConsumer = OptionState.DISABLED, + generateConditionalHelper = OptionState.DISABLED, + generateVarArgsHelpers = OptionState.DISABLED, + generateStringFormatHelpers = OptionState.DISABLED, + generateUnboxedOptional = OptionState.DISABLED, + usingArrayListBuilder = OptionState.DISABLED, + usingArrayListBuilderWithElementBuilders = OptionState.DISABLED, + usingHashSetBuilder = OptionState.DISABLED, + usingHashSetBuilderWithElementBuilders = OptionState.DISABLED, + usingHashMapBuilder = OptionState.DISABLED, + generateWithInterface = OptionState.DISABLED, + usingGeneratedAnnotation = OptionState.DISABLED)) +@Retention(RetentionPolicy.CLASS) +@Target(ElementType.TYPE) +public @interface ElementaryBuilder {} diff --git a/example/src/test/java/org/javahelpers/simple/builders/example/BookDtoBuilderTest.java b/example/src/test/java/org/javahelpers/simple/builders/example/BookDtoBuilderTest.java new file mode 100644 index 00000000..7cc0a69a --- /dev/null +++ b/example/src/test/java/org/javahelpers/simple/builders/example/BookDtoBuilderTest.java @@ -0,0 +1,125 @@ +/* + * MIT License + * + * Copyright (c) 2025 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.example; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import org.junit.jupiter.api.Test; + +class BookDtoBuilderTest { + + @Test + void testElementaryBuilderWithAllPropertyTypes() { + LocalDate publishDate = LocalDate.of(2008, 8, 1); + LocalDateTime lastUpdated = LocalDateTime.of(2024, 12, 20, 10, 30); + + BookDto book = + BookDtoBuilder.create() + .title("Clean Code") + .author("Robert C. Martin") + .isbn("978-0132350884") + .pages(464) + .price(39.99) + .exactPrice(new BigDecimal("39.99")) + .available(true) + .rating((byte) 5) + .edition((short) 1) + .salesCount(1000000L) + .discount(0.15f) + .category('T') + .publishDate(publishDate) + .lastUpdated(lastUpdated) + .subtitle(Optional.of("A Handbook of Agile Software Craftsmanship")) + .tags(List.of("programming", "clean-code", "best-practices")) + .genres(Set.of("Technical", "Software Engineering")) + .metadata(Map.of("language", "English", "format", "Paperback")) + .publisher(PersonDtoBuilder.create().name("Prentice Hall").build()) + .build(); + + assertNotNull(book); + assertEquals("Clean Code", book.getTitle()); + assertEquals("Robert C. Martin", book.getAuthor()); + assertEquals("978-0132350884", book.getIsbn()); + assertEquals(464, book.getPages()); + assertEquals(39.99, book.getPrice()); + assertEquals(new BigDecimal("39.99"), book.getExactPrice()); + assertTrue(book.isAvailable()); + assertEquals((byte) 5, book.getRating()); + assertEquals((short) 1, book.getEdition()); + assertEquals(1000000L, book.getSalesCount()); + assertEquals(0.15f, book.getDiscount()); + assertEquals('T', book.getCategory()); + assertEquals(publishDate, book.getPublishDate()); + assertEquals(lastUpdated, book.getLastUpdated()); + assertEquals(Optional.of("A Handbook of Agile Software Craftsmanship"), book.getSubtitle()); + assertEquals(3, book.getTags().size()); + assertEquals(2, book.getGenres().size()); + assertEquals(2, book.getMetadata().size()); + assertNotNull(book.getPublisher()); + assertEquals("Prentice Hall", book.getPublisher().getName()); + } + + @Test + void testPartialBuilder() { + BookDto book = + BookDtoBuilder.create() + .title("Effective Java") + .author("Joshua Bloch") + .available(false) + .build(); + + assertNotNull(book); + assertEquals("Effective Java", book.getTitle()); + assertEquals("Joshua Bloch", book.getAuthor()); + assertFalse(book.isAvailable()); + assertEquals(0, book.getPages()); + assertEquals(0.0, book.getPrice()); + } + + @Test + void testSetterOnlyBuilder() { + BookDto book = + BookDtoBuilder.create() + .title("Design Patterns") + .author("Gang of Four") + .pages(395) + .build(); + + assertNotNull(book); + assertEquals("Design Patterns", book.getTitle()); + assertEquals("Gang of Four", book.getAuthor()); + assertEquals(395, book.getPages()); + } +} From 4ed5ebcf4dd0078d0530a32991ec4e355308e727 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sat, 20 Dec 2025 18:22:28 +0100 Subject: [PATCH 60/63] Adding generated builders to git for showing them and improving documentation by linking to it --- README.md | 42 +- .../builders/example/BookDtoBuilder.java | 443 ++++++++++++++++++ .../example/MannschaftDtoBuilder.java | 226 +++++++++ .../builders/example/PersonDtoBuilder.java | 348 ++++++++++++++ .../example/ProductRecordBuilder.java | 259 ++++++++++ .../builders/example/SponsorDtoBuilder.java | 170 +++++++ 6 files changed, 1484 insertions(+), 4 deletions(-) create mode 100644 example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java create mode 100644 example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/MannschaftDtoBuilder.java create mode 100644 example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/PersonDtoBuilder.java create mode 100644 example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/ProductRecordBuilder.java create mode 100644 example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SponsorDtoBuilder.java diff --git a/README.md b/README.md index eb471670..ed8162cf 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,11 @@ - [Collections and Nested Objects](#collections-and-nested-objects) - [With Interface Pattern](#with-interface-pattern) - [Builder Configuration](#builder-configuration) + - [Compiler Arguments](#compiler-arguments) +- [Examples](#examples) + - [Elementary Builder Example](#elementary-builder-example) + - [Full-Featured Examples](#full-featured-examples) + - [Advanced Features](#advanced-features) - [Contributing](#contributing) - [License](#license) - [Acknowledgements](#acknowledgements) @@ -50,7 +55,7 @@ For Maven-based projects, add the following to your POM file in order to use Sim ```xml ... - 0.1.0 + 0.2.0 ... @@ -93,7 +98,7 @@ If you don't work with a dependency management tool, you can obtain a distributi Annotate your class with `@SimpleBuilder` to generate a builder: ```java -import org.javahelpers.simple.builders.core.annotation.SimpleBuilder; +import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; @SimpleBuilder public class Person { @@ -258,7 +263,7 @@ Person youngerPerson = person.with(p -> p.age(29)); The `With` interface provides type-safe setter methods that mirror the builder's API, making it easy to create object variations without manually copying all fields. -## Builder Configuration +### Builder Configuration Simple Builders provides extensive configuration options to customize the generated builder code. You can control: @@ -270,7 +275,7 @@ Simple Builders provides extensive configuration options to customize the genera Configuration can be applied per-class using `@SimpleBuilder.Options` annotation or project-wide using compiler options. -### Compiler Arguments +#### Compiler Arguments All configuration options are available as compiler arguments using the `-A` flag. For example: @@ -299,6 +304,35 @@ Or in Maven: 📖 **For complete documentation, examples, and all available options, see the [Configuration Guide](docs/CONFIGURATION.md).** +## Examples + +The `example` module contains real-world examples demonstrating various builder configurations and features. You can explore the source DTOs and their generated builders: + +### Elementary Builder Example + +A comprehensive example showcasing all fundamental Java property types with a minimal, setter-only builder configuration: + +- **Source DTO**: [`BookDto.java`](example/src/main/java/org/javahelpers/simple/builders/example/BookDto.java) - Demonstrates all primitive types, collections, Optional, BigDecimal, date/time types, and nested objects +- **Custom Annotation**: [`@ElementaryBuilder`](example/src/main/java/org/javahelpers/simple/builders/example/ElementaryBuilder.java) - A template annotation that disables all advanced features (suppliers, consumers, collection builders, With interface, @Generated annotation) +- **Generated Builder**: [`BookDtoBuilder.java`](example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java) - Clean, minimal builder with only setter methods +- **Tests**: [`BookDtoBuilderTest.java`](example/src/test/java/org/javahelpers/simple/builders/example/BookDtoBuilderTest.java) - Usage examples + +### Full-Featured Examples + +Examples with all builder features enabled: + +- **Person DTO**: [`PersonDto.java`](example/src/main/java/org/javahelpers/simple/builders/example/PersonDto.java) and [`PersonDtoBuilder.java`](example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/PersonDtoBuilder.java) - Demonstrates nested objects, collections, and various setter patterns +- **Product Record**: [`ProductRecord.java`](example/src/main/java/org/javahelpers/simple/builders/example/ProductRecord.java) and [`ProductRecordBuilder.java`](example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/ProductRecordBuilder.java) - Java Record support with full builder features + +### Advanced Features + +Examples demonstrating special annotations and nested object relationships: + +- **Sponsor DTO**: [`SponsorDto.java`](example/src/main/java/org/javahelpers/simple/builders/example/SponsorDto.java) and [`SponsorDtoBuilder.java`](example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SponsorDtoBuilder.java) - Simple DTO used as nested object in other examples +- **Mannschaft DTO**: [`MannschaftDto.java`](example/src/main/java/org/javahelpers/simple/builders/example/MannschaftDto.java) and [`MannschaftDtoBuilder.java`](example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/MannschaftDtoBuilder.java) - Demonstrates `@IgnoreInBuilder` annotation to exclude specific setter methods from the generated builder, plus Set collections with nested objects + +These examples serve as both documentation and integration tests for the annotation processor. + ## Contributing Contributions are welcome! Please see [CONTRIBUTING.md](docs/CONTRIBUTING.md) for: diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java new file mode 100644 index 00000000..719eb307 --- /dev/null +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java @@ -0,0 +1,443 @@ +package org.javahelpers.simple.builders.example; + +import static org.javahelpers.simple.builders.core.util.TrackedValue.changedValue; +import static org.javahelpers.simple.builders.core.util.TrackedValue.initialValue; +import static org.javahelpers.simple.builders.core.util.TrackedValue.unsetValue; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import org.javahelpers.simple.builders.core.annotations.BuilderImplementation; +import org.javahelpers.simple.builders.core.interfaces.IBuilderBase; +import org.javahelpers.simple.builders.core.util.TrackedValue; + +/** + * Builder for {@code org.javahelpers.simple.builders.example.BookDto}. + */ +@BuilderImplementation( + forClass = BookDto.class +) +public class BookDtoBuilder implements IBuilderBase { + /** + * Tracked value for title: the book title to set. + */ + private TrackedValue title = unsetValue(); + + /** + * Tracked value for author: the book author to set. + */ + private TrackedValue author = unsetValue(); + + /** + * Tracked value for isbn: the ISBN to set. + */ + private TrackedValue isbn = unsetValue(); + + /** + * Tracked value for pages: the page count to set. + */ + private TrackedValue pages = unsetValue(); + + /** + * Tracked value for price: the book price to set. + */ + private TrackedValue price = unsetValue(); + + /** + * Tracked value for exactPrice: the exact book price to set. + */ + private TrackedValue exactPrice = unsetValue(); + + /** + * Tracked value for available: true if available, false otherwise. + */ + private TrackedValue available = unsetValue(); + + /** + * Tracked value for rating: the book rating to set. + */ + private TrackedValue rating = unsetValue(); + + /** + * Tracked value for edition: the edition number to set. + */ + private TrackedValue edition = unsetValue(); + + /** + * Tracked value for salesCount: the sales count to set. + */ + private TrackedValue salesCount = unsetValue(); + + /** + * Tracked value for discount: the discount percentage to set. + */ + private TrackedValue discount = unsetValue(); + + /** + * Tracked value for category: the category code to set. + */ + private TrackedValue category = unsetValue(); + + /** + * Tracked value for publishDate: the publication date to set. + */ + private TrackedValue publishDate = unsetValue(); + + /** + * Tracked value for lastUpdated: the last update timestamp to set. + */ + private TrackedValue lastUpdated = unsetValue(); + + /** + * Tracked value for subtitle: an Optional containing the subtitle to set. + */ + private TrackedValue> subtitle = unsetValue(); + + /** + * Tracked value for tags: the list of tags to set. + */ + private TrackedValue> tags = unsetValue(); + + /** + * Tracked value for genres: the set of genres to set. + */ + private TrackedValue> genres = unsetValue(); + + /** + * Tracked value for metadata: the metadata map to set. + */ + private TrackedValue> metadata = unsetValue(); + + /** + * Tracked value for publisher: the publisher to set. + */ + private TrackedValue publisher = unsetValue(); + + /** + * Initialisation of builder for {@code org.javahelpers.simple.builders.example.BookDto} by a instance. + * + * @param instance object instance for initialisiation + */ + public BookDtoBuilder(BookDto instance) { + this.title = initialValue(instance.getTitle()); + this.author = initialValue(instance.getAuthor()); + this.isbn = initialValue(instance.getIsbn()); + this.pages = initialValue(instance.getPages()); + if (this.pages.value() == null) { + throw new IllegalArgumentException("Cannot initialize builder from instance: field 'pages' is marked as non-null but source object has null value"); + } + this.price = initialValue(instance.getPrice()); + if (this.price.value() == null) { + throw new IllegalArgumentException("Cannot initialize builder from instance: field 'price' is marked as non-null but source object has null value"); + } + this.exactPrice = initialValue(instance.getExactPrice()); + this.available = initialValue(instance.isAvailable()); + if (this.available.value() == null) { + throw new IllegalArgumentException("Cannot initialize builder from instance: field 'available' is marked as non-null but source object has null value"); + } + this.rating = initialValue(instance.getRating()); + if (this.rating.value() == null) { + throw new IllegalArgumentException("Cannot initialize builder from instance: field 'rating' is marked as non-null but source object has null value"); + } + this.edition = initialValue(instance.getEdition()); + if (this.edition.value() == null) { + throw new IllegalArgumentException("Cannot initialize builder from instance: field 'edition' is marked as non-null but source object has null value"); + } + this.salesCount = initialValue(instance.getSalesCount()); + if (this.salesCount.value() == null) { + throw new IllegalArgumentException("Cannot initialize builder from instance: field 'salesCount' is marked as non-null but source object has null value"); + } + this.discount = initialValue(instance.getDiscount()); + if (this.discount.value() == null) { + throw new IllegalArgumentException("Cannot initialize builder from instance: field 'discount' is marked as non-null but source object has null value"); + } + this.category = initialValue(instance.getCategory()); + if (this.category.value() == null) { + throw new IllegalArgumentException("Cannot initialize builder from instance: field 'category' is marked as non-null but source object has null value"); + } + this.publishDate = initialValue(instance.getPublishDate()); + this.lastUpdated = initialValue(instance.getLastUpdated()); + this.subtitle = initialValue(instance.getSubtitle()); + this.tags = initialValue(instance.getTags()); + this.genres = initialValue(instance.getGenres()); + this.metadata = initialValue(instance.getMetadata()); + this.publisher = initialValue(instance.getPublisher()); + } + + /** + * Empty constructor of builder for {@code org.javahelpers.simple.builders.example.BookDto}. + */ + public BookDtoBuilder() { + } + + /** + * Sets the value for lastUpdated. + * + * @param lastUpdated the last update timestamp to set + * @return current instance of builder + */ + public BookDtoBuilder lastUpdated(LocalDateTime lastUpdated) { + this.lastUpdated = changedValue(lastUpdated); + return this; + } + + /** + * Sets the value for title. + * + * @param title the book title to set + * @return current instance of builder + */ + public BookDtoBuilder title(String title) { + this.title = changedValue(title); + return this; + } + + /** + * Sets the value for subtitle. + * + * @param subtitle an Optional containing the subtitle to set + * @return current instance of builder + */ + public BookDtoBuilder subtitle(Optional subtitle) { + this.subtitle = changedValue(subtitle); + return this; + } + + /** + * Sets the value for publisher. + * + * @param publisher the publisher to set + * @return current instance of builder + */ + public BookDtoBuilder publisher(PersonDto publisher) { + this.publisher = changedValue(publisher); + return this; + } + + /** + * Sets the value for available. + * + * @param available true if available, false otherwise + * @return current instance of builder + */ + public BookDtoBuilder available(boolean available) { + this.available = changedValue(available); + return this; + } + + /** + * Sets the value for discount. + * + * @param discount the discount percentage to set + * @return current instance of builder + */ + public BookDtoBuilder discount(float discount) { + this.discount = changedValue(discount); + return this; + } + + /** + * Sets the value for salesCount. + * + * @param salesCount the sales count to set + * @return current instance of builder + */ + public BookDtoBuilder salesCount(long salesCount) { + this.salesCount = changedValue(salesCount); + return this; + } + + /** + * Sets the value for rating. + * + * @param rating the book rating to set + * @return current instance of builder + */ + public BookDtoBuilder rating(byte rating) { + this.rating = changedValue(rating); + return this; + } + + /** + * Sets the value for edition. + * + * @param edition the edition number to set + * @return current instance of builder + */ + public BookDtoBuilder edition(short edition) { + this.edition = changedValue(edition); + return this; + } + + /** + * Sets the value for genres. + * + * @param genres the set of genres to set + * @return current instance of builder + */ + public BookDtoBuilder genres(Set genres) { + this.genres = changedValue(genres); + return this; + } + + /** + * Sets the value for metadata. + * + * @param metadata the metadata map to set + * @return current instance of builder + */ + public BookDtoBuilder metadata(Map metadata) { + this.metadata = changedValue(metadata); + return this; + } + + /** + * Sets the value for price. + * + * @param price the book price to set + * @return current instance of builder + */ + public BookDtoBuilder price(double price) { + this.price = changedValue(price); + return this; + } + + /** + * Sets the value for tags. + * + * @param tags the list of tags to set + * @return current instance of builder + */ + public BookDtoBuilder tags(List tags) { + this.tags = changedValue(tags); + return this; + } + + /** + * Sets the value for category. + * + * @param category the category code to set + * @return current instance of builder + */ + public BookDtoBuilder category(char category) { + this.category = changedValue(category); + return this; + } + + /** + * Sets the value for author. + * + * @param author the book author to set + * @return current instance of builder + */ + public BookDtoBuilder author(String author) { + this.author = changedValue(author); + return this; + } + + /** + * Sets the value for publishDate. + * + * @param publishDate the publication date to set + * @return current instance of builder + */ + public BookDtoBuilder publishDate(LocalDate publishDate) { + this.publishDate = changedValue(publishDate); + return this; + } + + /** + * Sets the value for exactPrice. + * + * @param exactPrice the exact book price to set + * @return current instance of builder + */ + public BookDtoBuilder exactPrice(BigDecimal exactPrice) { + this.exactPrice = changedValue(exactPrice); + return this; + } + + /** + * Sets the value for pages. + * + * @param pages the page count to set + * @return current instance of builder + */ + public BookDtoBuilder pages(int pages) { + this.pages = changedValue(pages); + return this; + } + + /** + * Sets the value for isbn. + * + * @param isbn the ISBN to set + * @return current instance of builder + */ + public BookDtoBuilder isbn(String isbn) { + this.isbn = changedValue(isbn); + return this; + } + + @Override + public BookDto build() { + if (this.pages.isSet() && this.pages.value() == null) { + throw new IllegalStateException("Field 'pages' is marked as non-null but null value was provided"); + } + if (this.price.isSet() && this.price.value() == null) { + throw new IllegalStateException("Field 'price' is marked as non-null but null value was provided"); + } + if (this.available.isSet() && this.available.value() == null) { + throw new IllegalStateException("Field 'available' is marked as non-null but null value was provided"); + } + if (this.rating.isSet() && this.rating.value() == null) { + throw new IllegalStateException("Field 'rating' is marked as non-null but null value was provided"); + } + if (this.edition.isSet() && this.edition.value() == null) { + throw new IllegalStateException("Field 'edition' is marked as non-null but null value was provided"); + } + if (this.salesCount.isSet() && this.salesCount.value() == null) { + throw new IllegalStateException("Field 'salesCount' is marked as non-null but null value was provided"); + } + if (this.discount.isSet() && this.discount.value() == null) { + throw new IllegalStateException("Field 'discount' is marked as non-null but null value was provided"); + } + if (this.category.isSet() && this.category.value() == null) { + throw new IllegalStateException("Field 'category' is marked as non-null but null value was provided"); + } + BookDto result = new BookDto(); + this.title.ifSet(result::setTitle); + this.author.ifSet(result::setAuthor); + this.isbn.ifSet(result::setIsbn); + this.pages.ifSet(result::setPages); + this.price.ifSet(result::setPrice); + this.exactPrice.ifSet(result::setExactPrice); + this.available.ifSet(result::setAvailable); + this.rating.ifSet(result::setRating); + this.edition.ifSet(result::setEdition); + this.salesCount.ifSet(result::setSalesCount); + this.discount.ifSet(result::setDiscount); + this.category.ifSet(result::setCategory); + this.publishDate.ifSet(result::setPublishDate); + this.lastUpdated.ifSet(result::setLastUpdated); + this.subtitle.ifSet(result::setSubtitle); + this.tags.ifSet(result::setTags); + this.genres.ifSet(result::setGenres); + this.metadata.ifSet(result::setMetadata); + this.publisher.ifSet(result::setPublisher); + return result; + } + + /** + * Creating a new builder for {@code org.javahelpers.simple.builders.example.BookDto}. + * + * @return builder for {@code org.javahelpers.simple.builders.example.BookDto} + */ + public static BookDtoBuilder create() { + return new BookDtoBuilder(); + } +} diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/MannschaftDtoBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/MannschaftDtoBuilder.java new file mode 100644 index 00000000..917b6247 --- /dev/null +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/MannschaftDtoBuilder.java @@ -0,0 +1,226 @@ +package org.javahelpers.simple.builders.example; + +import static org.javahelpers.simple.builders.core.util.TrackedValue.changedValue; +import static org.javahelpers.simple.builders.core.util.TrackedValue.initialValue; +import static org.javahelpers.simple.builders.core.util.TrackedValue.unsetValue; + +import java.util.Set; +import java.util.function.BooleanSupplier; +import java.util.function.Consumer; +import java.util.function.Supplier; +import javax.annotation.processing.Generated; +import org.javahelpers.simple.builders.core.annotations.BuilderImplementation; +import org.javahelpers.simple.builders.core.builders.HashSetBuilderWithElementBuilders; +import org.javahelpers.simple.builders.core.interfaces.IBuilderBase; +import org.javahelpers.simple.builders.core.util.TrackedValue; + +/** + * Builder for {@code org.javahelpers.simple.builders.example.MannschaftDto}. + */ +@Generated("Generated by org.javahelpers.simple.builders.processor.BuilderProcessor") +@BuilderImplementation( + forClass = MannschaftDto.class +) +public class MannschaftDtoBuilder implements IBuilderBase { + /** + * Tracked value for name: name. + */ + private TrackedValue name = unsetValue(); + + /** + * Tracked value for sponsoren: sponsoren. + */ + private TrackedValue> sponsoren = unsetValue(); + + /** + * Initialisation of builder for {@code org.javahelpers.simple.builders.example.MannschaftDto} by a instance. + * + * @param instance object instance for initialisiation + */ + public MannschaftDtoBuilder(MannschaftDto instance) { + this.name = initialValue(instance.getName()); + this.sponsoren = initialValue(instance.getSponsoren()); + } + + /** + * Empty constructor of builder for {@code org.javahelpers.simple.builders.example.MannschaftDto}. + */ + public MannschaftDtoBuilder() { + } + + /** + * Sets the value for sponsoren. + * + * @param sponsoren sponsoren + * @return current instance of builder + */ + public MannschaftDtoBuilder sponsoren(SponsorDto... sponsoren) { + this.sponsoren = changedValue(Set.of(sponsoren)); + return this; + } + + /** + * Sets the value for name by invoking the provided supplier. + * + * @param nameSupplier supplier for name + * @return current instance of builder + */ + public MannschaftDtoBuilder name(Supplier nameSupplier) { + this.name = changedValue(nameSupplier.get()); + return this; + } + + /** + * Sets the value for name. + * + * @param format name + * @param args name + * @return current instance of builder + */ + public MannschaftDtoBuilder name(String format, Object... args) { + this.name = changedValue(String.format(format, args)); + return this; + } + + /** + * Sets the value for sponsoren by invoking the provided supplier. + * + * @param sponsorenSupplier supplier for sponsoren + * @return current instance of builder + */ + public MannschaftDtoBuilder sponsoren(Supplier> sponsorenSupplier) { + this.sponsoren = changedValue(sponsorenSupplier.get()); + return this; + } + + /** + * Sets the value for name by executing the provided consumer. + * + * @param nameStringBuilderConsumer consumer providing an instance of name + * @return current instance of builder + */ + public MannschaftDtoBuilder name(Consumer nameStringBuilderConsumer) { + StringBuilder builder = new StringBuilder(); + nameStringBuilderConsumer.accept(builder); + this.name = changedValue(builder.toString()); + return this; + } + + /** + * Sets the value for name. + * + * @param name name + * @return current instance of builder + */ + public MannschaftDtoBuilder name(String name) { + this.name = changedValue(name); + return this; + } + + /** + * Sets the value for sponsoren using a builder consumer that produces the value. + * + * @param sponsorenBuilderConsumer consumer providing an instance of a builder for sponsoren + * @return current instance of builder + */ + public MannschaftDtoBuilder sponsoren( + Consumer> sponsorenBuilderConsumer) { + HashSetBuilderWithElementBuilders builder = this.sponsoren.isSet() ? new HashSetBuilderWithElementBuilders(this.sponsoren.value(), SponsorDtoBuilder::create) : new HashSetBuilderWithElementBuilders(SponsorDtoBuilder::create); + sponsorenBuilderConsumer.accept(builder); + this.sponsoren = changedValue(builder.build()); + return this; + } + + /** + * Sets the value for sponsoren. + * + * @param sponsoren sponsoren + * @return current instance of builder + */ + public MannschaftDtoBuilder sponsoren(Set sponsoren) { + this.sponsoren = changedValue(sponsoren); + return this; + } + + @Override + public MannschaftDto build() { + MannschaftDto result = new MannschaftDto(); + this.name.ifSet(result::setName); + this.sponsoren.ifSet(result::setSponsoren); + return result; + } + + /** + * Creating a new builder for {@code org.javahelpers.simple.builders.example.MannschaftDto}. + * + * @return builder for {@code org.javahelpers.simple.builders.example.MannschaftDto} + */ + public static MannschaftDtoBuilder create() { + return new MannschaftDtoBuilder(); + } + + /** + * Conditionally applies builder modifications based on a condition evaluation. + * + * @param condition the condition to evaluate + * @param trueCase the consumer to apply if condition is true + * @param falseCase the consumer to apply if condition is false (can be null) + * @return this builder instance + */ + public MannschaftDtoBuilder conditional(BooleanSupplier condition, + Consumer trueCase, Consumer falseCase) { + if (condition.getAsBoolean()) { + trueCase.accept(this); + } else if (falseCase != null) { + falseCase.accept(this); + } + return this; + } + + /** + * Conditionally applies builder modifications if the condition is true. + * + * @param condition the condition to evaluate + * @param yesCondition the consumer to apply if condition is true + * @return this builder instance + */ + public MannschaftDtoBuilder conditional(BooleanSupplier condition, + Consumer yesCondition) { + return conditional(condition, yesCondition, null); + } + + /** + * Interface that can be implemented by the DTO to provide fluent modification methods. + */ + public interface With { + /** + * Applies modifications to a builder initialized from this instance and returns the built object. + * + * @param b the consumer to apply modifications + * @return the modified instance + */ + default MannschaftDto with(Consumer b) { + MannschaftDtoBuilder builder; + try { + builder = new MannschaftDtoBuilder(MannschaftDto.class.cast(this)); + } catch (ClassCastException ex) { + throw new IllegalArgumentException("The interface 'MannschaftDtoBuilder.With' should only be implemented by classes, which could be casted to 'MannschaftDto'", ex); + } + b.accept(builder); + return builder.build(); + } + + /** + * Creates a builder initialized from this instance. + * + * @return a builder initialized with this instance's values + */ + default MannschaftDtoBuilder with() { + try { + return new MannschaftDtoBuilder(MannschaftDto.class.cast(this)); + } catch (ClassCastException ex) { + throw new IllegalArgumentException("The interface 'MannschaftDtoBuilder.With' should only be implemented by classes, which could be casted to 'MannschaftDto'", ex); + } + } + } +} diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/PersonDtoBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/PersonDtoBuilder.java new file mode 100644 index 00000000..92855b25 --- /dev/null +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/PersonDtoBuilder.java @@ -0,0 +1,348 @@ +package org.javahelpers.simple.builders.example; + +import static org.javahelpers.simple.builders.core.util.TrackedValue.changedValue; +import static org.javahelpers.simple.builders.core.util.TrackedValue.initialValue; +import static org.javahelpers.simple.builders.core.util.TrackedValue.unsetValue; + +import java.time.LocalDate; +import java.util.List; +import java.util.function.BooleanSupplier; +import java.util.function.Consumer; +import java.util.function.Supplier; +import javax.annotation.processing.Generated; +import org.javahelpers.simple.builders.core.annotations.BuilderImplementation; +import org.javahelpers.simple.builders.core.builders.ArrayListBuilder; +import org.javahelpers.simple.builders.core.interfaces.IBuilderBase; +import org.javahelpers.simple.builders.core.util.TrackedValue; + +/** + * Builder for {@code org.javahelpers.simple.builders.example.PersonDto}. + */ +@Generated("Generated by org.javahelpers.simple.builders.processor.BuilderProcessor") +@BuilderImplementation( + forClass = PersonDto.class +) +public class PersonDtoBuilder implements IBuilderBase { + /** + * Tracked value for name: name. + */ + private TrackedValue name = unsetValue(); + + /** + * Tracked value for nickNames: nickNames. + */ + private TrackedValue> nickNames = unsetValue(); + + /** + * Tracked value for nickNames2: nickNames2. + */ + private TrackedValue nickNames2 = unsetValue(); + + /** + * Tracked value for birthdate: birthdate. + */ + private TrackedValue birthdate = unsetValue(); + + /** + * Tracked value for mannschaft: mannschaft. + */ + private TrackedValue mannschaft = unsetValue(); + + /** + * Initialisation of builder for {@code org.javahelpers.simple.builders.example.PersonDto} by a instance. + * + * @param instance object instance for initialisiation + */ + public PersonDtoBuilder(PersonDto instance) { + this.name = initialValue(instance.getName()); + this.nickNames = initialValue(instance.getNickNames()); + this.birthdate = initialValue(instance.getBirthdate()); + this.mannschaft = initialValue(instance.getMannschaft()); + } + + /** + * Empty constructor of builder for {@code org.javahelpers.simple.builders.example.PersonDto}. + */ + public PersonDtoBuilder() { + } + + /** + * Sets the value for nickNames2. + * + * @param nickNames2 nickNames2 + * @return current instance of builder + */ + public PersonDtoBuilder nickNames2(String... nickNames2) { + this.nickNames2 = changedValue(nickNames2); + return this; + } + + /** + * Sets the value for nickNames2 by invoking the provided supplier. + * + * @param nickNames2Supplier supplier for nickNames2 + * @return current instance of builder + */ + public PersonDtoBuilder nickNames2(Supplier nickNames2Supplier) { + this.nickNames2 = changedValue(nickNames2Supplier.get()); + return this; + } + + /** + * Sets the value for birthdate by invoking the provided supplier. + * + * @param birthdateSupplier supplier for birthdate + * @return current instance of builder + */ + public PersonDtoBuilder birthdate(Supplier birthdateSupplier) { + this.birthdate = changedValue(birthdateSupplier.get()); + return this; + } + + /** + * Sets the value for nickNames using a builder consumer that produces the value. + * + * @param nickNamesBuilderConsumer consumer providing an instance of a builder for nickNames + * @return current instance of builder + */ + public PersonDtoBuilder nickNames(Consumer> nickNamesBuilderConsumer) { + ArrayListBuilder builder = this.nickNames.isSet() ? new ArrayListBuilder(this.nickNames.value()) : new ArrayListBuilder(); + nickNamesBuilderConsumer.accept(builder); + this.nickNames = changedValue(builder.build()); + return this; + } + + /** + * Sets the value for name. + * + * @param format name + * @param args name + * @return current instance of builder + */ + public PersonDtoBuilder name(String format, Object... args) { + this.name = changedValue(String.format(format, args)); + return this; + } + + /** + * Sets the value for name by executing the provided consumer. + * + * @param nameStringBuilderConsumer consumer providing an instance of name + * @return current instance of builder + */ + public PersonDtoBuilder name(Consumer nameStringBuilderConsumer) { + StringBuilder builder = new StringBuilder(); + nameStringBuilderConsumer.accept(builder); + this.name = changedValue(builder.toString()); + return this; + } + + /** + * Sets the value for mannschaft. + * + * @param mannschaft mannschaft + * @return current instance of builder + */ + public PersonDtoBuilder mannschaft(MannschaftDto mannschaft) { + this.mannschaft = changedValue(mannschaft); + return this; + } + + /** + * Sets the value for mannschaft using a builder consumer that produces the value. + * + * @param mannschaftBuilderConsumer consumer providing an instance of a builder for mannschaft + * @return current instance of builder + */ + public PersonDtoBuilder mannschaft(Consumer mannschaftBuilderConsumer) { + MannschaftDtoBuilder builder = this.mannschaft.isSet() ? new MannschaftDtoBuilder(this.mannschaft.value()) : new MannschaftDtoBuilder(); + mannschaftBuilderConsumer.accept(builder); + this.mannschaft = changedValue(builder.build()); + return this; + } + + /** + * Sets the value for nickNames2. + * + * @param nickNames2 nickNames2 + * @return current instance of builder + */ + public PersonDtoBuilder nickNames2(List nickNames2) { + this.nickNames2 = changedValue(nickNames2.toArray(new String[0])); + return this; + } + + /** + * Sets the value for mannschaft by invoking the provided supplier. + * + * @param mannschaftSupplier supplier for mannschaft + * @return current instance of builder + */ + public PersonDtoBuilder mannschaft(Supplier mannschaftSupplier) { + this.mannschaft = changedValue(mannschaftSupplier.get()); + return this; + } + + /** + * Sets the value for nickNames. + * + * @param nickNames nickNames + * @return current instance of builder + */ + public PersonDtoBuilder nickNames(String... nickNames) { + this.nickNames = changedValue(List.of(nickNames)); + return this; + } + + /** + * Sets the value for birthdate. + * + * @param birthdate birthdate + * @return current instance of builder + */ + public PersonDtoBuilder birthdate(LocalDate birthdate) { + this.birthdate = changedValue(birthdate); + return this; + } + + /** + * Sets the value for name by invoking the provided supplier. + * + * @param nameSupplier supplier for name + * @return current instance of builder + */ + public PersonDtoBuilder name(Supplier nameSupplier) { + this.name = changedValue(nameSupplier.get()); + return this; + } + + /** + * Sets the value for nickNames by invoking the provided supplier. + * + * @param nickNamesSupplier supplier for nickNames + * @return current instance of builder + */ + public PersonDtoBuilder nickNames(Supplier> nickNamesSupplier) { + this.nickNames = changedValue(nickNamesSupplier.get()); + return this; + } + + /** + * Sets the value for name. + * + * @param name name + * @return current instance of builder + */ + public PersonDtoBuilder name(String name) { + this.name = changedValue(name); + return this; + } + + /** + * Sets the value for nickNames. + * + * @param nickNames nickNames + * @return current instance of builder + */ + public PersonDtoBuilder nickNames(List nickNames) { + this.nickNames = changedValue(nickNames); + return this; + } + + /** + * Sets the value for nickNames2 using the fluent builder consumer. + * + * @param nickNames2BuilderConsumer consumer for nickNames2 + * @return current instance of builder + */ + public PersonDtoBuilder nickNames2(Consumer> nickNames2BuilderConsumer) { + ArrayListBuilder builder = this.nickNames2.isSet() ? new ArrayListBuilder(java.util.List.of(this.nickNames2.value())) : new ArrayListBuilder(); + nickNames2BuilderConsumer.accept(builder); + this.nickNames2 = changedValue(builder.build().toArray(new String[0])); + return this; + } + + @Override + public PersonDto build() { + PersonDto result = new PersonDto(this.name.value()); + this.nickNames.ifSet(result::setNickNames); + this.nickNames2.ifSet(result::setNickNames2); + this.birthdate.ifSet(result::setBirthdate); + this.mannschaft.ifSet(result::setMannschaft); + return result; + } + + /** + * Creating a new builder for {@code org.javahelpers.simple.builders.example.PersonDto}. + * + * @return builder for {@code org.javahelpers.simple.builders.example.PersonDto} + */ + public static PersonDtoBuilder create() { + return new PersonDtoBuilder(); + } + + /** + * Conditionally applies builder modifications based on a condition evaluation. + * + * @param condition the condition to evaluate + * @param trueCase the consumer to apply if condition is true + * @param falseCase the consumer to apply if condition is false (can be null) + * @return this builder instance + */ + public PersonDtoBuilder conditional(BooleanSupplier condition, + Consumer trueCase, Consumer falseCase) { + if (condition.getAsBoolean()) { + trueCase.accept(this); + } else if (falseCase != null) { + falseCase.accept(this); + } + return this; + } + + /** + * Conditionally applies builder modifications if the condition is true. + * + * @param condition the condition to evaluate + * @param yesCondition the consumer to apply if condition is true + * @return this builder instance + */ + public PersonDtoBuilder conditional(BooleanSupplier condition, + Consumer yesCondition) { + return conditional(condition, yesCondition, null); + } + + /** + * Interface that can be implemented by the DTO to provide fluent modification methods. + */ + public interface With { + /** + * Applies modifications to a builder initialized from this instance and returns the built object. + * + * @param b the consumer to apply modifications + * @return the modified instance + */ + default PersonDto with(Consumer b) { + PersonDtoBuilder builder; + try { + builder = new PersonDtoBuilder(PersonDto.class.cast(this)); + } catch (ClassCastException ex) { + throw new IllegalArgumentException("The interface 'PersonDtoBuilder.With' should only be implemented by classes, which could be casted to 'PersonDto'", ex); + } + b.accept(builder); + return builder.build(); + } + + /** + * Creates a builder initialized from this instance. + * + * @return a builder initialized with this instance's values + */ + default PersonDtoBuilder with() { + try { + return new PersonDtoBuilder(PersonDto.class.cast(this)); + } catch (ClassCastException ex) { + throw new IllegalArgumentException("The interface 'PersonDtoBuilder.With' should only be implemented by classes, which could be casted to 'PersonDto'", ex); + } + } + } +} diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/ProductRecordBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/ProductRecordBuilder.java new file mode 100644 index 00000000..bfe00d16 --- /dev/null +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/ProductRecordBuilder.java @@ -0,0 +1,259 @@ +package org.javahelpers.simple.builders.example; + +import static org.javahelpers.simple.builders.core.util.TrackedValue.changedValue; +import static org.javahelpers.simple.builders.core.util.TrackedValue.initialValue; +import static org.javahelpers.simple.builders.core.util.TrackedValue.unsetValue; + +import java.util.function.BooleanSupplier; +import java.util.function.Consumer; +import java.util.function.Supplier; +import javax.annotation.processing.Generated; +import org.javahelpers.simple.builders.core.annotations.BuilderImplementation; +import org.javahelpers.simple.builders.core.interfaces.IBuilderBase; +import org.javahelpers.simple.builders.core.util.TrackedValue; + +/** + * Builder for {@code org.javahelpers.simple.builders.example.ProductRecord}. + */ +@Generated("Generated by org.javahelpers.simple.builders.processor.BuilderProcessor") +@BuilderImplementation( + forClass = ProductRecord.class +) +public class ProductRecordBuilder implements IBuilderBase { + /** + * Tracked value for name: name. + */ + private TrackedValue name = unsetValue(); + + /** + * Tracked value for price: price. + */ + private TrackedValue price = unsetValue(); + + /** + * Tracked value for category: category. + */ + private TrackedValue category = unsetValue(); + + /** + * Initialisation of builder for {@code org.javahelpers.simple.builders.example.ProductRecord} by a instance. + * + * @param instance object instance for initialisiation + */ + public ProductRecordBuilder(ProductRecord instance) { + this.name = initialValue(instance.name()); + this.price = initialValue(instance.price()); + if (this.price.value() == null) { + throw new IllegalArgumentException("Cannot initialize builder from instance: field 'price' is marked as non-null but source object has null value"); + } + this.category = initialValue(instance.category()); + } + + /** + * Empty constructor of builder for {@code org.javahelpers.simple.builders.example.ProductRecord}. + */ + public ProductRecordBuilder() { + } + + /** + * Sets the value for category. + * + * @param format category + * @param args category + * @return current instance of builder + */ + public ProductRecordBuilder category(String format, Object... args) { + this.category = changedValue(String.format(format, args)); + return this; + } + + /** + * Sets the value for price. + * + * @param price price + * @return current instance of builder + */ + public ProductRecordBuilder price(double price) { + this.price = changedValue(price); + return this; + } + + /** + * Sets the value for price by invoking the provided supplier. + * + * @param priceSupplier supplier for price + * @return current instance of builder + */ + public ProductRecordBuilder price(Supplier priceSupplier) { + this.price = changedValue(priceSupplier.get()); + return this; + } + + /** + * Sets the value for name by invoking the provided supplier. + * + * @param nameSupplier supplier for name + * @return current instance of builder + */ + public ProductRecordBuilder name(Supplier nameSupplier) { + this.name = changedValue(nameSupplier.get()); + return this; + } + + /** + * Sets the value for name. + * + * @param format name + * @param args name + * @return current instance of builder + */ + public ProductRecordBuilder name(String format, Object... args) { + this.name = changedValue(String.format(format, args)); + return this; + } + + /** + * Sets the value for category. + * + * @param category category + * @return current instance of builder + */ + public ProductRecordBuilder category(String category) { + this.category = changedValue(category); + return this; + } + + /** + * Sets the value for name by executing the provided consumer. + * + * @param nameStringBuilderConsumer consumer providing an instance of name + * @return current instance of builder + */ + public ProductRecordBuilder name(Consumer nameStringBuilderConsumer) { + StringBuilder builder = new StringBuilder(); + nameStringBuilderConsumer.accept(builder); + this.name = changedValue(builder.toString()); + return this; + } + + /** + * Sets the value for name. + * + * @param name name + * @return current instance of builder + */ + public ProductRecordBuilder name(String name) { + this.name = changedValue(name); + return this; + } + + /** + * Sets the value for category by invoking the provided supplier. + * + * @param categorySupplier supplier for category + * @return current instance of builder + */ + public ProductRecordBuilder category(Supplier categorySupplier) { + this.category = changedValue(categorySupplier.get()); + return this; + } + + /** + * Sets the value for category by executing the provided consumer. + * + * @param categoryStringBuilderConsumer consumer providing an instance of category + * @return current instance of builder + */ + public ProductRecordBuilder category(Consumer categoryStringBuilderConsumer) { + StringBuilder builder = new StringBuilder(); + categoryStringBuilderConsumer.accept(builder); + this.category = changedValue(builder.toString()); + return this; + } + + @Override + public ProductRecord build() { + if (!this.price.isSet()) { + throw new IllegalStateException("Required field 'price' must be set before calling build()"); + } + if (this.price.value() == null) { + throw new IllegalStateException("Field 'price' is marked as non-null but null value was provided"); + } + ProductRecord result = new ProductRecord(this.name.value(), this.price.value(), this.category.value()); + return result; + } + + /** + * Creating a new builder for {@code org.javahelpers.simple.builders.example.ProductRecord}. + * + * @return builder for {@code org.javahelpers.simple.builders.example.ProductRecord} + */ + public static ProductRecordBuilder create() { + return new ProductRecordBuilder(); + } + + /** + * Conditionally applies builder modifications based on a condition evaluation. + * + * @param condition the condition to evaluate + * @param trueCase the consumer to apply if condition is true + * @param falseCase the consumer to apply if condition is false (can be null) + * @return this builder instance + */ + public ProductRecordBuilder conditional(BooleanSupplier condition, + Consumer trueCase, Consumer falseCase) { + if (condition.getAsBoolean()) { + trueCase.accept(this); + } else if (falseCase != null) { + falseCase.accept(this); + } + return this; + } + + /** + * Conditionally applies builder modifications if the condition is true. + * + * @param condition the condition to evaluate + * @param yesCondition the consumer to apply if condition is true + * @return this builder instance + */ + public ProductRecordBuilder conditional(BooleanSupplier condition, + Consumer yesCondition) { + return conditional(condition, yesCondition, null); + } + + /** + * Interface that can be implemented by the DTO to provide fluent modification methods. + */ + public interface With { + /** + * Applies modifications to a builder initialized from this instance and returns the built object. + * + * @param b the consumer to apply modifications + * @return the modified instance + */ + default ProductRecord with(Consumer b) { + ProductRecordBuilder builder; + try { + builder = new ProductRecordBuilder(ProductRecord.class.cast(this)); + } catch (ClassCastException ex) { + throw new IllegalArgumentException("The interface 'ProductRecordBuilder.With' should only be implemented by classes, which could be casted to 'ProductRecord'", ex); + } + b.accept(builder); + return builder.build(); + } + + /** + * Creates a builder initialized from this instance. + * + * @return a builder initialized with this instance's values + */ + default ProductRecordBuilder with() { + try { + return new ProductRecordBuilder(ProductRecord.class.cast(this)); + } catch (ClassCastException ex) { + throw new IllegalArgumentException("The interface 'ProductRecordBuilder.With' should only be implemented by classes, which could be casted to 'ProductRecord'", ex); + } + } + } +} diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SponsorDtoBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SponsorDtoBuilder.java new file mode 100644 index 00000000..41c66f68 --- /dev/null +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SponsorDtoBuilder.java @@ -0,0 +1,170 @@ +package org.javahelpers.simple.builders.example; + +import static org.javahelpers.simple.builders.core.util.TrackedValue.changedValue; +import static org.javahelpers.simple.builders.core.util.TrackedValue.initialValue; +import static org.javahelpers.simple.builders.core.util.TrackedValue.unsetValue; + +import java.util.function.BooleanSupplier; +import java.util.function.Consumer; +import java.util.function.Supplier; +import javax.annotation.processing.Generated; +import org.javahelpers.simple.builders.core.annotations.BuilderImplementation; +import org.javahelpers.simple.builders.core.interfaces.IBuilderBase; +import org.javahelpers.simple.builders.core.util.TrackedValue; + +/** + * Builder for {@code org.javahelpers.simple.builders.example.SponsorDto}. + */ +@Generated("Generated by org.javahelpers.simple.builders.processor.BuilderProcessor") +@BuilderImplementation( + forClass = SponsorDto.class +) +public class SponsorDtoBuilder implements IBuilderBase { + /** + * Tracked value for name: name. + */ + private TrackedValue name = unsetValue(); + + /** + * Initialisation of builder for {@code org.javahelpers.simple.builders.example.SponsorDto} by a instance. + * + * @param instance object instance for initialisiation + */ + public SponsorDtoBuilder(SponsorDto instance) { + this.name = initialValue(instance.getName()); + } + + /** + * Empty constructor of builder for {@code org.javahelpers.simple.builders.example.SponsorDto}. + */ + public SponsorDtoBuilder() { + } + + /** + * Sets the value for name by invoking the provided supplier. + * + * @param nameSupplier supplier for name + * @return current instance of builder + */ + public SponsorDtoBuilder name(Supplier nameSupplier) { + this.name = changedValue(nameSupplier.get()); + return this; + } + + /** + * Sets the value for name. + * + * @param format name + * @param args name + * @return current instance of builder + */ + public SponsorDtoBuilder name(String format, Object... args) { + this.name = changedValue(String.format(format, args)); + return this; + } + + /** + * Sets the value for name by executing the provided consumer. + * + * @param nameStringBuilderConsumer consumer providing an instance of name + * @return current instance of builder + */ + public SponsorDtoBuilder name(Consumer nameStringBuilderConsumer) { + StringBuilder builder = new StringBuilder(); + nameStringBuilderConsumer.accept(builder); + this.name = changedValue(builder.toString()); + return this; + } + + /** + * Sets the value for name. + * + * @param name name + * @return current instance of builder + */ + public SponsorDtoBuilder name(String name) { + this.name = changedValue(name); + return this; + } + + @Override + public SponsorDto build() { + SponsorDto result = new SponsorDto(); + this.name.ifSet(result::setName); + return result; + } + + /** + * Creating a new builder for {@code org.javahelpers.simple.builders.example.SponsorDto}. + * + * @return builder for {@code org.javahelpers.simple.builders.example.SponsorDto} + */ + public static SponsorDtoBuilder create() { + return new SponsorDtoBuilder(); + } + + /** + * Conditionally applies builder modifications based on a condition evaluation. + * + * @param condition the condition to evaluate + * @param trueCase the consumer to apply if condition is true + * @param falseCase the consumer to apply if condition is false (can be null) + * @return this builder instance + */ + public SponsorDtoBuilder conditional(BooleanSupplier condition, + Consumer trueCase, Consumer falseCase) { + if (condition.getAsBoolean()) { + trueCase.accept(this); + } else if (falseCase != null) { + falseCase.accept(this); + } + return this; + } + + /** + * Conditionally applies builder modifications if the condition is true. + * + * @param condition the condition to evaluate + * @param yesCondition the consumer to apply if condition is true + * @return this builder instance + */ + public SponsorDtoBuilder conditional(BooleanSupplier condition, + Consumer yesCondition) { + return conditional(condition, yesCondition, null); + } + + /** + * Interface that can be implemented by the DTO to provide fluent modification methods. + */ + public interface With { + /** + * Applies modifications to a builder initialized from this instance and returns the built object. + * + * @param b the consumer to apply modifications + * @return the modified instance + */ + default SponsorDto with(Consumer b) { + SponsorDtoBuilder builder; + try { + builder = new SponsorDtoBuilder(SponsorDto.class.cast(this)); + } catch (ClassCastException ex) { + throw new IllegalArgumentException("The interface 'SponsorDtoBuilder.With' should only be implemented by classes, which could be casted to 'SponsorDto'", ex); + } + b.accept(builder); + return builder.build(); + } + + /** + * Creates a builder initialized from this instance. + * + * @return a builder initialized with this instance's values + */ + default SponsorDtoBuilder with() { + try { + return new SponsorDtoBuilder(SponsorDto.class.cast(this)); + } catch (ClassCastException ex) { + throw new IllegalArgumentException("The interface 'SponsorDtoBuilder.With' should only be implemented by classes, which could be casted to 'SponsorDto'", ex); + } + } + } +} From fb064faf54be5c27a5307b6b1509e49d43821bb7 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sat, 20 Dec 2025 18:34:27 +0100 Subject: [PATCH 61/63] Extending test in example to show functionality --- README.md | 6 +- .../example/PersonDtoBuilderTest.java | 132 +++++++++++++++++- 2 files changed, 135 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ed8162cf..00cdc15d 100644 --- a/README.md +++ b/README.md @@ -321,8 +321,10 @@ A comprehensive example showcasing all fundamental Java property types with a mi Examples with all builder features enabled: -- **Person DTO**: [`PersonDto.java`](example/src/main/java/org/javahelpers/simple/builders/example/PersonDto.java) and [`PersonDtoBuilder.java`](example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/PersonDtoBuilder.java) - Demonstrates nested objects, collections, and various setter patterns -- **Product Record**: [`ProductRecord.java`](example/src/main/java/org/javahelpers/simple/builders/example/ProductRecord.java) and [`ProductRecordBuilder.java`](example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/ProductRecordBuilder.java) - Java Record support with full builder features +- **Person DTO**: [`PersonDto.java`](example/src/main/java/org/javahelpers/simple/builders/example/PersonDto.java) and [`PersonDtoBuilder.java`](example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/PersonDtoBuilder.java) - Demonstrates nested objects, collections, suppliers, conditional logic, and various setter patterns + - **Usage Examples**: [`PersonDtoBuilderTest.java`](example/src/test/java/org/javahelpers/simple/builders/example/PersonDtoBuilderTest.java) - Shows supplier methods, collection builders, nested builder consumers, and conditional logic +- **Product Record**: [`ProductRecord.java`](example/src/main/java/org/javahelpers/simple/builders/example/ProductRecord.java) and [`ProductRecordBuilder.java`](example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/ProductRecordBuilder.java) - Java Record support with full builder features and With interface pattern + - **Usage Examples**: [`ProductRecordTest.java`](example/src/test/java/org/javahelpers/simple/builders/example/ProductRecordTest.java) - Comprehensive tests demonstrating With interface for immutable Records, fluent modifications, and custom with methods ### Advanced Features diff --git a/example/src/test/java/org/javahelpers/simple/builders/example/PersonDtoBuilderTest.java b/example/src/test/java/org/javahelpers/simple/builders/example/PersonDtoBuilderTest.java index 75e9ab50..40accdbd 100644 --- a/example/src/test/java/org/javahelpers/simple/builders/example/PersonDtoBuilderTest.java +++ b/example/src/test/java/org/javahelpers/simple/builders/example/PersonDtoBuilderTest.java @@ -26,14 +26,16 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.time.LocalDate; +import java.util.List; import org.junit.jupiter.api.Test; class PersonDtoBuilderTest { @Test - void testBuilder() { + void testBasicBuilder() { PersonDto personDto = PersonDtoBuilder.create() .birthdate(LocalDate.now()) @@ -59,4 +61,132 @@ void testBuilder() { private String nameSupplier() { return "Testname"; } + + @Test + void testSupplierMethods() { + PersonDto person = + PersonDtoBuilder.create() + .name(() -> "John Doe") + .birthdate(() -> LocalDate.of(1990, 5, 15)) + .build(); + + assertNotNull(person); + assertEquals("John Doe", person.getName()); + assertEquals(LocalDate.of(1990, 5, 15), person.getBirthdate()); + } + + @Test + void testConditionalLogic() { + boolean isPremiumUser = true; + boolean hasNickname = false; + + PersonDto person = + PersonDtoBuilder.create() + .name("Jane Smith") + .conditional( + () -> isPremiumUser, + builder -> builder.birthdate(LocalDate.of(1990, 1, 1)), + builder -> builder.birthdate(LocalDate.of(2000, 1, 1))) + .conditional( + () -> hasNickname, + builder -> builder.nickNames(List.of("JJ"))) + .build(); + + assertNotNull(person); + assertEquals("Jane Smith", person.getName()); + assertEquals(LocalDate.of(1990, 1, 1), person.getBirthdate()); + } + + @Test + void testVarArgsHelpers() { + PersonDto person = + PersonDtoBuilder.create() + .name("Alice") + .nickNames("Ally", "Al", "Liz") + .build(); + + assertNotNull(person); + assertEquals("Alice", person.getName()); + assertNotNull(person.getNickNames()); + assertEquals(3, person.getNickNames().size()); + assertTrue(person.getNickNames().contains("Ally")); + } + + @Test + void testCollectionBuilders() { + PersonDto person = + PersonDtoBuilder.create() + .name("Bob") + .nickNames(list -> list.add("Bobby").add("Rob").add("Robert")) + .mannschaft( + teamBuilder -> + teamBuilder + .name("Dream Team") + .sponsoren( + sponsors -> + sponsors + .add(SponsorDtoBuilder.create().name("TechCorp").build()) + .add(SponsorDtoBuilder.create().name("SportsCo").build()))) + .build(); + + assertNotNull(person); + assertEquals("Bob", person.getName()); + assertEquals(3, person.getNickNames().size()); + assertNotNull(person.getMannschaft()); + assertEquals("Dream Team", person.getMannschaft().getName()); + assertEquals(2, person.getMannschaft().getSponsoren().size()); + } + + @Test + void testNestedBuilderConsumers() { + PersonDto person = + PersonDtoBuilder.create() + .name("Charlie") + .mannschaft( + team -> + team.name("Champions") + .sponsoren( + sponsors -> + sponsors.add( + sponsor -> sponsor.name("MegaCorp")))) + .build(); + + assertNotNull(person); + assertEquals("Charlie", person.getName()); + assertNotNull(person.getMannschaft()); + assertEquals("Champions", person.getMannschaft().getName()); + assertEquals(1, person.getMannschaft().getSponsoren().size()); + assertEquals("MegaCorp", person.getMannschaft().getSponsoren().iterator().next().getName()); + } + + @Test + void testCombinedFeatures() { + boolean addExtraInfo = true; + + PersonDto person = + PersonDtoBuilder.create() + .name(() -> "David") + .birthdate(LocalDate.of(1985, 3, 20)) + .nickNames("Dave", "Davey") + .conditional( + () -> addExtraInfo, + builder -> + builder + .mannschaft( + team -> + team.name("Elite Squad") + .sponsoren( + sponsors -> + sponsors.add( + sponsor -> sponsor.name("GlobalTech"))))) + .build(); + + assertNotNull(person); + assertEquals("David", person.getName()); + assertEquals(LocalDate.of(1985, 3, 20), person.getBirthdate()); + assertEquals(2, person.getNickNames().size()); + assertNotNull(person.getMannschaft()); + assertEquals("Elite Squad", person.getMannschaft().getName()); + assertEquals(1, person.getMannschaft().getSponsoren().size()); + } } From 7abc81437aa4cca954d78a7b84d61c83fe5e06fb Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sat, 20 Dec 2025 18:36:45 +0100 Subject: [PATCH 62/63] Stabilize generation by sorting, so that the builder has its methods always in same ordering --- .../builders/example/BookDtoBuilder.java | 144 +++++++++--------- .../example/MannschaftDtoBuilder.java | 62 ++++---- .../builders/example/PersonDtoBuilder.java | 132 ++++++++-------- .../example/ProductRecordBuilder.java | 74 ++++----- .../builders/example/SponsorDtoBuilder.java | 16 +- .../processor/util/JavaCodeGenerator.java | 11 +- .../BuilderConfigurationReaderTest.java | 16 +- 7 files changed, 229 insertions(+), 226 deletions(-) diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java index 719eb307..a0b0cd9e 100644 --- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java @@ -175,123 +175,123 @@ public BookDtoBuilder() { } /** - * Sets the value for lastUpdated. + * Sets the value for author. * - * @param lastUpdated the last update timestamp to set + * @param author the book author to set * @return current instance of builder */ - public BookDtoBuilder lastUpdated(LocalDateTime lastUpdated) { - this.lastUpdated = changedValue(lastUpdated); + public BookDtoBuilder author(String author) { + this.author = changedValue(author); return this; } /** - * Sets the value for title. + * Sets the value for available. * - * @param title the book title to set + * @param available true if available, false otherwise * @return current instance of builder */ - public BookDtoBuilder title(String title) { - this.title = changedValue(title); + public BookDtoBuilder available(boolean available) { + this.available = changedValue(available); return this; } /** - * Sets the value for subtitle. + * Sets the value for category. * - * @param subtitle an Optional containing the subtitle to set + * @param category the category code to set * @return current instance of builder */ - public BookDtoBuilder subtitle(Optional subtitle) { - this.subtitle = changedValue(subtitle); + public BookDtoBuilder category(char category) { + this.category = changedValue(category); return this; } /** - * Sets the value for publisher. + * Sets the value for discount. * - * @param publisher the publisher to set + * @param discount the discount percentage to set * @return current instance of builder */ - public BookDtoBuilder publisher(PersonDto publisher) { - this.publisher = changedValue(publisher); + public BookDtoBuilder discount(float discount) { + this.discount = changedValue(discount); return this; } /** - * Sets the value for available. + * Sets the value for edition. * - * @param available true if available, false otherwise + * @param edition the edition number to set * @return current instance of builder */ - public BookDtoBuilder available(boolean available) { - this.available = changedValue(available); + public BookDtoBuilder edition(short edition) { + this.edition = changedValue(edition); return this; } /** - * Sets the value for discount. + * Sets the value for exactPrice. * - * @param discount the discount percentage to set + * @param exactPrice the exact book price to set * @return current instance of builder */ - public BookDtoBuilder discount(float discount) { - this.discount = changedValue(discount); + public BookDtoBuilder exactPrice(BigDecimal exactPrice) { + this.exactPrice = changedValue(exactPrice); return this; } /** - * Sets the value for salesCount. + * Sets the value for genres. * - * @param salesCount the sales count to set + * @param genres the set of genres to set * @return current instance of builder */ - public BookDtoBuilder salesCount(long salesCount) { - this.salesCount = changedValue(salesCount); + public BookDtoBuilder genres(Set genres) { + this.genres = changedValue(genres); return this; } /** - * Sets the value for rating. + * Sets the value for isbn. * - * @param rating the book rating to set + * @param isbn the ISBN to set * @return current instance of builder */ - public BookDtoBuilder rating(byte rating) { - this.rating = changedValue(rating); + public BookDtoBuilder isbn(String isbn) { + this.isbn = changedValue(isbn); return this; } /** - * Sets the value for edition. + * Sets the value for lastUpdated. * - * @param edition the edition number to set + * @param lastUpdated the last update timestamp to set * @return current instance of builder */ - public BookDtoBuilder edition(short edition) { - this.edition = changedValue(edition); + public BookDtoBuilder lastUpdated(LocalDateTime lastUpdated) { + this.lastUpdated = changedValue(lastUpdated); return this; } /** - * Sets the value for genres. + * Sets the value for metadata. * - * @param genres the set of genres to set + * @param metadata the metadata map to set * @return current instance of builder */ - public BookDtoBuilder genres(Set genres) { - this.genres = changedValue(genres); + public BookDtoBuilder metadata(Map metadata) { + this.metadata = changedValue(metadata); return this; } /** - * Sets the value for metadata. + * Sets the value for pages. * - * @param metadata the metadata map to set + * @param pages the page count to set * @return current instance of builder */ - public BookDtoBuilder metadata(Map metadata) { - this.metadata = changedValue(metadata); + public BookDtoBuilder pages(int pages) { + this.pages = changedValue(pages); return this; } @@ -307,79 +307,79 @@ public BookDtoBuilder price(double price) { } /** - * Sets the value for tags. + * Sets the value for publishDate. * - * @param tags the list of tags to set + * @param publishDate the publication date to set * @return current instance of builder */ - public BookDtoBuilder tags(List tags) { - this.tags = changedValue(tags); + public BookDtoBuilder publishDate(LocalDate publishDate) { + this.publishDate = changedValue(publishDate); return this; } /** - * Sets the value for category. + * Sets the value for publisher. * - * @param category the category code to set + * @param publisher the publisher to set * @return current instance of builder */ - public BookDtoBuilder category(char category) { - this.category = changedValue(category); + public BookDtoBuilder publisher(PersonDto publisher) { + this.publisher = changedValue(publisher); return this; } /** - * Sets the value for author. + * Sets the value for rating. * - * @param author the book author to set + * @param rating the book rating to set * @return current instance of builder */ - public BookDtoBuilder author(String author) { - this.author = changedValue(author); + public BookDtoBuilder rating(byte rating) { + this.rating = changedValue(rating); return this; } /** - * Sets the value for publishDate. + * Sets the value for salesCount. * - * @param publishDate the publication date to set + * @param salesCount the sales count to set * @return current instance of builder */ - public BookDtoBuilder publishDate(LocalDate publishDate) { - this.publishDate = changedValue(publishDate); + public BookDtoBuilder salesCount(long salesCount) { + this.salesCount = changedValue(salesCount); return this; } /** - * Sets the value for exactPrice. + * Sets the value for subtitle. * - * @param exactPrice the exact book price to set + * @param subtitle an Optional containing the subtitle to set * @return current instance of builder */ - public BookDtoBuilder exactPrice(BigDecimal exactPrice) { - this.exactPrice = changedValue(exactPrice); + public BookDtoBuilder subtitle(Optional subtitle) { + this.subtitle = changedValue(subtitle); return this; } /** - * Sets the value for pages. + * Sets the value for tags. * - * @param pages the page count to set + * @param tags the list of tags to set * @return current instance of builder */ - public BookDtoBuilder pages(int pages) { - this.pages = changedValue(pages); + public BookDtoBuilder tags(List tags) { + this.tags = changedValue(tags); return this; } /** - * Sets the value for isbn. + * Sets the value for title. * - * @param isbn the ISBN to set + * @param title the book title to set * @return current instance of builder */ - public BookDtoBuilder isbn(String isbn) { - this.isbn = changedValue(isbn); + public BookDtoBuilder title(String title) { + this.title = changedValue(title); return this; } diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/MannschaftDtoBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/MannschaftDtoBuilder.java index 917b6247..96783a5d 100644 --- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/MannschaftDtoBuilder.java +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/MannschaftDtoBuilder.java @@ -49,24 +49,13 @@ public MannschaftDtoBuilder() { } /** - * Sets the value for sponsoren. - * - * @param sponsoren sponsoren - * @return current instance of builder - */ - public MannschaftDtoBuilder sponsoren(SponsorDto... sponsoren) { - this.sponsoren = changedValue(Set.of(sponsoren)); - return this; - } - - /** - * Sets the value for name by invoking the provided supplier. + * Sets the value for name. * - * @param nameSupplier supplier for name + * @param name name * @return current instance of builder */ - public MannschaftDtoBuilder name(Supplier nameSupplier) { - this.name = changedValue(nameSupplier.get()); + public MannschaftDtoBuilder name(String name) { + this.name = changedValue(name); return this; } @@ -83,37 +72,37 @@ public MannschaftDtoBuilder name(String format, Object... args) { } /** - * Sets the value for sponsoren by invoking the provided supplier. + * Sets the value for name by executing the provided consumer. * - * @param sponsorenSupplier supplier for sponsoren + * @param nameStringBuilderConsumer consumer providing an instance of name * @return current instance of builder */ - public MannschaftDtoBuilder sponsoren(Supplier> sponsorenSupplier) { - this.sponsoren = changedValue(sponsorenSupplier.get()); + public MannschaftDtoBuilder name(Consumer nameStringBuilderConsumer) { + StringBuilder builder = new StringBuilder(); + nameStringBuilderConsumer.accept(builder); + this.name = changedValue(builder.toString()); return this; } /** - * Sets the value for name by executing the provided consumer. + * Sets the value for name by invoking the provided supplier. * - * @param nameStringBuilderConsumer consumer providing an instance of name + * @param nameSupplier supplier for name * @return current instance of builder */ - public MannschaftDtoBuilder name(Consumer nameStringBuilderConsumer) { - StringBuilder builder = new StringBuilder(); - nameStringBuilderConsumer.accept(builder); - this.name = changedValue(builder.toString()); + public MannschaftDtoBuilder name(Supplier nameSupplier) { + this.name = changedValue(nameSupplier.get()); return this; } /** - * Sets the value for name. + * Sets the value for sponsoren. * - * @param name name + * @param sponsoren sponsoren * @return current instance of builder */ - public MannschaftDtoBuilder name(String name) { - this.name = changedValue(name); + public MannschaftDtoBuilder sponsoren(Set sponsoren) { + this.sponsoren = changedValue(sponsoren); return this; } @@ -131,14 +120,25 @@ public MannschaftDtoBuilder sponsoren( return this; } + /** + * Sets the value for sponsoren by invoking the provided supplier. + * + * @param sponsorenSupplier supplier for sponsoren + * @return current instance of builder + */ + public MannschaftDtoBuilder sponsoren(Supplier> sponsorenSupplier) { + this.sponsoren = changedValue(sponsorenSupplier.get()); + return this; + } + /** * Sets the value for sponsoren. * * @param sponsoren sponsoren * @return current instance of builder */ - public MannschaftDtoBuilder sponsoren(Set sponsoren) { - this.sponsoren = changedValue(sponsoren); + public MannschaftDtoBuilder sponsoren(SponsorDto... sponsoren) { + this.sponsoren = changedValue(Set.of(sponsoren)); return this; } diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/PersonDtoBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/PersonDtoBuilder.java index 92855b25..7aa00344 100644 --- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/PersonDtoBuilder.java +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/PersonDtoBuilder.java @@ -67,24 +67,13 @@ public PersonDtoBuilder() { } /** - * Sets the value for nickNames2. - * - * @param nickNames2 nickNames2 - * @return current instance of builder - */ - public PersonDtoBuilder nickNames2(String... nickNames2) { - this.nickNames2 = changedValue(nickNames2); - return this; - } - - /** - * Sets the value for nickNames2 by invoking the provided supplier. + * Sets the value for birthdate. * - * @param nickNames2Supplier supplier for nickNames2 + * @param birthdate birthdate * @return current instance of builder */ - public PersonDtoBuilder nickNames2(Supplier nickNames2Supplier) { - this.nickNames2 = changedValue(nickNames2Supplier.get()); + public PersonDtoBuilder birthdate(LocalDate birthdate) { + this.birthdate = changedValue(birthdate); return this; } @@ -100,86 +89,84 @@ public PersonDtoBuilder birthdate(Supplier birthdateSupplier) { } /** - * Sets the value for nickNames using a builder consumer that produces the value. + * Sets the value for mannschaft using a builder consumer that produces the value. * - * @param nickNamesBuilderConsumer consumer providing an instance of a builder for nickNames + * @param mannschaftBuilderConsumer consumer providing an instance of a builder for mannschaft * @return current instance of builder */ - public PersonDtoBuilder nickNames(Consumer> nickNamesBuilderConsumer) { - ArrayListBuilder builder = this.nickNames.isSet() ? new ArrayListBuilder(this.nickNames.value()) : new ArrayListBuilder(); - nickNamesBuilderConsumer.accept(builder); - this.nickNames = changedValue(builder.build()); + public PersonDtoBuilder mannschaft(Consumer mannschaftBuilderConsumer) { + MannschaftDtoBuilder builder = this.mannschaft.isSet() ? new MannschaftDtoBuilder(this.mannschaft.value()) : new MannschaftDtoBuilder(); + mannschaftBuilderConsumer.accept(builder); + this.mannschaft = changedValue(builder.build()); return this; } /** - * Sets the value for name. + * Sets the value for mannschaft by invoking the provided supplier. * - * @param format name - * @param args name + * @param mannschaftSupplier supplier for mannschaft * @return current instance of builder */ - public PersonDtoBuilder name(String format, Object... args) { - this.name = changedValue(String.format(format, args)); + public PersonDtoBuilder mannschaft(Supplier mannschaftSupplier) { + this.mannschaft = changedValue(mannschaftSupplier.get()); return this; } /** - * Sets the value for name by executing the provided consumer. + * Sets the value for mannschaft. * - * @param nameStringBuilderConsumer consumer providing an instance of name + * @param mannschaft mannschaft * @return current instance of builder */ - public PersonDtoBuilder name(Consumer nameStringBuilderConsumer) { - StringBuilder builder = new StringBuilder(); - nameStringBuilderConsumer.accept(builder); - this.name = changedValue(builder.toString()); + public PersonDtoBuilder mannschaft(MannschaftDto mannschaft) { + this.mannschaft = changedValue(mannschaft); return this; } /** - * Sets the value for mannschaft. + * Sets the value for name. * - * @param mannschaft mannschaft + * @param name name * @return current instance of builder */ - public PersonDtoBuilder mannschaft(MannschaftDto mannschaft) { - this.mannschaft = changedValue(mannschaft); + public PersonDtoBuilder name(String name) { + this.name = changedValue(name); return this; } /** - * Sets the value for mannschaft using a builder consumer that produces the value. + * Sets the value for name. * - * @param mannschaftBuilderConsumer consumer providing an instance of a builder for mannschaft + * @param format name + * @param args name * @return current instance of builder */ - public PersonDtoBuilder mannschaft(Consumer mannschaftBuilderConsumer) { - MannschaftDtoBuilder builder = this.mannschaft.isSet() ? new MannschaftDtoBuilder(this.mannschaft.value()) : new MannschaftDtoBuilder(); - mannschaftBuilderConsumer.accept(builder); - this.mannschaft = changedValue(builder.build()); + public PersonDtoBuilder name(String format, Object... args) { + this.name = changedValue(String.format(format, args)); return this; } /** - * Sets the value for nickNames2. + * Sets the value for name by executing the provided consumer. * - * @param nickNames2 nickNames2 + * @param nameStringBuilderConsumer consumer providing an instance of name * @return current instance of builder */ - public PersonDtoBuilder nickNames2(List nickNames2) { - this.nickNames2 = changedValue(nickNames2.toArray(new String[0])); + public PersonDtoBuilder name(Consumer nameStringBuilderConsumer) { + StringBuilder builder = new StringBuilder(); + nameStringBuilderConsumer.accept(builder); + this.name = changedValue(builder.toString()); return this; } /** - * Sets the value for mannschaft by invoking the provided supplier. + * Sets the value for name by invoking the provided supplier. * - * @param mannschaftSupplier supplier for mannschaft + * @param nameSupplier supplier for name * @return current instance of builder */ - public PersonDtoBuilder mannschaft(Supplier mannschaftSupplier) { - this.mannschaft = changedValue(mannschaftSupplier.get()); + public PersonDtoBuilder name(Supplier nameSupplier) { + this.name = changedValue(nameSupplier.get()); return this; } @@ -195,24 +182,26 @@ public PersonDtoBuilder nickNames(String... nickNames) { } /** - * Sets the value for birthdate. + * Sets the value for nickNames. * - * @param birthdate birthdate + * @param nickNames nickNames * @return current instance of builder */ - public PersonDtoBuilder birthdate(LocalDate birthdate) { - this.birthdate = changedValue(birthdate); + public PersonDtoBuilder nickNames(List nickNames) { + this.nickNames = changedValue(nickNames); return this; } /** - * Sets the value for name by invoking the provided supplier. + * Sets the value for nickNames using a builder consumer that produces the value. * - * @param nameSupplier supplier for name + * @param nickNamesBuilderConsumer consumer providing an instance of a builder for nickNames * @return current instance of builder */ - public PersonDtoBuilder name(Supplier nameSupplier) { - this.name = changedValue(nameSupplier.get()); + public PersonDtoBuilder nickNames(Consumer> nickNamesBuilderConsumer) { + ArrayListBuilder builder = this.nickNames.isSet() ? new ArrayListBuilder(this.nickNames.value()) : new ArrayListBuilder(); + nickNamesBuilderConsumer.accept(builder); + this.nickNames = changedValue(builder.build()); return this; } @@ -228,24 +217,24 @@ public PersonDtoBuilder nickNames(Supplier> nickNamesSupplier) { } /** - * Sets the value for name. + * Sets the value for nickNames2. * - * @param name name + * @param nickNames2 nickNames2 * @return current instance of builder */ - public PersonDtoBuilder name(String name) { - this.name = changedValue(name); + public PersonDtoBuilder nickNames2(String... nickNames2) { + this.nickNames2 = changedValue(nickNames2); return this; } /** - * Sets the value for nickNames. + * Sets the value for nickNames2. * - * @param nickNames nickNames + * @param nickNames2 nickNames2 * @return current instance of builder */ - public PersonDtoBuilder nickNames(List nickNames) { - this.nickNames = changedValue(nickNames); + public PersonDtoBuilder nickNames2(List nickNames2) { + this.nickNames2 = changedValue(nickNames2.toArray(new String[0])); return this; } @@ -262,6 +251,17 @@ public PersonDtoBuilder nickNames2(Consumer> nickNames2 return this; } + /** + * Sets the value for nickNames2 by invoking the provided supplier. + * + * @param nickNames2Supplier supplier for nickNames2 + * @return current instance of builder + */ + public PersonDtoBuilder nickNames2(Supplier nickNames2Supplier) { + this.nickNames2 = changedValue(nickNames2Supplier.get()); + return this; + } + @Override public PersonDto build() { PersonDto result = new PersonDto(this.name.value()); diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/ProductRecordBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/ProductRecordBuilder.java index bfe00d16..3a6ab3c8 100644 --- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/ProductRecordBuilder.java +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/ProductRecordBuilder.java @@ -55,6 +55,17 @@ public ProductRecordBuilder(ProductRecord instance) { public ProductRecordBuilder() { } + /** + * Sets the value for category. + * + * @param category category + * @return current instance of builder + */ + public ProductRecordBuilder category(String category) { + this.category = changedValue(category); + return this; + } + /** * Sets the value for category. * @@ -68,35 +79,37 @@ public ProductRecordBuilder category(String format, Object... args) { } /** - * Sets the value for price. + * Sets the value for category by executing the provided consumer. * - * @param price price + * @param categoryStringBuilderConsumer consumer providing an instance of category * @return current instance of builder */ - public ProductRecordBuilder price(double price) { - this.price = changedValue(price); + public ProductRecordBuilder category(Consumer categoryStringBuilderConsumer) { + StringBuilder builder = new StringBuilder(); + categoryStringBuilderConsumer.accept(builder); + this.category = changedValue(builder.toString()); return this; } /** - * Sets the value for price by invoking the provided supplier. + * Sets the value for category by invoking the provided supplier. * - * @param priceSupplier supplier for price + * @param categorySupplier supplier for category * @return current instance of builder */ - public ProductRecordBuilder price(Supplier priceSupplier) { - this.price = changedValue(priceSupplier.get()); + public ProductRecordBuilder category(Supplier categorySupplier) { + this.category = changedValue(categorySupplier.get()); return this; } /** - * Sets the value for name by invoking the provided supplier. + * Sets the value for name. * - * @param nameSupplier supplier for name + * @param name name * @return current instance of builder */ - public ProductRecordBuilder name(Supplier nameSupplier) { - this.name = changedValue(nameSupplier.get()); + public ProductRecordBuilder name(String name) { + this.name = changedValue(name); return this; } @@ -112,17 +125,6 @@ public ProductRecordBuilder name(String format, Object... args) { return this; } - /** - * Sets the value for category. - * - * @param category category - * @return current instance of builder - */ - public ProductRecordBuilder category(String category) { - this.category = changedValue(category); - return this; - } - /** * Sets the value for name by executing the provided consumer. * @@ -137,37 +139,35 @@ public ProductRecordBuilder name(Consumer nameStringBuilderConsum } /** - * Sets the value for name. + * Sets the value for name by invoking the provided supplier. * - * @param name name + * @param nameSupplier supplier for name * @return current instance of builder */ - public ProductRecordBuilder name(String name) { - this.name = changedValue(name); + public ProductRecordBuilder name(Supplier nameSupplier) { + this.name = changedValue(nameSupplier.get()); return this; } /** - * Sets the value for category by invoking the provided supplier. + * Sets the value for price. * - * @param categorySupplier supplier for category + * @param price price * @return current instance of builder */ - public ProductRecordBuilder category(Supplier categorySupplier) { - this.category = changedValue(categorySupplier.get()); + public ProductRecordBuilder price(double price) { + this.price = changedValue(price); return this; } /** - * Sets the value for category by executing the provided consumer. + * Sets the value for price by invoking the provided supplier. * - * @param categoryStringBuilderConsumer consumer providing an instance of category + * @param priceSupplier supplier for price * @return current instance of builder */ - public ProductRecordBuilder category(Consumer categoryStringBuilderConsumer) { - StringBuilder builder = new StringBuilder(); - categoryStringBuilderConsumer.accept(builder); - this.category = changedValue(builder.toString()); + public ProductRecordBuilder price(Supplier priceSupplier) { + this.price = changedValue(priceSupplier.get()); return this; } diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SponsorDtoBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SponsorDtoBuilder.java index 41c66f68..adf74258 100644 --- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SponsorDtoBuilder.java +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SponsorDtoBuilder.java @@ -41,13 +41,13 @@ public SponsorDtoBuilder() { } /** - * Sets the value for name by invoking the provided supplier. + * Sets the value for name. * - * @param nameSupplier supplier for name + * @param name name * @return current instance of builder */ - public SponsorDtoBuilder name(Supplier nameSupplier) { - this.name = changedValue(nameSupplier.get()); + public SponsorDtoBuilder name(String name) { + this.name = changedValue(name); return this; } @@ -77,13 +77,13 @@ public SponsorDtoBuilder name(Consumer nameStringBuilderConsumer) } /** - * Sets the value for name. + * Sets the value for name by invoking the provided supplier. * - * @param name name + * @param nameSupplier supplier for name * @return current instance of builder */ - public SponsorDtoBuilder name(String name) { - this.name = changedValue(name); + public SponsorDtoBuilder name(Supplier nameSupplier) { + this.name = changedValue(nameSupplier.get()); return this; } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java index e92d89c4..97bb6d55 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java @@ -38,7 +38,6 @@ import com.palantir.javapoet.ParameterizedTypeName; import com.palantir.javapoet.TypeSpec; import java.io.IOException; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -233,10 +232,10 @@ private void writeClassToFile(String packageName, TypeSpec typeSpec) throws Buil /** * Resolves method conflicts by keeping only the highest priority method for each unique * signature. This prevents compilation errors when methods from different fields have the same - * signature. + * signature. Returns methods sorted by signature for stable generation order. * * @param methodToField mapping from method to its source field - * @return list of methods with conflicts resolved + * @return list of methods with conflicts resolved, sorted by signature for stability */ private List resolveMethodConflicts(Map methodToField) { Map signatureToMethod = new HashMap<>(); @@ -283,7 +282,11 @@ private List resolveMethodConflicts(Map methodTo } } - return new ArrayList<>(signatureToMethod.values()); + // Sort methods by signature key for stable generation order across compilations + return signatureToMethod.entrySet().stream() + .sorted(Map.Entry.comparingByKey()) + .map(Map.Entry::getValue) + .collect(java.util.stream.Collectors.toList()); } private CodeBlock createJavadocForClass(ClassName dtoClass) { 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 e49e80ea..53120c6a 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 @@ -495,24 +495,24 @@ public PersonDtoMinimalBuilder() { } /** - * Sets the value for tags. + * Sets the value for name. * - * @param tags tags + * @param name name * @return current instance of builder */ - public PersonDtoMinimalBuilder withTags(List tags) { - this.tags = changedValue(tags); + public PersonDtoMinimalBuilder withName(String name) { + this.name = changedValue(name); return this; } /** - * Sets the value for name. + * Sets the value for tags. * - * @param name name + * @param tags tags * @return current instance of builder */ - public PersonDtoMinimalBuilder withName(String name) { - this.name = changedValue(name); + public PersonDtoMinimalBuilder withTags(List tags) { + this.tags = changedValue(tags); return this; } From cf0299b4b3df9c740c29054cbacd101a2daa446d Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sat, 20 Dec 2025 18:41:00 +0100 Subject: [PATCH 63/63] Fixed codesmell on collection-creation by stream --- .../simple/builders/processor/util/JavaCodeGenerator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java index 97bb6d55..4e60003a 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java @@ -286,7 +286,7 @@ private List resolveMethodConflicts(Map methodTo return signatureToMethod.entrySet().stream() .sorted(Map.Entry.comparingByKey()) .map(Map.Entry::getValue) - .collect(java.util.stream.Collectors.toList()); + .toList(); } private CodeBlock createJavadocForClass(ClassName dtoClass) {