@@ -92,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 {
@@ -257,9 +263,81 @@ 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.
+
+#### 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).**
+
+## 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, 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
+
+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](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/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..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
@@ -25,11 +25,602 @@
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;
+import org.javahelpers.simple.builders.core.enums.OptionState;
-/** 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:
+ *
+ *
+ * Field Setters: generateFieldSupplier, generateFieldConsumer, generateBuilderConsumer
+ * (all default: true)
+ * Conditional Logic: generateConditionalHelper (default: true)
+ * Access Control: builderAccess, builderConstructorAccess, methodAccess (default:
+ * PUBLIC)
+ * Collection Helpers: generateVarArgsHelpers, usingArrayListBuilder,
+ * usingArrayListBuilderWithElementBuilders, usingHashSetBuilder,
+ * usingHashSetBuilderWithElementBuilders, usingHashMapBuilder (all default: true)
+ * Integration: generateWithInterface (default: true)
+ *
+ *
+ * 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 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)
+ @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.
+ *
+ * Example:
+ *
+ *
{@code
+ * PersonDto person = PersonDtoBuilder.create()
+ * .name(() -> fetchNameFromDatabase())
+ * .age(() -> calculateAge())
+ * .build();
+ * }
+ *
+ * Default: ENABLED
+ * Compiler option: -Asimplebuilder.generateFieldSupplier
+ */
+ OptionState generateFieldSupplier() default OptionState.UNSET;
+
+ /**
+ * 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.
+ *
+ * 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;
+
+ /**
+ * 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.
+ *
+ * Example:
+ *
+ *
{@code
+ * PersonDto person = PersonDtoBuilder.create()
+ * .address(ab -> ab
+ * .street("Main St")
+ * .city("Berlin")
+ * .zipCode("10115"))
+ * .build();
+ * }
+ *
+ * Default: ENABLED
+ * Compiler option: -Asimplebuilder.generateBuilderConsumer
+ */
+ OptionState generateBuilderConsumer() default OptionState.UNSET;
+
+ /**
+ * Generate conditional logic method (conditional)
+ * 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;
+
+ // === Access Control ===
+ /**
+ * Access level for the generated builder class.
+ *
+ *
+ * PUBLIC - For public APIs (default)
+ * PACKAGE_PRIVATE - For internal use within a package
+ *
+ *
+ * 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
+ * @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, PACKAGE_PRIVATE)
+ *
+ * @see #builderConstructorAccess() for controlling constructor visibility
+ */
+ 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,
+ * PACKAGE_PRIVATE, PRIVATE)
+ */
+ AccessModifier builderConstructorAccess() default AccessModifier.PUBLIC;
+
+ /**
+ * Access level for generated builder methods.
+ *
+ *
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
+ * @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, PACKAGE_PRIVATE)
+ */
+ AccessModifier methodAccess() default AccessModifier.PUBLIC;
+
+ // === Collection Options ===
+ /**
+ * Generate helper methods with VarArgs for Lists and Sets.
+ * 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.
+ * 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;
+
+ /**
+ * 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().
+ *
+ * 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;
+
+ /**
+ * 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: ENABLED Compiler option: -Asimplebuilder.usingArrayListBuilder
+ */
+ OptionState usingArrayListBuilder() default OptionState.UNSET;
+
+ /**
+ * 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: ENABLED Compiler option: -Asimplebuilder.usingArrayListBuilderWithElementBuilders
+ */
+ OptionState usingArrayListBuilderWithElementBuilders() 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:
+ * 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: ENABLED Compiler option: -Asimplebuilder.usingHashSetBuilder
+ */
+ OptionState usingHashSetBuilder() default OptionState.UNSET;
+
+ /**
+ * 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: ENABLED Compiler option: -Asimplebuilder.usingHashSetBuilderWithElementBuilders
+ */
+ OptionState usingHashSetBuilderWithElementBuilders() default OptionState.UNSET;
+
+ /**
+ * 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: ENABLED Compiler option: -Asimplebuilder.usingHashMapBuilder
+ */
+ OptionState usingHashMapBuilder() default OptionState.UNSET;
+
+ // === Annotations ===
+ /**
+ * Use {@code @Generated} annotation on the generated builder class.
+ * 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.
+ * 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.
+ * 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.
+ * 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;
+
+ // === 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".
+ *
+ * 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.
+ *
+ * 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 "";
+ }
+
+ /**
+ * 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(
+ * generateFieldSupplier = true,
+ * generateFieldConsumer = 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..602ff159
--- /dev/null
+++ b/core/src/main/java/org/javahelpers/simple/builders/core/enums/AccessModifier.java
@@ -0,0 +1,64 @@
+/*
+ * 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.
+ *
+ *
Note: PROTECTED is intentionally not included as builders follow the Builder pattern, not
+ * inheritance, and should not be extended.
+ */
+public enum AccessModifier {
+
+ /** Default access */
+ DEFAULT("public"),
+
+ /** Public access - accessible from anywhere */
+ PUBLIC("public"),
+
+ /** 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;
+ }
+}
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..cb955d20
--- /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:
+ *
+ *
+ * DEFAULT - Use inherited value (from compiler options or built-in defaults)
+ * ENABLED - Explicitly enable this option (override global/default)
+ * DISABLED - Explicitly disable this option (override global/default)
+ *
+ *
+ * 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 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.
+ */
+ UNSET,
+
+ /** 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/docs/CONFIGURATION.md b/docs/CONFIGURATION.md
new file mode 100644
index 00000000..ad16e7f2
--- /dev/null
+++ b/docs/CONFIGURATION.md
@@ -0,0 +1,1075 @@
+# 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 = OptionState.ENABLED,
+ generateFieldConsumer = OptionState.ENABLED,
+ generateBuilderConsumer = OptionState.ENABLED,
+ generateConditionalHelper = OptionState.ENABLED,
+ builderAccess = AccessModifier.PUBLIC,
+ methodAccess = AccessModifier.PUBLIC,
+ generateVarArgsHelpers = OptionState.ENABLED,
+ usingArrayListBuilder = OptionState.ENABLED,
+ usingHashMapBuilder = OptionState.ENABLED,
+ generateWithInterface = OptionState.ENABLED
+)
+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=ENABLED
+ -Asimplebuilder.generateFieldConsumer=ENABLED
+ -Asimplebuilder.builderAccess=PUBLIC
+ -Asimplebuilder.usingArrayListBuilder=ENABLED
+
+
+
+```
+
+### Gradle Configuration
+
+```gradle
+dependencies {
+ annotationProcessor "io.github.java-helpers:simple-builders-processor:${simpleBuildersVersion}"
+}
+
+compileJava {
+ options.compilerArgs += [
+ "-Asimplebuilder.generateFieldSupplier=ENABLED",
+ "-Asimplebuilder.generateFieldConsumer=ENABLED",
+ "-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
+
+All options use `OptionState` enum with values: `ENABLED`, `DISABLED`, or `UNSET` (uses default/compiler arg).
+
+### Field Setter Generation
+
+#### `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
+
+#### `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
+
+#### `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)
+
+⚠️ **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
+// 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
+
+⚠️ **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
+// 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
+
+#### `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 {
+ // ...
+}
+```
+
+**When DISABLED**: No `@Generated` annotation. Useful if you want the builder counted in code coverage.
+
+---
+
+#### `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
+
+### Minimal Builder
+
+Generate only essential builder methods:
+
+```java
+@SimpleBuilder
+@SimpleBuilder.Options(
+ 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;
+
+ 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=ENABLED
+ -Asimplebuilder.generateFieldConsumer=ENABLED
+ -Asimplebuilder.generateBuilderConsumer=ENABLED
+
+
+ -Asimplebuilder.builderAccess=PACKAGE_PRIVATE
+
+
+ -Asimplebuilder.usingArrayListBuilder=ENABLED
+ -Asimplebuilder.usingHashMapBuilder=ENABLED
+
+```
+
+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
+
+### 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
+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
+
+### 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
+)
+```
+
+❌ **Invalid Combinations (Will Cause Builder Generation to Fail)**:
+
+```java
+// ❌ 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 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
+
+### All Compiler Options
+
+```
+# Field Setter Generation
+-Asimplebuilder.generateFieldSupplier=ENABLED|DISABLED
+-Asimplebuilder.generateFieldConsumer=ENABLED|DISABLED
+-Asimplebuilder.generateBuilderConsumer=ENABLED|DISABLED
+
+# Conditional Logic
+-Asimplebuilder.generateConditionalHelper=ENABLED|DISABLED
+
+# Access Control
+-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)
+
+# Helper Methods
+-Asimplebuilder.generateVarArgsHelpers=ENABLED|DISABLED
+-Asimplebuilder.generateStringFormatHelpers=ENABLED|DISABLED
+-Asimplebuilder.generateUnboxedOptional=ENABLED|DISABLED
+
+# 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
+
+```java
+@SimpleBuilder
+@SimpleBuilder.Options(
+ // Field Setter Generation
+ generateFieldSupplier = OptionState.ENABLED,
+ generateFieldConsumer = OptionState.ENABLED,
+ generateBuilderConsumer = OptionState.ENABLED,
+
+ // Conditional Logic
+ 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
+ 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,
+
+ // Naming
+ builderSuffix = "Builder",
+ setterSuffix = ""
+)
+public class ExampleDto {
+ private String name;
+}
+```
+
+---
+
+**Related**: [README.md](../README.md)
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
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());
+ }
+}
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());
+ }
}
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..a0b0cd9e
--- /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 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 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 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 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 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 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 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 isbn.
+ *
+ * @param isbn the ISBN to set
+ * @return current instance of builder
+ */
+ public BookDtoBuilder isbn(String isbn) {
+ this.isbn = changedValue(isbn);
+ return this;
+ }
+
+ /**
+ * 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 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 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 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 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 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 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 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 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 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 title.
+ *
+ * @param title the book title to set
+ * @return current instance of builder
+ */
+ public BookDtoBuilder title(String title) {
+ this.title = changedValue(title);
+ 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..96783a5d
--- /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 name.
+ *
+ * @param name name
+ * @return current instance of builder
+ */
+ public MannschaftDtoBuilder name(String name) {
+ this.name = changedValue(name);
+ 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 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 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 sponsoren.
+ *
+ * @param sponsoren sponsoren
+ * @return current instance of builder
+ */
+ public MannschaftDtoBuilder sponsoren(Set sponsoren) {
+ this.sponsoren = changedValue(sponsoren);
+ 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 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(SponsorDto... sponsoren) {
+ this.sponsoren = changedValue(Set.of(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..7aa00344
--- /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 birthdate.
+ *
+ * @param birthdate birthdate
+ * @return current instance of builder
+ */
+ public PersonDtoBuilder birthdate(LocalDate birthdate) {
+ this.birthdate = changedValue(birthdate);
+ 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 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 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 mannschaft.
+ *
+ * @param mannschaft mannschaft
+ * @return current instance of builder
+ */
+ public PersonDtoBuilder mannschaft(MannschaftDto mannschaft) {
+ this.mannschaft = changedValue(mannschaft);
+ 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 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 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.
+ *
+ * @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 nickNames.
+ *
+ * @param nickNames nickNames
+ * @return current instance of builder
+ */
+ public PersonDtoBuilder nickNames(List nickNames) {
+ this.nickNames = changedValue(nickNames);
+ 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 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 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.
+ *
+ * @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 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;
+ }
+
+ /**
+ * 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());
+ 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..3a6ab3c8
--- /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 category category
+ * @return current instance of builder
+ */
+ public ProductRecordBuilder category(String category) {
+ this.category = changedValue(category);
+ return this;
+ }
+
+ /**
+ * 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 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;
+ }
+
+ /**
+ * 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 name.
+ *
+ * @param name name
+ * @return current instance of builder
+ */
+ public ProductRecordBuilder name(String name) {
+ this.name = changedValue(name);
+ 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 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 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 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;
+ }
+
+ @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..adf74258
--- /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.
+ *
+ * @param name name
+ * @return current instance of builder
+ */
+ public SponsorDtoBuilder name(String name) {
+ this.name = changedValue(name);
+ 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 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;
+ }
+
+ @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);
+ }
+ }
+ }
+}
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..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
@@ -27,18 +27,26 @@
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;
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.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;
import org.javahelpers.simple.builders.processor.util.ProcessingLogger;
@@ -49,8 +57,7 @@
* javax.annotation.processing.AbstractProcessor}.
*/
@AutoService(Processor.class)
-@SupportedAnnotationTypes("org.javahelpers.simple.builders.core.annotations.SimpleBuilder")
-@SupportedOptions("verbose")
+@SupportedAnnotationTypes("*")
public class BuilderProcessor extends AbstractProcessor {
private ProcessingContext context;
private JavaCodeGenerator codeGenerator;
@@ -60,10 +67,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) {
@@ -79,32 +93,44 @@ public boolean process(Set extends TypeElement> 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:
+ // 1. Elements annotated with @SimpleBuilder
+ // 2. Elements annotated with custom annotations that have @SimpleBuilder.Template
+ // Configuration is resolved per-element to handle priority correctly when both exist
+ 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) {
+ elementsToProcess.addAll(roundEnv.getElementsAnnotatedWith(simpleBuilderAnnotation));
+ }
+
+ // Find all Annotations with @SimpleBuilder.Template
+ List annotationsWithTemplate = extractingAnnotationsWithTemplate(annotations);
+ for (TypeElement annotation : annotationsWithTemplate) {
+ elementsToProcess.addAll(roundEnv.getElementsAnnotatedWith(annotation));
}
- Set extends Element> 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 (Element annotatedElement : elementsToProcess) {
try {
context.debug("------------------------------------");
context.debug("simple-builders: Processing element: %s", annotatedElement.getSimpleName());
context.debug("------------------------------------");
- process(annotatedElement);
+ // 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",
annotatedElement.getSimpleName());
@@ -117,12 +143,24 @@ public boolean process(Set extends TypeElement> 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();
}
- private void process(Element annotatedElement) throws BuilderException {
+ private void process(Element annotatedElement, BuilderConfiguration config)
+ throws BuilderException {
+ context.initConfigurationForProcessingTarget(config);
BuilderDefinitionDto builderDef = extractFromElement(annotatedElement, context);
codeGenerator.generateBuilder(builderDef);
}
@@ -139,4 +177,30 @@ private static boolean isAtLeastJava17(SourceVersion current) {
return false;
}
}
+
+ private static List extractingAnnotationsWithTemplate(
+ Set extends TypeElement> 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(annotation);
+ }
+ }
+ return result;
+ }
}
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..1422e948
--- /dev/null
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java
@@ -0,0 +1,619 @@
+/*
+ * 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 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.annotations.SimpleBuilder;
+import org.javahelpers.simple.builders.core.enums.AccessModifier;
+import org.javahelpers.simple.builders.core.enums.OptionState;
+
+/**
+ * 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 generateFieldConsumer Generate field consumer 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
+ * @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
+ * @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 usingGeneratedAnnotation Use Generated annotation
+ * @param usingBuilderImplementationAnnotation Use BuilderImplementation annotation
+ * @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,
+ OptionState generateFieldConsumer,
+ OptionState generateBuilderConsumer,
+ OptionState generateConditionalHelper,
+ AccessModifier builderAccess,
+ AccessModifier builderConstructorAccess,
+ AccessModifier methodAccess,
+ OptionState generateVarArgsHelpers,
+ OptionState generateStringFormatHelpers,
+ OptionState generateUnboxedOptional,
+ OptionState usingArrayListBuilder,
+ OptionState usingArrayListBuilderWithElementBuilders,
+ OptionState usingHashSetBuilder,
+ OptionState usingHashSetBuilderWithElementBuilders,
+ OptionState usingHashMapBuilder,
+ OptionState usingGeneratedAnnotation,
+ OptionState usingBuilderImplementationAnnotation,
+ OptionState implementsBuilderBase,
+ OptionState generateWithInterface,
+ String builderSuffix,
+ String setterSuffix) {
+
+ public static final BuilderConfiguration DEFAULT =
+ builder()
+ .generateSupplier(ENABLED)
+ .generateConsumer(ENABLED)
+ .generateBuilderConsumer(ENABLED)
+ .generateConditionalLogic(ENABLED)
+ .builderAccess(PUBLIC)
+ .builderConstructorAccess(PUBLIC)
+ .methodAccess(PUBLIC)
+ .generateVarArgsHelpers(ENABLED)
+ .generateStringFormatHelpers(ENABLED)
+ .generateUnboxedOptional(ENABLED)
+ .usingArrayListBuilder(ENABLED)
+ .usingArrayListBuilderWithElementBuilders(ENABLED)
+ .usingHashSetBuilder(ENABLED)
+ .usingHashSetBuilderWithElementBuilders(ENABLED)
+ .usingHashMapBuilder(ENABLED)
+ .usingGeneratedAnnotation(ENABLED)
+ .usingBuilderImplementationAnnotation(ENABLED)
+ .implementsBuilderBase(ENABLED)
+ .generateWithInterface(ENABLED)
+ .builderSuffix("Builder")
+ .setterSuffix("")
+ .build();
+
+ // === Convenience accessors with 'is' prefix for boolean properties ===
+ public boolean shouldGenerateFieldSupplier() {
+ return generateFieldSupplier == ENABLED;
+ }
+
+ public boolean shouldGenerateFieldConsumer() {
+ return generateFieldConsumer == ENABLED;
+ }
+
+ public boolean shouldGenerateBuilderConsumer() {
+ return generateBuilderConsumer == ENABLED;
+ }
+
+ public boolean shouldGenerateConditionalLogic() {
+ return generateConditionalHelper == ENABLED;
+ }
+
+ public boolean shouldGenerateWithInterface() {
+ return generateWithInterface == ENABLED;
+ }
+
+ public boolean shouldGenerateVarArgsHelpers() {
+ return generateVarArgsHelpers == ENABLED;
+ }
+
+ public boolean shouldGenerateStringFormatHelpers() {
+ return generateStringFormatHelpers == ENABLED;
+ }
+
+ public boolean shouldUseArrayListBuilder() {
+ return usingArrayListBuilder == ENABLED;
+ }
+
+ public boolean shouldUseArrayListBuilderWithElementBuilders() {
+ return usingArrayListBuilderWithElementBuilders == ENABLED;
+ }
+
+ public boolean shouldUseHashSetBuilder() {
+ return usingHashSetBuilder == ENABLED;
+ }
+
+ public boolean shouldUseHashSetBuilderWithElementBuilders() {
+ return usingHashSetBuilderWithElementBuilders == ENABLED;
+ }
+
+ public boolean shouldUseHashMapBuilder() {
+ return usingHashMapBuilder == ENABLED;
+ }
+
+ public boolean shouldGenerateUnboxedOptional() {
+ return generateUnboxedOptional == ENABLED;
+ }
+
+ public boolean shouldUseGeneratedAnnotation() {
+ return usingGeneratedAnnotation == ENABLED;
+ }
+
+ public boolean shouldUseBuilderImplementationAnnotation() {
+ return usingBuilderImplementationAnnotation == ENABLED;
+ }
+
+ public boolean shouldImplementBuilderBase() {
+ return implementsBuilderBase == ENABLED;
+ }
+
+ // === String accessors ===
+ public AccessModifier getBuilderAccess() {
+ return builderAccess;
+ }
+
+ public AccessModifier getBuilderConstructorAccess() {
+ return builderConstructorAccess;
+ }
+
+ public AccessModifier getMethodAccess() {
+ return methodAccess;
+ }
+
+ public String getBuilderSuffix() {
+ return builderSuffix;
+ }
+
+ public String getSetterSuffix() {
+ return setterSuffix;
+ }
+
+ /**
+ * 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(mergeOptionState(other.generateFieldSupplier, this.generateFieldSupplier))
+ .generateConsumer(mergeOptionState(other.generateFieldConsumer, this.generateFieldConsumer))
+ .generateBuilderConsumer(
+ mergeOptionState(other.generateBuilderConsumer, this.generateBuilderConsumer))
+ .generateConditionalLogic(
+ mergeOptionState(other.generateConditionalHelper, this.generateConditionalHelper))
+ .builderAccess(mergeAccessModifier(other.builderAccess, this.builderAccess))
+ .builderConstructorAccess(
+ mergeAccessModifier(other.builderConstructorAccess, this.builderConstructorAccess))
+ .methodAccess(mergeAccessModifier(other.methodAccess, this.methodAccess))
+ .generateVarArgsHelpers(
+ mergeOptionState(other.generateVarArgsHelpers, this.generateVarArgsHelpers))
+ .generateStringFormatHelpers(
+ mergeOptionState(other.generateStringFormatHelpers, this.generateStringFormatHelpers))
+ .generateUnboxedOptional(
+ mergeOptionState(other.generateUnboxedOptional, this.generateUnboxedOptional))
+ .usingArrayListBuilder(
+ mergeOptionState(other.usingArrayListBuilder, this.usingArrayListBuilder))
+ .usingArrayListBuilderWithElementBuilders(
+ mergeOptionState(
+ other.usingArrayListBuilderWithElementBuilders,
+ this.usingArrayListBuilderWithElementBuilders))
+ .usingHashSetBuilder(mergeOptionState(other.usingHashSetBuilder, this.usingHashSetBuilder))
+ .usingHashSetBuilderWithElementBuilders(
+ mergeOptionState(
+ other.usingHashSetBuilderWithElementBuilders,
+ this.usingHashSetBuilderWithElementBuilders))
+ .usingHashMapBuilder(mergeOptionState(other.usingHashMapBuilder, this.usingHashMapBuilder))
+ .usingGeneratedAnnotation(
+ mergeOptionState(other.usingGeneratedAnnotation, this.usingGeneratedAnnotation))
+ .usingBuilderImplementationAnnotation(
+ mergeOptionState(
+ other.usingBuilderImplementationAnnotation,
+ this.usingBuilderImplementationAnnotation))
+ .implementsBuilderBase(
+ mergeOptionState(other.implementsBuilderBase, this.implementsBuilderBase))
+ .generateWithInterface(
+ 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.
+ *
+ *
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)
+ * @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);
+
+ if (generateFieldSupplier != UNSET) {
+ builder.append("generateFieldSupplier", generateFieldSupplier);
+ }
+ if (generateFieldConsumer != UNSET) {
+ builder.append("generateFieldConsumer", generateFieldConsumer);
+ }
+ if (generateBuilderConsumer != UNSET) {
+ builder.append("generateBuilderConsumer", generateBuilderConsumer);
+ }
+ 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 (builderSuffix != null && !builderSuffix.equals("Builder")) {
+ builder.append("builderSuffix", builderSuffix);
+ }
+ if (setterSuffix != null && !setterSuffix.isEmpty()) {
+ builder.append("setterSuffix", setterSuffix);
+ }
+
+ return builder.toString();
+ }
+
+ // === Builder Pattern ===
+ // All defaults are DEFAULT to allow proper three-state resolution
+ public static class Builder {
+ // === Field Setter Generation ===
+ private OptionState generateFieldSupplier = OptionState.UNSET;
+ private OptionState generateFieldConsumer = OptionState.UNSET;
+ private OptionState generateBuilderConsumer = OptionState.UNSET;
+
+ // === Conditional Logic ===
+ private OptionState generateConditionalHelper = OptionState.UNSET;
+
+ // === Access Control ===
+ private AccessModifier builderAccess = AccessModifier.DEFAULT;
+ private AccessModifier builderConstructorAccess = AccessModifier.DEFAULT;
+ private AccessModifier methodAccess = AccessModifier.DEFAULT;
+
+ // === 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;
+ private OptionState usingHashSetBuilder = OptionState.UNSET;
+ private OptionState usingHashSetBuilderWithElementBuilders = OptionState.UNSET;
+ private OptionState usingHashMapBuilder = OptionState.UNSET;
+
+ // === Annotations ===
+ private OptionState usingGeneratedAnnotation = OptionState.UNSET;
+ private OptionState usingBuilderImplementationAnnotation = OptionState.UNSET;
+
+ // === Integration ===
+ private OptionState implementsBuilderBase = OptionState.UNSET;
+ private OptionState generateWithInterface = OptionState.UNSET;
+
+ // === Naming ===
+ private String builderSuffix = null;
+ private String setterSuffix = null;
+
+ // === Setters ===
+ public Builder generateSupplier(OptionState value) {
+ this.generateFieldSupplier = value;
+ return this;
+ }
+
+ public Builder generateSupplier(boolean value) {
+ this.generateFieldSupplier = value ? ENABLED : DISABLED;
+ return this;
+ }
+
+ public Builder generateConsumer(OptionState value) {
+ this.generateFieldConsumer = value;
+ return this;
+ }
+
+ public Builder generateConsumer(boolean value) {
+ this.generateFieldConsumer = value ? ENABLED : DISABLED;
+ return this;
+ }
+
+ public Builder generateBuilderConsumer(OptionState value) {
+ this.generateBuilderConsumer = value;
+ return this;
+ }
+
+ public Builder generateBuilderConsumer(boolean value) {
+ this.generateBuilderConsumer = value ? ENABLED : DISABLED;
+ return this;
+ }
+
+ public Builder generateConditionalLogic(OptionState value) {
+ this.generateConditionalHelper = value;
+ return this;
+ }
+
+ public Builder generateConditionalLogic(boolean value) {
+ this.generateConditionalHelper = value ? ENABLED : DISABLED;
+ return this;
+ }
+
+ public Builder generateWithInterface(OptionState value) {
+ this.generateWithInterface = value;
+ return this;
+ }
+
+ public Builder generateWithInterface(boolean value) {
+ this.generateWithInterface = value ? ENABLED : DISABLED;
+ return this;
+ }
+
+ public Builder generateVarArgsHelpers(OptionState value) {
+ this.generateVarArgsHelpers = value;
+ return this;
+ }
+
+ public Builder generateVarArgsHelpers(boolean value) {
+ this.generateVarArgsHelpers = value ? ENABLED : DISABLED;
+ 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;
+ }
+
+ public Builder generateUnboxedOptional(boolean value) {
+ this.generateUnboxedOptional = value ? ENABLED : DISABLED;
+ return this;
+ }
+
+ public Builder usingArrayListBuilder(OptionState value) {
+ this.usingArrayListBuilder = value;
+ return this;
+ }
+
+ public Builder usingArrayListBuilder(boolean value) {
+ this.usingArrayListBuilder = value ? ENABLED : DISABLED;
+ return this;
+ }
+
+ public Builder usingArrayListBuilderWithElementBuilders(OptionState value) {
+ this.usingArrayListBuilderWithElementBuilders = value;
+ return this;
+ }
+
+ public Builder usingArrayListBuilderWithElementBuilders(boolean value) {
+ this.usingArrayListBuilderWithElementBuilders = value ? ENABLED : DISABLED;
+ return this;
+ }
+
+ public Builder usingHashSetBuilder(OptionState value) {
+ this.usingHashSetBuilder = value;
+ return this;
+ }
+
+ public Builder usingHashSetBuilder(boolean value) {
+ this.usingHashSetBuilder = value ? ENABLED : DISABLED;
+ return this;
+ }
+
+ public Builder usingHashSetBuilderWithElementBuilders(OptionState value) {
+ this.usingHashSetBuilderWithElementBuilders = value;
+ return this;
+ }
+
+ public Builder usingHashSetBuilderWithElementBuilders(boolean value) {
+ this.usingHashSetBuilderWithElementBuilders = value ? ENABLED : DISABLED;
+ return this;
+ }
+
+ public Builder usingHashMapBuilder(OptionState value) {
+ this.usingHashMapBuilder = value;
+ return this;
+ }
+
+ public Builder usingHashMapBuilder(boolean value) {
+ this.usingHashMapBuilder = value ? ENABLED : DISABLED;
+ 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 usingBuilderImplementationAnnotation(OptionState value) {
+ this.usingBuilderImplementationAnnotation = value;
+ return this;
+ }
+
+ public Builder usingBuilderImplementationAnnotation(boolean value) {
+ this.usingBuilderImplementationAnnotation = value ? ENABLED : DISABLED;
+ 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;
+ }
+
+ public Builder builderAccess(String value) {
+ this.builderAccess = AccessModifier.valueOf(value.toUpperCase());
+ 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;
+ }
+
+ public Builder methodAccess(String value) {
+ this.methodAccess = AccessModifier.valueOf(value.toUpperCase());
+ return this;
+ }
+
+ public Builder builderSuffix(String value) {
+ this.builderSuffix = value == null ? null : value.trim();
+ return this;
+ }
+
+ public Builder setterSuffix(String value) {
+ this.setterSuffix = value == null ? null : value.trim();
+ return this;
+ }
+
+ public BuilderConfiguration build() {
+ return new BuilderConfiguration(
+ generateFieldSupplier,
+ generateFieldConsumer,
+ generateBuilderConsumer,
+ generateConditionalHelper,
+ builderAccess,
+ builderConstructorAccess,
+ methodAccess,
+ generateVarArgsHelpers,
+ generateStringFormatHelpers,
+ generateUnboxedOptional,
+ usingArrayListBuilder,
+ usingArrayListBuilderWithElementBuilders,
+ usingHashSetBuilder,
+ usingHashSetBuilderWithElementBuilders,
+ usingHashMapBuilder,
+ usingGeneratedAnnotation,
+ usingBuilderImplementationAnnotation,
+ implementsBuilderBase,
+ generateWithInterface,
+ builderSuffix,
+ setterSuffix);
+ }
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+}
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/enums/CompilerArgumentsEnum.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java
new file mode 100644
index 00000000..bda79262
--- /dev/null
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java
@@ -0,0 +1,166 @@
+/*
+ * 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:
+ *
+ *
+ * Annotation methods in {@code SimpleBuilder.Options}
+ * Compiler options (with {@code -A} prefix)
+ * Configuration resolution
+ *
+ *
+ * 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 consumer generation. */
+ GENERATE_FIELD_CONSUMER("generateFieldConsumer"),
+
+ /** Option for builder consumer generation. */
+ GENERATE_BUILDER_CONSUMER("generateBuilderConsumer"),
+
+ // === Conditional Logic ===
+ /** Option for conditional helper generation. */
+ GENERATE_CONDITIONAL_HELPER("generateConditionalHelper"),
+
+ // === Access Control ===
+ /** 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"),
+
+ // === Collection Options ===
+ /** 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"),
+
+ /** 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"),
+
+ // === Annotations ===
+ /** Option for using Generated annotation. */
+ USING_GENERATED_ANNOTATION("usingGeneratedAnnotation"),
+
+ /** Option for using BuilderImplementation annotation. */
+ USING_BUILDER_IMPLEMENTATION_ANNOTATION("usingBuilderImplementationAnnotation"),
+
+ // === Integration ===
+ /** Option for implementing IBuilderBase interface. */
+ IMPLEMENTS_BUILDER_BASE("implementsBuilderBase"),
+
+ /** Option for With interface generation. */
+ GENERATE_WITH_INTERFACE("generateWithInterface"),
+
+ // === Naming ===
+ /** 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");
+
+ /** 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;
+ }
+}
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..95e48feb
--- /dev/null
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java
@@ -0,0 +1,397 @@
+/*
+ * 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.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;
+import org.javahelpers.simple.builders.processor.exceptions.BuilderException;
+
+/**
+ * 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:
+ *
+ *
+ * {@code @SimpleBuilder(options = ...)} inline options (highest priority)
+ * Custom template annotations (e.g., {@code @CustomBuilder})
+ * Global compiler arguments
+ * Built-in defaults (lowest priority)
+ *
+ *
+ * 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;
+
+ /**
+ * 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, ProcessingLogger logger, Elements elementUtils) {
+ this.globalConfiguration = globalConfiguration;
+ this.logger = logger;
+ this.elementUtils = elementUtils;
+ }
+
+ /**
+ * Reads builder configuration from {@code @SimpleBuilder(options = ...)} inline options.
+ *
+ *
Returns null if the element has no {@code @SimpleBuilder} annotation.
+ *
+ * @param element the annotated element to analyze
+ * @return configuration from the inline options, or null if not present
+ */
+ public BuilderConfiguration readFromInlineOptions(Element element) {
+ AnnotationMirror simpleBuilderMirror =
+ extractAnnotationMirror(element, SIMPLE_BUILDER_ANNOTATION);
+ 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;
+ }
+
+ /**
+ * 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;
+ }
+
+ // Find the 'options' attribute
+ AnnotationMirror optionsMirror = null;
+ Map extends ExecutableElement, ? extends AnnotationValue> elementValues =
+ annotationMirror.getElementValues();
+
+ for (Map.Entry extends ExecutableElement, ? extends AnnotationValue> 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 extends ExecutableElement, ? extends AnnotationValue> values =
+ optionsMirror.getElementValues();
+
+ BuilderConfiguration.Builder builder = BuilderConfiguration.builder();
+
+ for (Map.Entry extends ExecutableElement, ? extends AnnotationValue> 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());
+ default ->
+ logger.warning(
+ "Unknown configuration option '%s' with value '%s' - ignoring", name, value);
+ }
+ }
+
+ 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
+ * @return configuration from the template annotation, or null if not present
+ */
+ public BuilderConfiguration readFromTemplate(Element element) {
+ // 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()) {
+ BuilderConfiguration templateConfig = checkForTemplateAnnotation(mirror, element);
+ if (templateConfig != null) {
+ return templateConfig;
+ }
+ }
+
+ 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()) {
+ 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
+ return metaAnnotationName.equals(SIMPLE_BUILDER_TEMPLATE_ANNOTATION)
+ || metaAnnotationName.equals(SIMPLE_BUILDER_TEMPLATE_ANNOTATION_ALT);
+ }
+
+ /**
+ * Extracts configuration from @SimpleBuilder.Template(options = ...) using AnnotationMirror.
+ * Fallback for same-round compiled templates where reflection doesn't work.
+ */
+ private BuilderConfiguration extractOptionsFromTemplateMirror(AnnotationMirror templateMirror) {
+ Map extends ExecutableElement, ? extends AnnotationValue> templateValues =
+ elementUtils.getElementValuesWithDefaults(templateMirror);
+
+ for (Map.Entry extends ExecutableElement, ? extends AnnotationValue> entry :
+ templateValues.entrySet()) {
+ if (entry.getKey().getSimpleName().toString().equals("options")) {
+ Object value = entry.getValue().getValue();
+ if (value instanceof AnnotationMirror optionsMirror) {
+ return parseOptionsFromMirror(optionsMirror);
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Resolves the complete builder configuration for an element by chaining all configuration
+ * sources in priority order.
+ *
+ *
Priority chain (highest to lowest):
+ *
+ *
+ * {@code @SimpleBuilder(options = ...)} inline options (highest priority)
+ * Custom template annotations (only if {@code @SimpleBuilder} not present)
+ * Global compiler arguments
+ * 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
+ * @return the fully resolved configuration with all sources merged
+ */
+ public BuilderConfiguration resolveConfiguration(Element element) throws BuilderException {
+ 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);
+
+ // 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 static 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/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..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
@@ -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.getConfiguration().shouldGenerateWithInterface()) {
+ NestedTypeDto withInterface = createWithInterface(result, context);
+ result.addNestedType(withInterface);
+ }
return result;
}
@@ -118,11 +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.getConfiguration().getBuilderSuffix();
+ result.setBuilderTypeName(new TypeName(packageName, simpleClassName + builderSuffix));
result.setBuildingTargetTypeName(new TypeName(packageName, simpleClassName));
+ result.setConfiguration(context.getConfiguration());
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);
@@ -267,20 +271,28 @@ 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
- if (isString(field.getFieldType()) && !(field.getFieldType() instanceof TypeNameArray)) {
+ if (isString(field.getFieldType())
+ && !(field.getFieldType() instanceof TypeNameArray)
+ && context.getConfiguration().shouldGenerateStringFormatHelpers()) {
String fieldName = field.getFieldNameEstimated();
- field.addMethod(
+ MethodDto method =
createStringFormatMethodWithTransform(
fieldName,
fieldNameInBuilder,
fieldJavaDoc,
"String.format(format, args)",
annotations,
- builderType));
+ builderType,
+ context);
+ field.addMethod(method);
}
if ((field.getFieldType() instanceof TypeNameArray arrayType)) {
@@ -289,15 +301,24 @@ 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));
-
- // Add Consumer> method
- TypeName collectionBuilderType = map2TypeName(ArrayListBuilder.class);
- field.addMethod(
- createFieldConsumerWithArrayBuilder(
- fieldName, fieldNameInBuilder, collectionBuilderType, elementType, builderType));
+ fieldName, fieldNameInBuilder, listType, elementType, builderType, context);
+ field.addMethod(method1);
+
+ // Add Consumer> method only if builder consumers are enabled
+ if (context.getConfiguration().shouldGenerateBuilderConsumer()) {
+ TypeName collectionBuilderType = map2TypeName(ArrayListBuilder.class);
+ MethodDto method2 =
+ createFieldConsumerWithArrayBuilder(
+ fieldName,
+ fieldNameInBuilder,
+ collectionBuilderType,
+ elementType,
+ builderType,
+ context);
+ field.addMethod(method2);
+ }
return;
}
@@ -309,62 +330,85 @@ 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.getConfiguration().shouldGenerateVarArgsHelpers()) {
+ String fieldName = field.getFieldNameEstimated();
+ MethodDto method =
+ createFieldSetterWithTransform(
+ fieldName,
+ fieldNameInBuilder,
+ fieldJavaDoc,
+ "List.of(%s)",
+ new TypeNameArray(innerTypes.get(0), false),
+ builderType,
+ context);
+ field.addMethod(method);
+ }
} 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.getConfiguration().shouldGenerateVarArgsHelpers()) {
+ String fieldName = field.getFieldNameEstimated();
+ MethodDto method =
+ createFieldSetterWithTransform(
+ fieldName,
+ fieldNameInBuilder,
+ fieldJavaDoc,
+ "Set.of(%s)",
+ new TypeNameArray(innerTypes.get(0), true),
+ builderType,
+ context);
+ field.addMethod(method);
+ }
} 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.getConfiguration().shouldGenerateVarArgsHelpers()) {
+ TypeName mapEntryType =
+ new TypeNameArray(
+ new TypeNameGeneric("java.util", "Map.Entry", innerTypes.get(0), innerTypes.get(1)),
+ false);
+ String fieldName = field.getFieldNameEstimated();
+ MethodDto method =
+ createFieldSetterWithTransform(
+ fieldName,
+ fieldNameInBuilder,
+ fieldJavaDoc,
+ "Map.ofEntries(%s)",
+ mapEntryType,
+ builderType,
+ context);
+ field.addMethod(method);
+ }
} 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.getConfiguration().shouldGenerateUnboxedOptional()) {
+ // Add setter that accepts the inner type T and wraps it in Optional.ofNullable()
+ MethodDto method =
+ createFieldSetterWithTransform(
+ fieldName,
+ fieldNameInBuilder,
+ fieldJavaDoc,
+ "Optional.ofNullable(%s)",
+ innerTypes.get(0),
+ builderType,
+ context);
+ field.addMethod(method);
+ }
// If Optional, add format method
TypeName innerType = innerTypes.get(0);
- if (isString(innerType)) {
- field.addMethod(
+ if (isString(innerType) && context.getConfiguration().shouldGenerateStringFormatHelpers()) {
+ MethodDto method =
createStringFormatMethodWithTransform(
fieldName,
fieldNameInBuilder,
fieldJavaDoc,
"Optional.of(String.format(format, args))",
List.of(),
- builderType));
+ builderType,
+ context);
+ field.addMethod(method);
}
}
}
@@ -375,7 +419,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;
}
@@ -387,9 +431,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,47 +443,78 @@ private static boolean tryAddBuilderConsumer(
VariableElement fieldParameter,
TypeName builderType,
ProcessingContext context) {
+ // Builder consumers are controlled by generateBuilderConsumer
+ if (!context.getConfiguration().shouldGenerateBuilderConsumer()) {
+ return false;
+ }
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.getFieldName(),
+ field.getJavaDoc(),
+ fieldBuilderType,
+ builderType,
+ context);
+ field.addMethod(method);
return true;
}
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.getConfiguration().shouldGenerateFieldConsumer()) {
+ return false;
+ }
if (!isJavaClass(field.getFieldType())
&& fieldTypeElement != null
&& fieldTypeElement.getKind() == javax.lang.model.element.ElementKind.CLASS
&& !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.getFieldName(),
+ field.getJavaDoc(),
+ field.getFieldType(),
+ builderType,
+ context);
+ field.addMethod(method);
return true;
}
return false;
}
/** 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 generateBuilderConsumer
+ if (!context.getConfiguration().shouldGenerateBuilderConsumer()) {
+ return false;
+ }
if (shouldGenerateStringBuilderConsumer(field.getFieldType())) {
String transform =
isOptionalString(field.getFieldType())
? "Optional.of(builder.toString())"
: "builder.toString()";
- field.addMethod(
+ MethodDto method =
createStringBuilderConsumer(
- field.getFieldName(), field.getJavaDoc(), transform, builderType));
+ field.getFieldName(),
+ field.getFieldName(),
+ field.getJavaDoc(),
+ transform,
+ builderType,
+ context);
+ field.addMethod(method);
return true;
}
return false;
@@ -466,36 +541,59 @@ private static boolean tryAddListConsumer(
Optional elementBuilderType =
resolveBuilderType(elementType, elementTypeMirror, context);
- if (elementBuilderType.isPresent()) {
- // Element type has a builder - use ArrayListBuilderWithElementBuilders
+ // Only generate builder consumer methods if enabled
+ if (!context.getConfiguration().shouldGenerateBuilderConsumer()) {
+ return false;
+ }
+
+ if (elementBuilderType.isPresent()
+ && context.getConfiguration().shouldUseArrayListBuilderWithElementBuilders()) {
+ // Element type has a builder - use ArrayListBuilderWithElementBuilders if enabled
TypeName collectionBuilderType =
new TypeNameGeneric(
map2TypeName(ArrayListBuilderWithElementBuilders.class),
elementType,
elementBuilderType.get());
- field.addMethod(
+ MethodDto method =
createFieldConsumerWithElementBuilders(
+ field.getFieldName(),
field.getFieldName(),
field.getJavaDoc(),
collectionBuilderType,
elementBuilderType.get(),
- builderType));
- } else {
- // Regular ArrayListBuilder
+ builderType,
+ context);
+ field.addMethod(method);
+ } else if (context.getConfiguration().shouldUseArrayListBuilder()) {
+ // Regular ArrayListBuilder if enabled
TypeName collectionBuilderType = map2TypeName(ArrayListBuilder.class);
- field.addMethod(
+ MethodDto method =
createFieldConsumerWithBuilder(
+ field.getFieldName(),
field.getFieldName(),
field.getJavaDoc(),
collectionBuilderType,
elementType,
- builderType));
+ builderType,
+ context);
+ field.addMethod(method);
+ } 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 builder consumers are enabled
+ if (!context.getConfiguration().shouldGenerateBuilderConsumer()) {
+ return false;
+ }
+ // Check if HashMapBuilder is enabled
+ if (!context.getConfiguration().shouldUseHashMapBuilder()) {
+ return false;
+ }
if (!(isMap(field.getFieldType())
&& field.getFieldType() instanceof TypeNameGeneric fieldTypeGeneric
&& fieldTypeGeneric.getInnerTypeArguments().size() == 2)) {
@@ -509,7 +607,12 @@ private static boolean tryAddMapConsumer(FieldDto field, TypeName builderType) {
fieldTypeGeneric.getInnerTypeArguments().get(1));
MethodDto mapConsumerWithBuilder =
BuilderDefinitionCreator.createFieldConsumerWithBuilder(
- field.getFieldName(), field.getJavaDoc(), builderTargetTypeName, builderType);
+ field.getFieldName(),
+ field.getFieldName(),
+ field.getJavaDoc(),
+ builderTargetTypeName,
+ builderType,
+ context);
field.addMethod(mapConsumerWithBuilder);
return true;
}
@@ -535,36 +638,57 @@ private static boolean tryAddSetConsumer(
Optional elementBuilderType =
resolveBuilderType(elementType, elementTypeMirror, context);
- if (elementBuilderType.isPresent()) {
- // Element type has a builder - use HashSetBuilderWithElementBuilders
+ // Only generate builder consumer methods if enabled
+ if (!context.getConfiguration().shouldGenerateBuilderConsumer()) {
+ return false;
+ }
+
+ if (elementBuilderType.isPresent()
+ && context.getConfiguration().shouldUseHashSetBuilderWithElementBuilders()) {
+ // Element type has a builder - use HashSetBuilderWithElementBuilders if enabled
TypeName collectionBuilderType =
new TypeNameGeneric(
map2TypeName(HashSetBuilderWithElementBuilders.class),
elementType,
elementBuilderType.get());
- field.addMethod(
+ MethodDto method =
createFieldConsumerWithElementBuilders(
+ field.getFieldName(),
field.getFieldName(),
field.getJavaDoc(),
collectionBuilderType,
elementBuilderType.get(),
- builderType));
- } else {
- // Regular HashSetBuilder
+ builderType,
+ context);
+ field.addMethod(method);
+ } else if (context.getConfiguration().shouldUseHashSetBuilder()) {
+ // Regular HashSetBuilder if enabled
TypeName collectionBuilderType = map2TypeName(HashSetBuilder.class);
- field.addMethod(
+ MethodDto method =
createFieldConsumerWithBuilder(
+ field.getFieldName(),
field.getFieldName(),
field.getJavaDoc(),
collectionBuilderType,
elementType,
- builderType));
+ builderType,
+ context);
+ field.addMethod(method);
+ } else {
+ return false;
}
return true;
}
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.getConfiguration().shouldGenerateFieldSupplier()) {
+ return;
+ }
// Skip supplier generation for functional interfaces
if (isFunctionalInterface(fieldTypeElement)) {
return;
@@ -572,9 +696,15 @@ 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,
+ context);
+ field.addMethod(method);
}
private static Optional createFieldFromSetter(
@@ -765,14 +895,22 @@ 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,
+ context);
+ field.addMethod(method);
// Add consumer/supplier/helper methods - use ORIGINAL field name for method names
addConsumerMethodsForField(field, param, fieldTypeElement, builderType, context);
- addSupplierMethodsForField(field, fieldTypeElement, builderType);
- addAdditionalHelperMethodsForField(field, annotations, builderType);
+ addSupplierMethodsForField(field, fieldTypeElement, builderType, context);
+ addAdditionalHelperMethodsForField(field, annotations, builderType, context);
return Optional.of(field);
}
@@ -793,9 +931,17 @@ private static MethodDto createFieldSetterWithTransform(
String fieldJavadoc,
String transform,
TypeName fieldType,
- TypeName builderType) {
+ TypeName builderType,
+ ProcessingContext context) {
return createFieldSetterWithTransform(
- fieldName, fieldNameInBuilder, fieldJavadoc, transform, fieldType, List.of(), builderType);
+ fieldName,
+ fieldNameInBuilder,
+ fieldJavadoc,
+ transform,
+ fieldType,
+ List.of(),
+ builderType,
+ context);
}
/**
@@ -815,17 +961,18 @@ 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(generateSetterName(fieldName, context));
methodDto.setReturnType(builderType);
methodDto.addParameter(parameter);
- methodDto.setModifier(Modifier.PUBLIC);
+ setMethodAccessModifier(methodDto, getMethodAccessModifier(context));
String params;
if (StringUtils.isBlank(transform)) {
params = parameter.getParameterName();
@@ -855,16 +1002,21 @@ 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(generateSetterName(fieldName, context));
methodDto.setReturnType(builderType);
methodDto.addParameter(parameter);
- methodDto.setModifier(Modifier.PUBLIC);
+ setMethodAccessModifier(methodDto, getMethodAccessModifier(context));
methodDto.setCode(
"""
$helperType:T consumer = this.$fieldName:N.isSet() ? this.$fieldName:N.value() : new $helperType:T();
@@ -872,7 +1024,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);
@@ -889,7 +1041,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);
@@ -897,9 +1054,9 @@ private static MethodDto createStringBuilderConsumer(
parameter.setParameterName(fieldName + "StringBuilderConsumer");
parameter.setParameterTypeName(consumerType);
MethodDto methodDto = new MethodDto();
- methodDto.setMethodName(fieldName);
+ methodDto.setMethodName(generateSetterName(fieldName, context));
methodDto.addParameter(parameter);
- methodDto.setModifier(Modifier.PUBLIC);
+ setMethodAccessModifier(methodDto, getMethodAccessModifier(context));
methodDto.setCode(
"""
StringBuilder builder = new StringBuilder();
@@ -907,7 +1064,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);
@@ -926,29 +1083,40 @@ private static MethodDto createStringBuilderConsumer(
private static MethodDto createFieldConsumerWithBuilder(
String fieldName,
+ String fieldNameInBuilder,
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,
+ fieldNameInBuilder,
+ fieldJavadoc,
+ builderTypeGeneric,
+ returnBuilderType,
+ context);
}
private static MethodDto createFieldConsumerWithBuilder(
String fieldName,
+ String fieldNameInBuilder,
String fieldJavaDoc,
TypeName consumerBuilderType,
- TypeName returnBuilderType) {
+ TypeName returnBuilderType,
+ ProcessingContext context) {
return createFieldConsumerWithBuilder(
fieldName,
+ fieldNameInBuilder,
fieldJavaDoc,
consumerBuilderType,
"this.$fieldName:N.value()",
"",
Map.of(),
- returnBuilderType);
+ returnBuilderType,
+ context);
}
/**
@@ -957,18 +1125,22 @@ private static MethodDto createFieldConsumerWithBuilder(
*/
private static MethodDto createFieldConsumerWithElementBuilders(
String fieldName,
+ String fieldNameInBuilder,
String fieldJavaDoc,
TypeName collectionBuilderType,
TypeName elementBuilderType,
- TypeName returnBuilderType) {
+ TypeName returnBuilderType,
+ ProcessingContext context) {
return createFieldConsumerWithBuilder(
fieldName,
+ fieldNameInBuilder,
fieldJavaDoc,
collectionBuilderType,
"this.$fieldName:N.value(), $elementBuilderType:T::create",
"$elementBuilderType:T::create",
Map.of("elementBuilderType", elementBuilderType),
- returnBuilderType);
+ returnBuilderType,
+ context);
}
/**
@@ -981,25 +1153,28 @@ 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,
+ String fieldNameInBuilder,
String fieldJavaDoc,
TypeName consumerBuilderType,
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(generateSetterName(fieldName, context));
methodDto.setReturnType(returnBuilderType);
methodDto.addParameter(parameter);
- methodDto.setModifier(Modifier.PUBLIC);
+ setMethodAccessModifier(methodDto, getMethodAccessModifier(context));
methodDto.setCode(
"""
$helperType:T builder = this.$fieldName:N.isSet() ? new $helperType:T(%s) : new $helperType:T(%s);
@@ -1008,7 +1183,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);
@@ -1030,16 +1205,17 @@ 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(generateSetterName(fieldName, context));
methodDto.setReturnType(builderType);
methodDto.addParameter(parameter);
- methodDto.setModifier(Modifier.PUBLIC);
+ setMethodAccessModifier(methodDto, getMethodAccessModifier(context));
methodDto.setCode(
"""
this.$fieldName:N = $builderFieldWrapper:T.changedValue($dtoMethodParam:N.get());
@@ -1066,7 +1242,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();
@@ -1080,11 +1257,11 @@ private static MethodDto createStringFormatMethodWithTransform(
argsParam.setParameterTypeName(new TypeNameArray(TypeName.of(Object.class), false));
MethodDto methodDto = new MethodDto();
- methodDto.setMethodName(fieldName);
+ methodDto.setMethodName(generateSetterName(fieldName, context));
methodDto.setReturnType(builderType);
methodDto.addParameter(formatParam);
methodDto.addParameter(argsParam);
- methodDto.setModifier(Modifier.PUBLIC);
+ setMethodAccessModifier(methodDto, getMethodAccessModifier(context));
methodDto.setCode(
"""
this.$fieldName:N = $builderFieldWrapper:T.changedValue($transform:N);
@@ -1125,16 +1302,17 @@ 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(generateSetterName(fieldName, context));
methodDto.setReturnType(builderType);
methodDto.addParameter(parameter);
- methodDto.setModifier(Modifier.PUBLIC);
+ setMethodAccessModifier(methodDto, getMethodAccessModifier(context));
methodDto.setCode(
"""
this.$fieldName:N = $builderFieldWrapper:T.changedValue($dtoMethodParams:N.toArray(new $elementType:T[0]));
@@ -1165,7 +1343,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);
@@ -1175,11 +1354,10 @@ private static MethodDto createFieldConsumerWithArrayBuilder(
parameter.setParameterTypeName(consumerType);
MethodDto methodDto = new MethodDto();
- methodDto.setMethodName(fieldName);
+ methodDto.setMethodName(generateSetterName(fieldName, context));
methodDto.setReturnType(returnBuilderType);
methodDto.addParameter(parameter);
- methodDto.setModifier(Modifier.PUBLIC);
-
+ 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();
@@ -1292,10 +1470,11 @@ private static Optional resolveBuilderTypeFromTypeElement(
String packageName = context.getPackageName(typeElement);
String simpleClassName = typeElement.getSimpleName().toString();
+ String builderSuffix = context.getConfiguration().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));
}
/**
@@ -1433,4 +1612,52 @@ private static MethodDto createWithBuilderMethod(BuilderDefinitionDto builderDef
return method;
}
+
+ /**
+ * 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.
+ *
+ *
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 generateSetterName(String fieldName, ProcessingContext context) {
+ String suffix = context.getConfiguration().getSetterSuffix();
+ if (suffix == null || suffix.isEmpty()) {
+ return fieldName;
+ }
+ return suffix + StringUtils.capitalize(fieldName);
+ }
+
+ /**
+ * 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.getConfiguration().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
new file mode 100644
index 00000000..2b6747b8
--- /dev/null
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java
@@ -0,0 +1,173 @@
+/*
+ * 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.equalsAny(value, "package-private", "package_private")) {
+ return AccessModifier.PACKAGE_PRIVATE;
+ } else {
+ return AccessModifier.DEFAULT;
+ }
+ }
+
+ /**
+ * Reads a complete BuilderConfiguration from compiler arguments.
+ *
+ *
This method reads all configuration options from compiler arguments like:
+ *
+ *
+ * {@code -Asimplebuilder.generateFieldSupplier=true}
+ * {@code -Asimplebuilder.builderAccess=public}
+ * etc.
+ *
+ *
+ * All values default to UNSET or DEFAULT if not specified in compiler arguments.
+ *
+ * @return a BuilderConfiguration with values read from compiler arguments
+ */
+ public BuilderConfiguration readBuilderConfiguration() {
+ return BuilderConfiguration.builder()
+ .generateSupplier(readOptionState(CompilerArgumentsEnum.GENERATE_FIELD_SUPPLIER))
+ .generateConsumer(readOptionState(CompilerArgumentsEnum.GENERATE_FIELD_CONSUMER))
+ .generateBuilderConsumer(readOptionState(CompilerArgumentsEnum.GENERATE_BUILDER_CONSUMER))
+ .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(
+ readOptionState(CompilerArgumentsEnum.GENERATE_STRING_FORMAT_HELPERS))
+ .generateUnboxedOptional(readOptionState(CompilerArgumentsEnum.GENERATE_UNBOXED_OPTIONAL))
+ .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))
+ .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))
+ .builderSuffix(readValue(CompilerArgumentsEnum.BUILDER_SUFFIX))
+ .setterSuffix(readValue(CompilerArgumentsEnum.SETTER_SUFFIX))
+ .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 bbbcad9c..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
@@ -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;
@@ -104,14 +103,30 @@ 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));
- // Adding Constructors for builder
+ // 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));
+ }
+
+ // Get access modifiers from configuration
+ Modifier constructorAccessModifier =
+ map2Modifier(builderDef.getConfiguration().getBuilderConstructorAccess());
+ Modifier methodAccessModifier = map2Modifier(builderDef.getConfiguration().getMethodAccess());
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",
@@ -154,18 +169,27 @@ 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,
dtoTypeName,
builderDef.getConstructorFieldsForBuilder(),
builderDef.getSetterFieldsForBuilder(),
- builderDef.getGenerics()));
+ builderDef.getGenerics(),
+ builderDef.getConfiguration().shouldImplementBuilderBase(),
+ PUBLIC));
classBuilder.addMethod(
createMethodStaticCreate(
- builderBaseClass, builderTypeName, dtoBaseClass, builderDef.getGenerics()));
- classBuilder.addMethod(createMethodConditional(builderTypeName));
- classBuilder.addMethod(createMethodConditionalPositiveOnly(builderTypeName));
+ builderBaseClass, builderTypeName, dtoBaseClass, builderDef.getGenerics(), PUBLIC));
+
+ // Add conditional methods only if enabled in configuration
+ if (builderDef.getConfiguration().shouldGenerateConditionalLogic()) {
+ classBuilder.addMethod(createMethodConditional(builderTypeName, methodAccessModifier));
+ classBuilder.addMethod(
+ createMethodConditionalPositiveOnly(builderTypeName, methodAccessModifier));
+ }
// Adding nested types (e.g., With interface)
for (NestedTypeDto nestedType : builderDef.getNestedTypes()) {
@@ -175,8 +199,12 @@ public void generateBuilder(BuilderDefinitionDto builderDef) throws BuilderExcep
}
// Adding annotations
- classBuilder.addAnnotation(createAnnotationGenerated());
- classBuilder.addAnnotation(createAnnotationBuilderImplementation(dtoBaseClass));
+ if (builderDef.getConfiguration().shouldUseGeneratedAnnotation()) {
+ classBuilder.addAnnotation(createAnnotationGenerated());
+ }
+ if (builderDef.getConfiguration().shouldUseBuilderImplementationAnnotation()) {
+ classBuilder.addAnnotation(createAnnotationBuilderImplementation(dtoBaseClass));
+ }
logger.debug(
"Writing builder class to file: %s.%s",
@@ -204,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<>();
@@ -254,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)
+ .toList();
}
private CodeBlock createJavadocForClass(ClassName dtoClass) {
@@ -280,10 +312,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}.
@@ -294,10 +326,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(
"""
@@ -360,12 +395,18 @@ private MethodSpec createMethodBuild(
com.palantir.javapoet.TypeName returnType,
List constructorFields,
List setterFields,
- List generics) {
- MethodSpec.Builder mb =
- MethodSpec.methodBuilder("build")
- .addModifiers(PUBLIC)
- .returns(returnType)
- .addAnnotation(Override.class);
+ List generics,
+ 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) {
+ 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)
@@ -429,21 +470,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 {
@@ -455,10 +500,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(
@@ -470,7 +519,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
@@ -485,15 +534,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(
@@ -507,19 +559,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());
@@ -535,9 +580,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);
}
@@ -545,11 +589,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 =
@@ -563,23 +608,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();
@@ -588,6 +627,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/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..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
@@ -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,18 @@ 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 PRIVATE -> javax.lang.model.element.Modifier.PRIVATE;
+ case PACKAGE_PRIVATE -> null; // Package-private has no explicit modifier
+ };
+ }
}
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..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
@@ -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,8 @@ public final class ProcessingContext {
private final Elements elementUtils;
private final Types typeUtils;
private final ProcessingLogger logger;
+ private final BuilderConfigurationReader configurationReader;
+ private BuilderConfiguration configurationForProcessingTarget;
/**
* Creates a new processing context.
@@ -49,11 +52,30 @@ 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.configurationReader =
+ new BuilderConfigurationReader(globalConfiguration, logger, elementUtils);
+ }
+
+ public void initConfigurationForProcessingTarget(BuilderConfiguration config) {
+ this.configurationForProcessingTarget = config;
+ }
+
+ public BuilderConfiguration getConfiguration() {
+ return this.configurationForProcessingTarget;
+ }
+
+ public BuilderConfigurationReader getConfigurationReader() {
+ return configurationReader;
}
/**
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
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..53120c6a
--- /dev/null
+++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderConfigurationReaderTest.java
@@ -0,0 +1,654 @@
+/*
+ * 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 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 inline @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(options = @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 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(");
+
+ // 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 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");
+
+ // 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() {
+ JavaFileObject dtoSource =
+ ProcessorTestUtils.forSource(
+ """
+ package test;
+ import org.javahelpers.simple.builders.processor.testing.MyBuliderForTestAnnotation;
+
+ @MyBuliderForTestAnnotation
+ 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(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: 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).
+ *
+ *
Verifies BuilderConfigurationReader.resolveConfiguration() applies correct priority: Options
+ * > Template > Compiler args > Defaults
+ */
+ @Test
+ void resolveConfiguration_OptionsOverridesTemplate_AppliesPriorityCorrectly() {
+ JavaFileObject dtoSource =
+ ProcessorTestUtils.forSource(
+ """
+ package test;
+ import org.javahelpers.simple.builders.core.annotations.SimpleBuilder;
+ import org.javahelpers.simple.builders.processor.testing.MyBuliderForTestAnnotation;
+
+ @MyBuliderForTestAnnotation
+ @SimpleBuilder(options = @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(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(options = @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 optional features disabled through mixed configuration layers.
+ *
+ * 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 - ALL optional features disabled:
+ *
+ *
+ * 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)
+ *
+ */
+ @Test
+ void resolveConfiguration_AllLayersTogether_CompleteChain() {
+ 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;
+
+ @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,
+ usingGeneratedAnnotation = OptionState.DISABLED,
+ usingBuilderImplementationAnnotation = OptionState.DISABLED,
+ builderSuffix = "MinimalBuilder",
+ 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 with compiler args that disable additional features
+ Compilation compilation =
+ ProcessorTestUtils.createCompiler()
+ .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, "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 org.javahelpers.simple.builders.core.interfaces.IBuilderBase;
+ import org.javahelpers.simple.builders.core.util.TrackedValue;
+
+ /**
+ * Builder for {@code test.PersonDto}.
+ */
+ 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 name.
+ *
+ * @param name name
+ * @return current instance of builder
+ */
+ public PersonDtoMinimalBuilder withName(String name) {
+ this.name = changedValue(name);
+ return this;
+ }
+
+ /**
+ * Sets the value for tags.
+ *
+ * @param tags tags
+ * @return current instance of builder
+ */
+ public PersonDtoMinimalBuilder withTags(List tags) {
+ this.tags = changedValue(tags);
+ 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.");
+ }
+
+ /**
+ * 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();
+ }
+}
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/CompilerArgumentsReaderTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/CompilerArgumentsReaderTest.java
new file mode 100644
index 00000000..0edef695
--- /dev/null
+++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/CompilerArgumentsReaderTest.java
@@ -0,0 +1,413 @@
+/*
+ * 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" and "enabled". */
+ @ParameterizedTest
+ @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);
+
+ 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/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, ConsumerThese 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:
+ *
+ *
+ * BuilderConfiguration record parameters
+ * BuilderConfiguration.Builder
+ * 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:
+ *
+ *
+ * Add the parameter to BuilderConfiguration record
+ * Add builder methods in BuilderConfiguration.Builder
+ * Update DEFAULT configuration
+ * 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)
+ .generateConsumer(OptionState.ENABLED)
+ .generateBuilderConsumer(OptionState.ENABLED)
+ // Conditional logic
+ .generateConditionalLogic(OptionState.ENABLED)
+ // Access control
+ .builderAccess(AccessModifier.PACKAGE_PRIVATE)
+ .builderConstructorAccess(AccessModifier.PRIVATE)
+ .methodAccess(AccessModifier.PACKAGE_PRIVATE)
+ // 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();
+
+ // 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.generateFieldConsumer());
+ assertEquals(OptionState.ENABLED, config.generateBuilderConsumer());
+ 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());
+ }
+
+ /**
+ * 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 and Address without
+ // builder
+ JavaFileObject nestedDto =
+ ProcessorTestUtils.simpleBuilderClass(
+ "test",
+ "NestedDto",
+ """
+ private String value;
+ public String getValue() { return value; }
+ 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",
+ "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;
+ private Address address;
+
+ 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; }
+
+ public Address getAddress() { return address; }
+ public void setAddress(Address address) { this.address = address; }
+ """);
+
+ // When: Compile with ALL compiler arguments disabled and custom builder suffix
+ Compilation compilation =
+ ProcessorTestUtils.createCompiler()
+ .withOptions(
+ "-Asimplebuilder.generateFieldSupplier=false",
+ "-Asimplebuilder.generateFieldConsumer=false",
+ "-Asimplebuilder.generateBuilderConsumer=false",
+ "-Asimplebuilder.generateConditionalHelper=false",
+ "-Asimplebuilder.builderAccess=PACKAGE_PRIVATE",
+ "-Asimplebuilder.builderConstructorAccess=PRIVATE",
+ "-Asimplebuilder.methodAccess=PACKAGE_PRIVATE",
+ "-Asimplebuilder.generateVarArgsHelpers=false",
+ "-Asimplebuilder.generateStringFormatHelpers=false",
+ "-Asimplebuilder.generateUnboxedOptional=false",
+ "-Asimplebuilder.usingArrayListBuilder=false",
+ "-Asimplebuilder.usingArrayListBuilderWithElementBuilders=false",
+ "-Asimplebuilder.usingHashSetBuilder=false",
+ "-Asimplebuilder.usingHashSetBuilderWithElementBuilders=false",
+ "-Asimplebuilder.usingHashMapBuilder=false",
+ "-Asimplebuilder.usingGeneratedAnnotation=false",
+ "-Asimplebuilder.usingBuilderImplementationAnnotation=false",
+ "-Asimplebuilder.implementsBuilderBase=false",
+ "-Asimplebuilder.generateWithInterface=false",
+ "-Asimplebuilder.builderSuffix=CustomBuilder",
+ "-Asimplebuilder.setterSuffix=with")
+ .compile(nestedDto, addressDto, source);
+
+ // Then: Compilation should succeed
+ assertThat(compilation).succeeded();
+
+ // And: Generated builder should contain only basic functionality
+ String generatedCode =
+ ProcessorTestUtils.loadGeneratedSource(compilation, "MinimalDtoCustomBuilder");
+
+ // With generateFieldSupplier=false, NO supplier methods should be generated
+ ProcessorAsserts.assertNotContaining(
+ generatedCode,
+ "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 withAddress(Consumer addressConsumer)",
+ "public MinimalDtoCustomBuilder withNested(Consumer nestedConsumer)",
+ "public MinimalDtoCustomBuilder withItems(Consumer> itemsConsumer)",
+ "public MinimalDtoCustomBuilder withTags(Consumer> tagsConsumer)",
+ "public MinimalDtoCustomBuilder withProperties(Consumer> propertiesConsumer)");
+
+ // With generateBuilderConsumer=false, NO builder consumer methods should be generated
+ // Builder consumers include: StringBuilder, collection builders, nested DTO builders
+ ProcessorAsserts.assertNotContaining(
+ generatedCode,
+ "public MinimalDtoCustomBuilder withNested(Consumer nestedBuilderConsumer)",
+ "public MinimalDtoCustomBuilder withName(Consumer nameStringBuilderConsumer)",
+ "public MinimalDtoCustomBuilder withDescription(Consumer descriptionStringBuilderConsumer)");
+
+ // With generateConditionalHelper=false, NO conditional methods
+ ProcessorAsserts.assertNotContaining(
+ generatedCode, "public MinimalDtoCustomBuilder conditional(BooleanSupplier condition");
+
+ // With generateVarArgsHelpers=false, NO VarArgs helpers should be generated
+ ProcessorAsserts.assertNotContaining(
+ generatedCode,
+ "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 withDescription(String description)");
+
+ // With generateStringFormatHelpers=false, NO String format methods should be generated
+ ProcessorAsserts.assertNotContaining(
+ generatedCode,
+ "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(");
+
+ // With usingBuilderImplementationAnnotation=false, NO @BuilderImplementation annotation should
+ // 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 builderAccess=PACKAGE_PRIVATE, builder class should NOT have public modifier
+ ProcessorAsserts.assertNotContaining(generatedCode, "public class MinimalDtoCustomBuilder");
+
+ // But package-private class should exist
+ ProcessorAsserts.assertContaining(generatedCode, "class MinimalDtoCustomBuilder");
+
+ // With methodAccess=PACKAGE_PRIVATE, methods should NOT have public modifier
+ ProcessorAsserts.assertNotContaining(
+ generatedCode, "public MinimalDtoCustomBuilder withName(String name)");
+
+ // But package-private methods should exist - with setterSuffix="with", methods are prefixed
+ ProcessorAsserts.assertContaining(
+ generatedCode,
+ "MinimalDtoCustomBuilder withName(String name)",
+ "MinimalDto build()",
+ "static MinimalDtoCustomBuilder create()");
+
+ // With usingArrayListBuilder=false AND generateBuilderConsumer=false, NO ArrayListBuilder
+ // should be used
+ ProcessorAsserts.assertNotContaining(
+ generatedCode,
+ "MinimalDtoCustomBuilder withItems(Consumer> itemsBuilderConsumer)");
+
+ // With usingHashSetBuilder=false AND generateBuilderConsumer=false, NO HashSetBuilder should be
+ // used
+ ProcessorAsserts.assertNotContaining(
+ generatedCode,
+ "MinimalDtoCustomBuilder withTags(Consumer> tagsBuilderConsumer)");
+
+ // With usingHashMapBuilder=false AND generateBuilderConsumer=false, NO HashMapBuilder should be
+ // used
+ ProcessorAsserts.assertNotContaining(
+ generatedCode,
+ "MinimalDtoCustomBuilder withProperties(Consumer> propertiesBuilderConsumer)");
+
+ // 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 withName(String name)",
+ "MinimalDtoCustomBuilder withItems(List items)",
+ "MinimalDtoCustomBuilder withProperties(Map properties)",
+ "MinimalDtoCustomBuilder withDescription(Optional description)",
+ "MinimalDtoCustomBuilder withTags(Set