From 2196f9c173af8e4399f8e77438e4cf0b20020ded Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Thu, 23 Apr 2026 23:04:11 +0200 Subject: [PATCH 01/23] Adding a testclass with expectations of the result --- .../processor/BuilderJavadocExampleTest.java | 461 ++++++++++++++++++ 1 file changed, 461 insertions(+) create mode 100644 processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderJavadocExampleTest.java diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderJavadocExampleTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderJavadocExampleTest.java new file mode 100644 index 00000000..d4ccf3a2 --- /dev/null +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderJavadocExampleTest.java @@ -0,0 +1,461 @@ +/* + * MIT License + * + * Copyright (c) 2026 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.javahelpers.simple.builders.processor.testing.ProcessorAsserts.assertGenerationSucceeded; +import static org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils.loadGeneratedSource; + +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; + +/** + * Tests that verify the field-specific code examples added to the generated builder javadoc. + * + *

Two levels are covered: + * + *

+ * + *

Assertions use exact javadoc text blocks (whitespace-normalized) so both the presence and the + * relative ordering of lines is verified in a single substring match. + */ +class BuilderJavadocExampleTest { + + protected Compilation compile(JavaFileObject... sourceFiles) { + return ProcessorTestUtils.createCompiler().compile(sourceFiles); + } + + @Test + void shouldGenerateClassJavadocExampleWithKitchenSinkChain() { + // Given: a DTO with fields covering basic setter, supplier and list helpers + String packageName = "test"; + String className = "BookDto"; + String builderClassName = className + "Builder"; + + JavaFileObject dto = + ProcessorTestUtils.simpleBuilderClass( + packageName, + className, + """ + private String title; + private int pages; + private java.util.List tags; + + public String getTitle() { return title; } + public void setTitle(String title) { this.title = title; } + public int getPages() { return pages; } + public void setPages(int pages) { this.pages = pages; } + public java.util.List getTags() { return tags; } + public void setTags(java.util.List tags) { this.tags = tags; } + """); + + // When + Compilation compilation = compile(dto); + + // Then + String generatedCode = loadGeneratedSource(compilation, builderClassName); + assertGenerationSucceeded(compilation, builderClassName, generatedCode); + + // The generated class javadoc must contain the full kitchen-sink chain, + // with fields in DTO declaration order (title, pages, tags) and within each + // field the generator lines in priority order (BasicSetter=100, Supplier=60, + // ListConsumer=53, AddToCollection=30). + ProcessorAsserts.assertContaining( + generatedCode, + """ + *

Example:

+ *
{@code
+        * BookDto result = BookDtoBuilder.create()
+        *     .title("example value")
+        *     .titleSupplier(() -> "example value")
+        *     .pages(42)
+        *     .pagesSupplier(() -> 42)
+        *     .tags(List.of("example value"))
+        *     .tagsSupplier(() -> List.of("example value"))
+        *     .tags(t -> t.add("example value"))
+        *     .add2Tags("example value")
+        *     .build();
+        * }
+ """); + } + + @Test + void shouldGenerateMethodJavadocExampleForBasicStringSetter() { + String packageName = "test"; + String className = "Person"; + String builderClassName = className + "Builder"; + + JavaFileObject dto = + ProcessorTestUtils.simpleBuilderClass( + packageName, + className, + """ + private String teamname; + public String getTeamname() { return teamname; } + public void setTeamname(String teamname) { this.teamname = teamname; } + """); + + Compilation compilation = compile(dto); + String generatedCode = loadGeneratedSource(compilation, builderClassName); + assertGenerationSucceeded(compilation, builderClassName, generatedCode); + + // Expected method javadoc for the basic setter (description + example + tags) + ProcessorAsserts.assertContaining( + generatedCode, + """ + * Sets the value for teamname. + *

Example:

+ *
{@code
+        * builder.teamname("example value");
+        * }
+ * @param teamname teamname + * @return current instance of builder + */ + public PersonBuilder teamname(String teamname) + """); + } + + @Test + void shouldGenerateMethodJavadocExampleForPrimitiveSetter() { + String packageName = "test"; + String className = "Counter"; + String builderClassName = className + "Builder"; + + JavaFileObject dto = + ProcessorTestUtils.simpleBuilderClass( + packageName, + className, + """ + private int amount; + public int getAmount() { return amount; } + public void setAmount(int amount) { this.amount = amount; } + """); + + Compilation compilation = compile(dto); + String generatedCode = loadGeneratedSource(compilation, builderClassName); + assertGenerationSucceeded(compilation, builderClassName, generatedCode); + + ProcessorAsserts.assertContaining( + generatedCode, + """ + * Sets the value for amount. + *

Example:

+ *
{@code
+        * builder.amount(42);
+        * }
+ * @param amount amount + * @return current instance of builder + */ + public CounterBuilder amount(int amount) + """); + } + + @Test + void shouldGenerateMethodJavadocExampleForSupplier() { + String packageName = "test"; + String className = "SupplierDto"; + String builderClassName = className + "Builder"; + + JavaFileObject dto = + ProcessorTestUtils.simpleBuilderClass( + packageName, + className, + """ + private String title; + public String getTitle() { return title; } + public void setTitle(String title) { this.title = title; } + """); + + Compilation compilation = compile(dto); + String generatedCode = loadGeneratedSource(compilation, builderClassName); + assertGenerationSucceeded(compilation, builderClassName, generatedCode); + + ProcessorAsserts.assertContaining( + generatedCode, + """ + *

Example:

+ *
{@code
+        * builder.titleSupplier(() -> "example value");
+        * }
+ """); + } + + @Test + void shouldGenerateMethodJavadocExampleForAddToCollection() { + String packageName = "test"; + String className = "TagsDto"; + String builderClassName = className + "Builder"; + + JavaFileObject dto = + ProcessorTestUtils.simpleBuilderClass( + packageName, + className, + """ + private java.util.List tags; + public java.util.List getTags() { return tags; } + public void setTags(java.util.List tags) { this.tags = tags; } + """); + + Compilation compilation = compile(dto); + String generatedCode = loadGeneratedSource(compilation, builderClassName); + assertGenerationSucceeded(compilation, builderClassName, generatedCode); + + ProcessorAsserts.assertContaining( + generatedCode, + """ + * Adds a single element to tags. + *

Example:

+ *
{@code
+        * builder.add2Tags("example value");
+        * }
+ * @param element the element to add + * @return current instance of builder + */ + public TagsDtoBuilder add2Tags(String element) + """); + } + + @Test + void shouldGenerateMethodJavadocExampleForListConsumer() { + String packageName = "test"; + String className = "ListDto"; + String builderClassName = className + "Builder"; + + JavaFileObject dto = + ProcessorTestUtils.simpleBuilderClass( + packageName, + className, + """ + private java.util.List tags; + public java.util.List getTags() { return tags; } + public void setTags(java.util.List tags) { this.tags = tags; } + """); + + Compilation compilation = compile(dto); + String generatedCode = loadGeneratedSource(compilation, builderClassName); + assertGenerationSucceeded(compilation, builderClassName, generatedCode); + + ProcessorAsserts.assertContaining( + generatedCode, + """ + *

Example:

+ *
{@code
+        * builder.tags(t -> t.add("example value"));
+        * }
+ """); + } + + @Test + void shouldGenerateMethodJavadocExampleForCreate() { + String packageName = "test"; + String className = "CreateDto"; + String builderClassName = className + "Builder"; + + JavaFileObject dto = + ProcessorTestUtils.simpleBuilderClass( + packageName, + className, + """ + private String name; + public String getName() { return name; } + public void setName(String name) { this.name = name; } + """); + + Compilation compilation = compile(dto); + String generatedCode = loadGeneratedSource(compilation, builderClassName); + assertGenerationSucceeded(compilation, builderClassName, generatedCode); + + ProcessorAsserts.assertContaining( + generatedCode, + """ + *

Example:

+ *
{@code
+        * CreateDtoBuilder builder = CreateDtoBuilder.create();
+        * }
+ """); + } + + @Test + void shouldGenerateMethodJavadocExampleForBuild() { + String packageName = "test"; + String className = "BuildDto"; + String builderClassName = className + "Builder"; + + JavaFileObject dto = + ProcessorTestUtils.simpleBuilderClass( + packageName, + className, + """ + private String name; + public String getName() { return name; } + public void setName(String name) { this.name = name; } + """); + + Compilation compilation = compile(dto); + String generatedCode = loadGeneratedSource(compilation, builderClassName); + assertGenerationSucceeded(compilation, builderClassName, generatedCode); + + ProcessorAsserts.assertContaining( + generatedCode, + """ + *

Example:

+ *
{@code
+        * BuildDto result = builder.build();
+        * }
+ """); + } + + // --------------------------------------------------------------------------- + // Negative cases: no example emitted when placeholder value is unresolvable + // and empty code blocks must not be rendered at all. + // --------------------------------------------------------------------------- + + @Test + void shouldOmitExampleBlockWhenFieldTypeHasNoDefaultValue() { + // Given: field with a reference type that has no entry in JavadocExampleValues + // and no @SimpleBuilder on the helper type -> no example value available. + String packageName = "test"; + String className = "UnknownTypeDto"; + String builderClassName = className + "Builder"; + + JavaFileObject dto = + ProcessorTestUtils.simpleBuilderClass( + packageName, + className, + """ + private HelperPlain helper; + public HelperPlain getHelper() { return helper; } + public void setHelper(HelperPlain helper) { this.helper = helper; } + """); + + JavaFileObject helper = + ProcessorTestUtils.forSource( + """ + package test; + public class HelperPlain { public HelperPlain() {} } + """); + + Compilation compilation = compile(dto, helper); + String generatedCode = loadGeneratedSource(compilation, builderClassName); + assertGenerationSucceeded(compilation, builderClassName, generatedCode); + + // The basic setter must still be generated, + // but the method javadoc must NOT contain any example block: + // neither a bogus "builder.helper(null)" line + // nor an empty "
{@code ... }
" block. + ProcessorAsserts.assertContaining(generatedCode, "public UnknownTypeDtoBuilder helper("); + ProcessorAsserts.assertNotContaining( + generatedCode, + // no example line falling back to `null` + "builder.helper(null)", + // no empty example block either + "
{@code\n}
", + "
{@code }
", + "
{@code}
"); + } + + @Test + void shouldOmitClassExampleLineForUnresolvableField() { + // Given: a DTO with both a resolvable field and an unresolvable field. + // The class-level kitchen-sink chain must include ONLY the resolvable field. + String packageName = "test"; + String className = "MixedDto"; + String builderClassName = className + "Builder"; + + JavaFileObject dto = + ProcessorTestUtils.simpleBuilderClass( + packageName, + className, + """ + private String title; + private HelperPlain helper; + public String getTitle() { return title; } + public void setTitle(String title) { this.title = title; } + public HelperPlain getHelper() { return helper; } + public void setHelper(HelperPlain helper) { this.helper = helper; } + """); + + JavaFileObject helper = + ProcessorTestUtils.forSource( + """ + package test; + public class HelperPlain { public HelperPlain() {} } + """); + + Compilation compilation = compile(dto, helper); + String generatedCode = loadGeneratedSource(compilation, builderClassName); + assertGenerationSucceeded(compilation, builderClassName, generatedCode); + + // Class-level example contains ONLY the resolvable field's lines + ProcessorAsserts.assertContaining( + generatedCode, + """ + *

Example:

+ *
{@code
+        * MixedDto result = MixedDtoBuilder.create()
+        *     .title("example value")
+        *     .titleSupplier(() -> "example value")
+        *     .build();
+        * }
+ """); + + // ...but NO `.helper(...)` line in the class example chain + ProcessorAsserts.assertNotContaining(generatedCode, ".helper(null)", ".helper(\""); + } + + @Test + void shouldNotRenderEmptyJavadocExampleBlocks() { + // Sanity guard: no generated method should ever contain an empty example block. + String packageName = "test"; + String className = "NoEmptyDto"; + String builderClassName = className + "Builder"; + + JavaFileObject dto = + ProcessorTestUtils.simpleBuilderClass( + packageName, + className, + """ + private String name; + public String getName() { return name; } + public void setName(String name) { this.name = name; } + """); + + Compilation compilation = compile(dto); + String generatedCode = loadGeneratedSource(compilation, builderClassName); + assertGenerationSucceeded(compilation, builderClassName, generatedCode); + + // Degenerate empty blocks must never appear. + ProcessorAsserts.assertNotContaining( + generatedCode, "
{@code\n}
", "
{@code }
", "
{@code}
"); + } +} From 4af38e441364661df62ee78417627681a516bd38 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Tue, 28 Apr 2026 21:58:06 +0200 Subject: [PATCH 02/23] Finalizing 1st implementation of javadoc generation --- .../builders/example/BookDtoBuilder.java | 82 ++++++++ .../example/JacksonIntegrationDtoBuilder.java | 47 +++++ .../example/MannschaftDtoBuilder.java | 40 ++++ .../builders/example/PersonDtoBuilder.java | 61 ++++++ .../example/ProductRecordBuilder.java | 61 ++++++ .../builders/example/SponsorDtoBuilder.java | 30 +++ .../classgen/roaster/RoasterMapper.java | 6 +- .../builder/ClassJavaDocEnhancer.java | 20 +- .../builder/CoreMethodsEnhancer.java | 24 ++- .../field/AddToCollectionGenerator.java | 16 ++ .../field/BasicSetterGenerator.java | 20 ++ .../field/ListConsumerGenerator.java | 34 +++ .../field/SupplierMethodGenerator.java | 20 ++ .../generators/util/JavadocExampleValues.java | 88 ++++++++ .../model/core/BuilderDefinitionDto.java | 37 ++++ .../model/javadoc/JavadocCodeBlockDto.java | 38 ++++ .../processor/model/javadoc/JavadocDto.java | 40 +++- .../model/method/CodeTemplateDto.java | 199 ++++++++++++++++++ .../processor/model/method/MethodCodeDto.java | 167 +-------------- .../BuilderConfigurationReaderTest.java | 24 +++ .../processor/BuilderJavadocExampleTest.java | 38 ++-- .../processor/BuilderProcessorTest.java | 2 +- .../ComprehensiveFeatureIntegrationTest.java | 105 ++++++++- 23 files changed, 997 insertions(+), 202 deletions(-) create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/JavadocExampleValues.java create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/model/javadoc/JavadocCodeBlockDto.java create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/CodeTemplateDto.java diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java index 0461b5df..a566416c 100644 --- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java @@ -20,6 +20,22 @@ * This builder provides a fluent API for creating instances of org.javahelpers.simple.builders.example.BookDto with * method chaining and validation. Use the static {@code create()} method to obtain a new builder instance, configure * the desired properties using the setter methods, and then call {@code build()} to create the final DTO. + * + *

Example:

+ * + *
{@code
+ * BookDto result = BookDtoBuilder.create()
+ *     .author("example value")
+ *     .available(true)
+ *     .category('x')
+ *     .discount(3.14f)
+ *     .isbn("example value")
+ *     .pages(42)
+ *     .price(3.14)
+ *     .salesCount(42L)
+ *     .title("example value")
+ *     .build();
+ * }
*/ public class BookDtoBuilder { @@ -136,6 +152,12 @@ public BookDtoBuilder(BookDto instance) { /** * Creating a new builder for {@code org.javahelpers.simple.builders.example.BookDto}. * + *

Example:

+ * + *
{@code
+   * BookDtoBuilder builder = BookDtoBuilder.create();
+   * }
+ * * @return builder for {@code org.javahelpers.simple.builders.example.BookDto} */ public static BookDtoBuilder create() { @@ -145,6 +167,12 @@ public static BookDtoBuilder create() { /** * Sets the value for author. * + *

Example:

+ * + *
{@code
+   * builder.author("example value");
+   * }
+ * * @param author the book author to set * @return current instance of builder */ @@ -156,6 +184,12 @@ public BookDtoBuilder author(String author) { /** * Sets the value for available. * + *

Example:

+ * + *
{@code
+   * builder.available(true);
+   * }
+ * * @param available true if available, false otherwise * @return current instance of builder */ @@ -167,6 +201,12 @@ public BookDtoBuilder available(boolean available) { /** * Sets the value for category. * + *

Example:

+ * + *
{@code
+   * builder.category('x');
+   * }
+ * * @param category the category code to set * @return current instance of builder */ @@ -178,6 +218,12 @@ public BookDtoBuilder category(char category) { /** * Sets the value for discount. * + *

Example:

+ * + *
{@code
+   * builder.discount(3.14f);
+   * }
+ * * @param discount the discount percentage to set * @return current instance of builder */ @@ -222,6 +268,12 @@ public BookDtoBuilder genres(Set genres) { /** * Sets the value for isbn. * + *

Example:

+ * + *
{@code
+   * builder.isbn("example value");
+   * }
+ * * @param isbn the ISBN to set * @return current instance of builder */ @@ -255,6 +307,12 @@ public BookDtoBuilder metadata(Map metadata) { /** * Sets the value for pages. * + *

Example:

+ * + *
{@code
+   * builder.pages(42);
+   * }
+ * * @param pages the page count to set * @return current instance of builder */ @@ -266,6 +324,12 @@ public BookDtoBuilder pages(int pages) { /** * Sets the value for price. * + *

Example:

+ * + *
{@code
+   * builder.price(3.14);
+   * }
+ * * @param price the book price to set * @return current instance of builder */ @@ -310,6 +374,12 @@ public BookDtoBuilder rating(byte rating) { /** * Sets the value for salesCount. * + *

Example:

+ * + *
{@code
+   * builder.salesCount(42L);
+   * }
+ * * @param salesCount the sales count to set * @return current instance of builder */ @@ -343,6 +413,12 @@ public BookDtoBuilder tags(List tags) { /** * Sets the value for title. * + *

Example:

+ * + *
{@code
+   * builder.title("example value");
+   * }
+ * * @param title the book title to set * @return current instance of builder */ @@ -392,6 +468,12 @@ BookDtoBuilder validateTitle() { /** * Builds the configured DTO instance. + * + *

Example:

+ * + *
{@code
+   * BookDto result = builder.build();
+   * }
*/ public BookDto build() { if (this.available.isSet() && this.available.value() == null) { diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/JacksonIntegrationDtoBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/JacksonIntegrationDtoBuilder.java index b9aba88d..b369df0d 100644 --- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/JacksonIntegrationDtoBuilder.java +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/JacksonIntegrationDtoBuilder.java @@ -21,6 +21,17 @@ * org.javahelpers.simple.builders.example.JacksonIntegrationDto with method chaining and validation. Use the static * {@code create()} method to obtain a new builder instance, configure the desired properties using the setter methods, * and then call {@code build()} to create the final DTO. + * + *

Example:

+ * + *
{@code
+ * JacksonIntegrationDto result = JacksonIntegrationDtoBuilder.create()
+ *     .name("example value")
+ *     .name(() -> "example value")
+ *     .age(42)
+ *     .age(() -> 42)
+ *     .build();
+ * }
*/ @Generated("Generated by org.javahelpers.simple.builders.processor.BuilderProcessor") @BuilderImplementation(forClass = JacksonIntegrationDto.class) @@ -55,6 +66,12 @@ public JacksonIntegrationDtoBuilder(JacksonIntegrationDto instance) { /** * Creating a new builder for {@code org.javahelpers.simple.builders.example.JacksonIntegrationDto}. * + *

Example:

+ * + *
{@code
+   * JacksonIntegrationDtoBuilder builder = JacksonIntegrationDtoBuilder.create();
+   * }
+ * * @return builder for {@code org.javahelpers.simple.builders.example.JacksonIntegrationDto} */ public static JacksonIntegrationDtoBuilder create() { @@ -64,6 +81,12 @@ public static JacksonIntegrationDtoBuilder create() { /** * Sets the value for age. * + *

Example:

+ * + *
{@code
+   * builder.age(42);
+   * }
+ * * @param age age * @return current instance of builder */ @@ -75,6 +98,12 @@ public JacksonIntegrationDtoBuilder age(int age) { /** * Sets the value for age by invoking the provided supplier. * + *

Example:

+ * + *
{@code
+   * builder.age(() -> 42);
+   * }
+ * * @param ageSupplier supplier for age * @return current instance of builder */ @@ -86,6 +115,12 @@ public JacksonIntegrationDtoBuilder age(Supplier ageSupplier) { /** * Sets the value for name. * + *

Example:

+ * + *
{@code
+   * builder.name("example value");
+   * }
+ * * @param name name * @return current instance of builder */ @@ -110,6 +145,12 @@ public JacksonIntegrationDtoBuilder name(Consumer nameStringBuild /** * Sets the value for name by invoking the provided supplier. * + *

Example:

+ * + *
{@code
+   * builder.name(() -> "example value");
+   * }
+ * * @param nameSupplier supplier for name * @return current instance of builder */ @@ -176,6 +217,12 @@ public JacksonIntegrationDtoBuilder conditional(BooleanSupplier condition, /** * Builds the configured DTO instance. + * + *

Example:

+ * + *
{@code
+   * JacksonIntegrationDto result = builder.build();
+   * }
*/ @Override public JacksonIntegrationDto build() { diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/MannschaftDtoBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/MannschaftDtoBuilder.java index 1ad4b485..a85453ad 100644 --- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/MannschaftDtoBuilder.java +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/MannschaftDtoBuilder.java @@ -22,6 +22,16 @@ * This builder provides a fluent API for creating instances of org.javahelpers.simple.builders.example.MannschaftDto * with method chaining and validation. Use the static {@code create()} method to obtain a new builder instance, * configure the desired properties using the setter methods, and then call {@code build()} to create the final DTO. + * + *

Example:

+ * + *
{@code
+ * MannschaftDto result = MannschaftDtoBuilder.create()
+ *     .name("example value")
+ *     .name(() -> "example value")
+ *     .add2Sponsoren("example value")
+ *     .build();
+ * }
*/ @Generated("Generated by org.javahelpers.simple.builders.processor.BuilderProcessor") @BuilderImplementation(forClass = MannschaftDto.class) @@ -55,6 +65,12 @@ public MannschaftDtoBuilder(MannschaftDto instance) { /** * Creating a new builder for {@code org.javahelpers.simple.builders.example.MannschaftDto}. * + *

Example:

+ * + *
{@code
+   * MannschaftDtoBuilder builder = MannschaftDtoBuilder.create();
+   * }
+ * * @return builder for {@code org.javahelpers.simple.builders.example.MannschaftDto} */ public static MannschaftDtoBuilder create() { @@ -64,6 +80,12 @@ public static MannschaftDtoBuilder create() { /** * Adds a single element to sponsoren. * + *

Example:

+ * + *
{@code
+   * builder.add2Sponsoren("example value");
+   * }
+ * * @param element the element to add * @return current instance of builder */ @@ -82,6 +104,12 @@ public MannschaftDtoBuilder add2Sponsoren(SponsorDto element) { /** * Sets the value for name. * + *

Example:

+ * + *
{@code
+   * builder.name("example value");
+   * }
+ * * @param name name * @return current instance of builder */ @@ -106,6 +134,12 @@ public MannschaftDtoBuilder name(Consumer nameStringBuilderConsum /** * Sets the value for name by invoking the provided supplier. * + *

Example:

+ * + *
{@code
+   * builder.name(() -> "example value");
+   * }
+ * * @param nameSupplier supplier for name * @return current instance of builder */ @@ -221,6 +255,12 @@ public MannschaftDtoBuilder conditional(BooleanSupplier condition, ConsumerExample: + * + *
{@code
+   * MannschaftDto result = builder.build();
+   * }
*/ @Override public MannschaftDto build() { diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/PersonDtoBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/PersonDtoBuilder.java index c3f6e7a4..33f7fa0e 100644 --- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/PersonDtoBuilder.java +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/PersonDtoBuilder.java @@ -23,6 +23,19 @@ * This builder provides a fluent API for creating instances of org.javahelpers.simple.builders.example.PersonDto with * method chaining and validation. Use the static {@code create()} method to obtain a new builder instance, configure * the desired properties using the setter methods, and then call {@code build()} to create the final DTO. + * + *

Example:

+ * + *
{@code
+ * PersonDto result = PersonDtoBuilder.create()
+ *     .name("example value")
+ *     .name(() -> "example value")
+ *     .nickNames(t -> t.add("example value"))
+ *     .add2NickNames("example value")
+ *     .nickNames2("example value")
+ *     .nickNames2(() -> "example value")
+ *     .build();
+ * }
*/ @Generated("Generated by org.javahelpers.simple.builders.processor.BuilderProcessor") @BuilderImplementation(forClass = PersonDto.class) @@ -70,6 +83,12 @@ public PersonDtoBuilder(PersonDto instance) { /** * Creating a new builder for {@code org.javahelpers.simple.builders.example.PersonDto}. * + *

Example:

+ * + *
{@code
+   * PersonDtoBuilder builder = PersonDtoBuilder.create();
+   * }
+ * * @return builder for {@code org.javahelpers.simple.builders.example.PersonDto} */ public static PersonDtoBuilder create() { @@ -79,6 +98,12 @@ public static PersonDtoBuilder create() { /** * Adds a single element to nickNames. * + *

Example:

+ * + *
{@code
+   * builder.add2NickNames("example value");
+   * }
+ * * @param element the element to add * @return current instance of builder */ @@ -156,6 +181,12 @@ public PersonDtoBuilder mannschaft(Supplier mannschaftSupplier) { /** * Sets the value for name. * + *

Example:

+ * + *
{@code
+   * builder.name("example value");
+   * }
+ * * @param name name * @return current instance of builder */ @@ -180,6 +211,12 @@ public PersonDtoBuilder name(Consumer nameStringBuilderConsumer) /** * Sets the value for name by invoking the provided supplier. * + *

Example:

+ * + *
{@code
+   * builder.name(() -> "example value");
+   * }
+ * * @param nameSupplier supplier for name * @return current instance of builder */ @@ -226,6 +263,12 @@ public PersonDtoBuilder nickNames(List nickNames) { /** * Sets the value for nickNames using a builder consumer that produces the value. * + *

Example:

+ * + *
{@code
+   * builder.nickNames(t -> t.add("example value"));
+   * }
+ * * @param nickNamesBuilderConsumer consumer providing an instance of a builder for nickNames * @return current instance of builder */ @@ -252,6 +295,12 @@ public PersonDtoBuilder nickNames(Supplier> nickNamesSupplier) { /** * Sets the value for nickNames2. * + *

Example:

+ * + *
{@code
+   * builder.nickNames2("example value");
+   * }
+ * * @param nickNames2 nickNames2 * @return current instance of builder */ @@ -289,6 +338,12 @@ public PersonDtoBuilder nickNames2(Consumer> nickNames2 /** * Sets the value for nickNames2 by invoking the provided supplier. * + *

Example:

+ * + *
{@code
+   * builder.nickNames2(() -> "example value");
+   * }
+ * * @param nickNames2Supplier supplier for nickNames2 * @return current instance of builder */ @@ -341,6 +396,12 @@ public PersonDtoBuilder conditional(BooleanSupplier condition, ConsumerExample: + * + *
{@code
+   * PersonDto result = builder.build();
+   * }
*/ @Override public PersonDto build() { diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/ProductRecordBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/ProductRecordBuilder.java index adc17e48..2de9ef62 100644 --- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/ProductRecordBuilder.java +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/ProductRecordBuilder.java @@ -19,6 +19,19 @@ * This builder provides a fluent API for creating instances of org.javahelpers.simple.builders.example.ProductRecord * with method chaining and validation. Use the static {@code create()} method to obtain a new builder instance, * configure the desired properties using the setter methods, and then call {@code build()} to create the final DTO. + * + *

Example:

+ * + *
{@code
+ * ProductRecord result = ProductRecordBuilder.create()
+ *     .name("example value")
+ *     .name(() -> "example value")
+ *     .price(3.14)
+ *     .price(() -> 3.14)
+ *     .category("example value")
+ *     .category(() -> "example value")
+ *     .build();
+ * }
*/ @Generated("Generated by org.javahelpers.simple.builders.processor.BuilderProcessor") @BuilderImplementation(forClass = ProductRecord.class) @@ -57,6 +70,12 @@ public ProductRecordBuilder(ProductRecord instance) { /** * Creating a new builder for {@code org.javahelpers.simple.builders.example.ProductRecord}. * + *

Example:

+ * + *
{@code
+   * ProductRecordBuilder builder = ProductRecordBuilder.create();
+   * }
+ * * @return builder for {@code org.javahelpers.simple.builders.example.ProductRecord} */ public static ProductRecordBuilder create() { @@ -66,6 +85,12 @@ public static ProductRecordBuilder create() { /** * Sets the value for category. * + *

Example:

+ * + *
{@code
+   * builder.category("example value");
+   * }
+ * * @param category category * @return current instance of builder */ @@ -90,6 +115,12 @@ public ProductRecordBuilder category(Consumer categoryStringBuild /** * Sets the value for category by invoking the provided supplier. * + *

Example:

+ * + *
{@code
+   * builder.category(() -> "example value");
+   * }
+ * * @param categorySupplier supplier for category * @return current instance of builder */ @@ -114,6 +145,12 @@ public ProductRecordBuilder category(String format, Object... args) { /** * Sets the value for name. * + *

Example:

+ * + *
{@code
+   * builder.name("example value");
+   * }
+ * * @param name name * @return current instance of builder */ @@ -138,6 +175,12 @@ public ProductRecordBuilder name(Consumer nameStringBuilderConsum /** * Sets the value for name by invoking the provided supplier. * + *

Example:

+ * + *
{@code
+   * builder.name(() -> "example value");
+   * }
+ * * @param nameSupplier supplier for name * @return current instance of builder */ @@ -162,6 +205,12 @@ public ProductRecordBuilder name(String format, Object... args) { /** * Sets the value for price. * + *

Example:

+ * + *
{@code
+   * builder.price(3.14);
+   * }
+ * * @param price price * @return current instance of builder */ @@ -173,6 +222,12 @@ public ProductRecordBuilder price(double price) { /** * Sets the value for price by invoking the provided supplier. * + *

Example:

+ * + *
{@code
+   * builder.price(() -> 3.14);
+   * }
+ * * @param priceSupplier supplier for price * @return current instance of builder */ @@ -238,6 +293,12 @@ public ProductRecordBuilder conditional(BooleanSupplier condition, ConsumerExample: + * + *
{@code
+   * ProductRecord result = builder.build();
+   * }
*/ @Override public ProductRecord build() { diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SponsorDtoBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SponsorDtoBuilder.java index 295bcb70..0447ae9e 100644 --- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SponsorDtoBuilder.java +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SponsorDtoBuilder.java @@ -19,6 +19,12 @@ * This builder provides a fluent API for creating instances of org.javahelpers.simple.builders.example.SponsorDto with * method chaining and validation. Use the static {@code create()} method to obtain a new builder instance, configure * the desired properties using the setter methods, and then call {@code build()} to create the final DTO. + * + *

Example:

+ * + *
{@code
+ * SponsorDto result = SponsorDtoBuilder.create().name("example value").name(() -> "example value").build();
+ * }
*/ @Generated("Generated by org.javahelpers.simple.builders.processor.BuilderProcessor") @BuilderImplementation(forClass = SponsorDto.class) @@ -47,6 +53,12 @@ public SponsorDtoBuilder(SponsorDto instance) { /** * Creating a new builder for {@code org.javahelpers.simple.builders.example.SponsorDto}. * + *

Example:

+ * + *
{@code
+   * SponsorDtoBuilder builder = SponsorDtoBuilder.create();
+   * }
+ * * @return builder for {@code org.javahelpers.simple.builders.example.SponsorDto} */ public static SponsorDtoBuilder create() { @@ -56,6 +68,12 @@ public static SponsorDtoBuilder create() { /** * Sets the value for name. * + *

Example:

+ * + *
{@code
+   * builder.name("example value");
+   * }
+ * * @param name name * @return current instance of builder */ @@ -80,6 +98,12 @@ public SponsorDtoBuilder name(Consumer nameStringBuilderConsumer) /** * Sets the value for name by invoking the provided supplier. * + *

Example:

+ * + *
{@code
+   * builder.name(() -> "example value");
+   * }
+ * * @param nameSupplier supplier for name * @return current instance of builder */ @@ -145,6 +169,12 @@ public SponsorDtoBuilder conditional(BooleanSupplier condition, ConsumerExample: + * + *
{@code
+   * SponsorDto result = builder.build();
+   * }
*/ @Override public SponsorDto build() { diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterMapper.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterMapper.java index 8769949a..69cd0c14 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterMapper.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterMapper.java @@ -30,7 +30,7 @@ import org.javahelpers.simple.builders.processor.classgen.roaster.exceptions.RoasterMapperException; import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto; import org.javahelpers.simple.builders.processor.model.annotation.InterfaceName; -import org.javahelpers.simple.builders.processor.model.method.MethodCodeDto; +import org.javahelpers.simple.builders.processor.model.method.CodeTemplateDto; import org.javahelpers.simple.builders.processor.model.method.MethodCodePlaceholder; import org.javahelpers.simple.builders.processor.model.method.MethodCodeStringPlaceholder; import org.javahelpers.simple.builders.processor.model.method.MethodCodeTypePlaceholder; @@ -164,12 +164,12 @@ public static String mapInterfaceToTypeName(InterfaceName interfaceName) { } /** - * Resolves the JavaPoet-style named template used in MethodCodeDto to plain Java source code. + * Resolves the JavaPoet-style named template used in CodeTemplateDto to plain Java source code. * * @param codeDto code template DTO * @return resolved Java source code */ - public static String resolveCodeTemplate(MethodCodeDto codeDto) { + public static String resolveCodeTemplate(CodeTemplateDto codeDto) { String code = codeDto.getCodeFormat(); for (MethodCodePlaceholder placeHolderValue : codeDto.getCodeArguments()) { String label = placeHolderValue.getLabel(); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ClassJavaDocEnhancer.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ClassJavaDocEnhancer.java index 71ae20f9..ef573261 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ClassJavaDocEnhancer.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ClassJavaDocEnhancer.java @@ -71,7 +71,7 @@ */ public class ClassJavaDocEnhancer implements BuilderEnhancer { - private static final int PRIORITY = 200; + private static final int PRIORITY = 10; @Override public int getPriority() { @@ -88,6 +88,24 @@ public boolean appliesTo( public void enhanceBuilder(BuilderDefinitionDto builderDto, ProcessingContext context) { TypeName targetType = builderDto.getBuildingTargetTypeName(); JavadocDto javadoc = createClassJavadoc(targetType); + + // Finalize class-level example if any lines were contributed + if (builderDto.getClassExampleBlock() != null && builderDto.getClassExampleBlock().hasCode()) { + // Prepend opening line + String builderTypeName = builderDto.getBuilderTypeName().getClassName(); + String openingLine = + "%s result = %s.create()".formatted(targetType.getClassName(), builderTypeName); + builderDto + .getClassExampleBlock() + .setCodeFormat(openingLine + "\n" + builderDto.getClassExampleBlock().getCodeFormat()); + + // Append closing line + builderDto.getClassExampleBlock().append(".build();"); + + // Add the example block to the class Javadoc + javadoc.addExample(builderDto.getClassExampleBlock()); + } + builderDto.setClassJavadoc(javadoc); } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java index 44ed603f..4fabadaf 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java @@ -32,6 +32,7 @@ import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto; import org.javahelpers.simple.builders.processor.model.core.BuilderDefinitionDto; import org.javahelpers.simple.builders.processor.model.core.FieldDto; +import org.javahelpers.simple.builders.processor.model.javadoc.JavadocCodeBlockDto; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; import org.javahelpers.simple.builders.processor.model.method.MethodDto; import org.javahelpers.simple.builders.processor.model.type.GenericParameterDto; @@ -197,7 +198,15 @@ protected MethodDto createBuildMethod(BuilderDefinitionDto builderDto) { method.addArgument("dtoBaseType", builderDto.getBuildingTargetTypeName()); method.addArgument("buildResultType", returnType); method.getMethodCodeDto().addCodeBlockImport(IllegalStateException.class); - method.setJavadoc(new JavadocDto("Builds the configured DTO instance.")); + JavadocDto javadoc = new JavadocDto("Builds the configured DTO instance."); + + // Add example to build() method + JavadocCodeBlockDto exampleBlock = new JavadocCodeBlockDto(); + String targetSimpleName = builderDto.getBuildingTargetTypeName().getClassName(); + exampleBlock.setCodeFormat("%s result = builder.build();".formatted(targetSimpleName)); + javadoc.addExample(exampleBlock); + + method.setJavadoc(javadoc); return method; } @@ -228,10 +237,19 @@ protected MethodDto createStaticCreateMethod(BuilderDefinitionDto builderDto) { method.addArgument("builderType", builderDto.getBuilderTypeName()); String targetFullName = builderDto.getBuildingTargetTypeName().getFullQualifiedName(); + String builderSimpleName = builderDto.getBuilderTypeName().getClassName(); - method.setJavadoc( + JavadocDto javadoc = new JavadocDto("Creating a new builder for {@code %s}.", targetFullName) - .addReturn("builder for {@code %s}", targetFullName)); + .addReturn("builder for {@code %s}", targetFullName); + + // Add example to create() method + JavadocCodeBlockDto exampleBlock = new JavadocCodeBlockDto(); + exampleBlock.setCodeFormat( + "%s builder = %s.create();".formatted(builderSimpleName, builderSimpleName)); + javadoc.addExample(exampleBlock); + + method.setJavadoc(javadoc); return method; } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/AddToCollectionGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/AddToCollectionGenerator.java index e835219b..0a2601c2 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/AddToCollectionGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/AddToCollectionGenerator.java @@ -32,6 +32,7 @@ import org.javahelpers.simple.builders.processor.generators.MethodGenerator; import org.javahelpers.simple.builders.processor.generators.util.JavadocConstants; import org.javahelpers.simple.builders.processor.model.core.FieldDto; +import org.javahelpers.simple.builders.processor.model.javadoc.JavadocCodeBlockDto; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; import org.javahelpers.simple.builders.processor.model.method.MethodDto; import org.javahelpers.simple.builders.processor.model.method.MethodParameterDto; @@ -187,6 +188,21 @@ private MethodDto createAddToCollectionMethod( .addParam("element", "the element to add") .addReturn(JavadocConstants.RETURN_BUILDER_INSTANCE)); + // Add method-level example for add-to-collection method + if (methodDto.getJavadoc() != null) { + JavadocCodeBlockDto exampleBlock = new JavadocCodeBlockDto(); + exampleBlock.setCodeFormat( + "builder.%s(\"example value\");".formatted(methodDto.getMethodName())); + methodDto.getJavadoc().addExample(exampleBlock); + } + + // Contribute to class-level example + if (context.getCurrentBuilderDto() != null) { + context + .getCurrentBuilderDto() + .addClassExampleLine(" .%s(\"example value\")".formatted(methodDto.getMethodName())); + } + return methodDto; } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/BasicSetterGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/BasicSetterGenerator.java index cf1bd0b9..7076c97c 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/BasicSetterGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/BasicSetterGenerator.java @@ -28,8 +28,11 @@ import java.util.Collections; import java.util.List; +import java.util.Optional; import org.javahelpers.simple.builders.processor.generators.MethodGenerator; +import org.javahelpers.simple.builders.processor.generators.util.JavadocExampleValues; import org.javahelpers.simple.builders.processor.model.core.FieldDto; +import org.javahelpers.simple.builders.processor.model.javadoc.JavadocCodeBlockDto; import org.javahelpers.simple.builders.processor.model.method.MethodDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.processing.ProcessingContext; @@ -91,6 +94,23 @@ public List generateMethods( createBuilderMethodForFieldWithTransform( field, null, field.getFieldType(), builderType, context); + // Add method-level example if we have an example value for the field type + Optional exampleValue = JavadocExampleValues.getExampleValue(field.getFieldType()); + if (exampleValue.isPresent() && setterMethod.getJavadoc() != null) { + JavadocCodeBlockDto exampleBlock = new JavadocCodeBlockDto(); + exampleBlock.setCodeFormat( + "builder.%s(%s);".formatted(setterMethod.getMethodName(), exampleValue.get())); + setterMethod.getJavadoc().addExample(exampleBlock); + } + + // Contribute to class-level example if we have an example value + if (exampleValue.isPresent() && context.getCurrentBuilderDto() != null) { + String methodName = setterMethod.getMethodName(); + context + .getCurrentBuilderDto() + .addClassExampleLine(" .%s(%s)".formatted(methodName, exampleValue.get())); + } + return Collections.singletonList(setterMethod); } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ListConsumerGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ListConsumerGenerator.java index 820d9504..0d78788e 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ListConsumerGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ListConsumerGenerator.java @@ -35,6 +35,7 @@ import org.javahelpers.simple.builders.core.builders.ArrayListBuilderWithElementBuilders; import org.javahelpers.simple.builders.processor.generators.MethodGenerator; import org.javahelpers.simple.builders.processor.model.core.FieldDto; +import org.javahelpers.simple.builders.processor.model.javadoc.JavadocCodeBlockDto; import org.javahelpers.simple.builders.processor.model.method.MethodDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.model.type.TypeNameGeneric; @@ -133,6 +134,10 @@ public List generateMethods( MethodDto method = createFieldConsumerWithElementBuilders( field, collectionBuilderType, elementBuilderType.get(), builderType, context); + + // Add method-level example for list consumer + addExampleToListConsumer(method, field.getOriginalFieldName(), context); + return List.of(method); } else if (context.getConfiguration().shouldUseArrayListBuilder()) { TypeName arrayListBuilderType = map2TypeName(ArrayListBuilder.class); @@ -146,9 +151,38 @@ public List generateMethods( Map.of(), builderType, context); + + // Add method-level example for list consumer + addExampleToListConsumer(method, field.getOriginalFieldName(), context); + return List.of(method); } return Collections.emptyList(); } + + /** + * Adds method-level example and contributes to class-level example for list consumer methods. + * + * @param method the method to add the example to + * @param fieldName the field name + * @param context the processing context + */ + private void addExampleToListConsumer( + MethodDto method, String fieldName, ProcessingContext context) { + if (method.getJavadoc() != null) { + JavadocCodeBlockDto exampleBlock = new JavadocCodeBlockDto(); + exampleBlock.setCodeFormat( + "builder.%s(t -> t.add(\"example value\"));".formatted(method.getMethodName())); + method.getJavadoc().addExample(exampleBlock); + } + + // Contribute to class-level example + if (context.getCurrentBuilderDto() != null) { + context + .getCurrentBuilderDto() + .addClassExampleLine( + " .%s(t -> t.add(\"example value\"))".formatted(method.getMethodName())); + } + } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/SupplierMethodGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/SupplierMethodGenerator.java index 101468dd..c7bd27d2 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/SupplierMethodGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/SupplierMethodGenerator.java @@ -29,10 +29,13 @@ import java.util.Collections; import java.util.List; +import java.util.Optional; import java.util.function.Supplier; import org.javahelpers.simple.builders.processor.generators.MethodGenerator; import org.javahelpers.simple.builders.processor.generators.util.JavadocConstants; +import org.javahelpers.simple.builders.processor.generators.util.JavadocExampleValues; import org.javahelpers.simple.builders.processor.model.core.FieldDto; +import org.javahelpers.simple.builders.processor.model.javadoc.JavadocCodeBlockDto; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; import org.javahelpers.simple.builders.processor.model.method.MethodDto; import org.javahelpers.simple.builders.processor.model.method.MethodParameterDto; @@ -151,6 +154,23 @@ private MethodDto createFieldSupplier( .addParam(parameterName, "supplier for %s", fieldJavaDoc) .addReturn(JavadocConstants.RETURN_BUILDER_INSTANCE)); + // Add method-level example if we have an example value for the field type + Optional exampleValue = JavadocExampleValues.getExampleValue(fieldType); + if (exampleValue.isPresent()) { + JavadocCodeBlockDto exampleBlock = new JavadocCodeBlockDto(); + exampleBlock.setCodeFormat( + "builder.%s(() -> %s);".formatted(methodDto.getMethodName(), exampleValue.get())); + methodDto.getJavadoc().addExample(exampleBlock); + } + + // Contribute to class-level example if we have an example value + if (exampleValue.isPresent() && context.getCurrentBuilderDto() != null) { + context + .getCurrentBuilderDto() + .addClassExampleLine( + " .%s(() -> %s)".formatted(methodDto.getMethodName(), exampleValue.get())); + } + return methodDto; } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/JavadocExampleValues.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/JavadocExampleValues.java new file mode 100644 index 00000000..18e61aca --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/JavadocExampleValues.java @@ -0,0 +1,88 @@ +/* + * MIT License + * + * Copyright (c) 2026 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.generators.util; + +import java.util.Map; +import java.util.Optional; +import org.javahelpers.simple.builders.processor.model.type.TypeName; +import org.javahelpers.simple.builders.processor.model.type.TypeNamePrimitive; +import org.javahelpers.simple.builders.processor.model.type.TypeNamePrimitive.PrimitiveTypeEnum; + +/** + * Provides default example values for Javadoc code examples. + * + *

This class provides sensible default values for common types to be used in generated Javadoc + * code examples. These values are designed to be realistic yet generic enough to work in most + * contexts. + */ +public final class JavadocExampleValues { + + private static final String STRING_EXAMPLE = "\"example value\""; + private static final String INT_EXAMPLE = "42"; + private static final String LONG_EXAMPLE = "42L"; + private static final String DOUBLE_EXAMPLE = "3.14"; + private static final String FLOAT_EXAMPLE = "3.14f"; + private static final String BOOLEAN_EXAMPLE = "true"; + private static final String CHAR_EXAMPLE = "'x'"; + + /** Map of primitive type enums to their example values. */ + private static final Map PRIMITIVE_EXAMPLES = + Map.of( + PrimitiveTypeEnum.INT, INT_EXAMPLE, + PrimitiveTypeEnum.LONG, LONG_EXAMPLE, + PrimitiveTypeEnum.DOUBLE, DOUBLE_EXAMPLE, + PrimitiveTypeEnum.FLOAT, FLOAT_EXAMPLE, + PrimitiveTypeEnum.BOOLEAN, BOOLEAN_EXAMPLE, + PrimitiveTypeEnum.CHAR, CHAR_EXAMPLE); + + private JavadocExampleValues() { + // Utility class - prevent instantiation + } + + /** + * Returns an example value for the given type name, if available. + * + *

This method returns example values for: + * + *

    + *
  • Primitive types (int, long, double, float, boolean, char) + *
  • String type (returns {@value STRING_EXAMPLE}) + *
+ * + * @param typeName the type name to get an example value for + * @return an Optional containing the example value, or empty if no example is available + */ + public static Optional getExampleValue(TypeName typeName) { + if (typeName instanceof TypeNamePrimitive primitive) { + return Optional.ofNullable(PRIMITIVE_EXAMPLES.get(primitive.getType())); + } + // Check for String type + if ("java.lang.String".equals(typeName.getFullQualifiedName()) + || "String".equals(typeName.getClassName())) { + return Optional.of(STRING_EXAMPLE); + } + return Optional.empty(); + } +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderDefinitionDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderDefinitionDto.java index 7bc198e2..935fde3d 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderDefinitionDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderDefinitionDto.java @@ -26,6 +26,7 @@ import java.util.LinkedList; import java.util.List; +import org.javahelpers.simple.builders.processor.model.javadoc.JavadocCodeBlockDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; /** @@ -54,6 +55,9 @@ public class BuilderDefinitionDto extends GenerationTargetClassDto { /** Configuration for builder generation. */ private BuilderConfiguration configuration; + /** Code block for class-level Javadoc example. */ + private JavadocCodeBlockDto classExampleBlock; + /** * Getting type of builder. Delegates to base class typeName. * @@ -174,4 +178,37 @@ public BuilderConfiguration getConfiguration() { public void setConfiguration(BuilderConfiguration configuration) { this.configuration = configuration; } + + /** + * Gets the class-level Javadoc example code block. + * + * @return the class example block, or null if not set + */ + public JavadocCodeBlockDto getClassExampleBlock() { + return classExampleBlock; + } + + /** + * Sets the class-level Javadoc example code block. + * + * @param classExampleBlock the class example block + */ + public void setClassExampleBlock(JavadocCodeBlockDto classExampleBlock) { + this.classExampleBlock = classExampleBlock; + } + + /** + * Adds a line to the class-level Javadoc example code block. + * + *

If the class example block doesn't exist, it will be created. This method appends the line + * to the existing code format. + * + * @param line the line to add to the class example + */ + public void addClassExampleLine(String line) { + if (classExampleBlock == null) { + classExampleBlock = new JavadocCodeBlockDto(); + } + classExampleBlock.append(line); + } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/javadoc/JavadocCodeBlockDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/javadoc/JavadocCodeBlockDto.java new file mode 100644 index 00000000..ccd8a2f8 --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/javadoc/JavadocCodeBlockDto.java @@ -0,0 +1,38 @@ +/* + * MIT License + * + * Copyright (c) 2026 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.model.javadoc; + +import org.javahelpers.simple.builders.processor.model.method.CodeTemplateDto; + +/** + * DTO for holding code example blocks in Javadoc. + * + *

Extends {@link CodeTemplateDto} to reuse the placeholder resolution mechanism for code + * examples that appear in Javadoc comments. This allows generators to create code examples with + * placeholders that are resolved during code generation. + */ +public class JavadocCodeBlockDto extends CodeTemplateDto { + // All functionality inherited from CodeTemplateDto +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/javadoc/JavadocDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/javadoc/JavadocDto.java index dc21fb92..f841158e 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/javadoc/JavadocDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/javadoc/JavadocDto.java @@ -53,6 +53,9 @@ public class JavadocDto { /** List of Javadoc tags in the order they appear. */ private final List tags = new ArrayList<>(); + /** List of code example blocks for this Javadoc. */ + private final List codeBlocks = new ArrayList<>(); + /** Default constructor. */ public JavadocDto() { // Default constructor @@ -116,6 +119,37 @@ public List getTags() { return tags; } + /** + * Gets the list of code example blocks. + * + * @return the list of code blocks + */ + public List getCodeBlocks() { + return codeBlocks; + } + + /** + * Adds a code block to this Javadoc. + * + * @param codeBlock the code block to add + */ + public void addCodeBlock(JavadocCodeBlockDto codeBlock) { + if (codeBlock != null) { + codeBlocks.add(codeBlock); + } + } + + /** + * Adds a code example block to this Javadoc. + * + *

This is a convenience method for adding code examples. + * + * @param codeBlock the code example block to add + */ + public void addExample(JavadocCodeBlockDto codeBlock) { + addCodeBlock(codeBlock); + } + /** * Adds a tag with the given name and value. * @@ -208,12 +242,12 @@ public JavadocDto addThrows(String exceptionName, String descriptionFormat, Obje } /** - * Returns whether this Javadoc has any content (description or tags). + * Returns whether this Javadoc has any content (description, tags, or code blocks). * - * @return true if there is description text or at least one tag + * @return true if there is description text, at least one tag, or at least one code block */ public boolean hasContent() { - return StringUtils.isNotBlank(description) || !tags.isEmpty(); + return StringUtils.isNotBlank(description) || !tags.isEmpty() || !codeBlocks.isEmpty(); } @Override diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/CodeTemplateDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/CodeTemplateDto.java new file mode 100644 index 00000000..1a773c3a --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/CodeTemplateDto.java @@ -0,0 +1,199 @@ +/* + * MIT License + * + * Copyright (c) 2026 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.model.method; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import org.apache.commons.lang3.StringUtils; +import org.javahelpers.simple.builders.processor.model.imports.ImportStatement; +import org.javahelpers.simple.builders.processor.model.imports.RegularImport; +import org.javahelpers.simple.builders.processor.model.imports.StaticImport; +import org.javahelpers.simple.builders.processor.model.type.TypeName; +import org.javahelpers.simple.builders.processor.model.type.TypeNameArray; +import org.javahelpers.simple.builders.processor.model.type.TypeNameGeneric; + +/** + * Base DTO for holding code template information with placeholders. + * + *

This class provides the common infrastructure for code templates that use placeholders (e.g., + * {@code $label:N}, {@code $label:L}, {@code $label:S}, {@code $label:T}) which are resolved during + * code generation. + */ +public class CodeTemplateDto { + /** Format of code. Holding placeholder for dynamic values. */ + private String codeFormat; + + /** List of placeholders in CodeFormat. Containing dynamic values too. */ + private final List> codeArguments = new ArrayList<>(); + + /** Types used in the code body that aren't covered by arguments. */ + private final Set codeBlockImports = new LinkedHashSet<>(); + + /** + * Setting format of code. + * + * @param codeFormat Codeformat + */ + public void setCodeFormat(String codeFormat) { + this.codeFormat = codeFormat; + } + + /** + * Adding an argument for Codeformat. Helperfunction to set text value. + * + * @param name name in codeformat + * @param value value to fill in codeformat + */ + public void addArgument(String name, String value) { + codeArguments.add(new MethodCodeStringPlaceholder(name, value)); + } + + /** + * Adding an argument for Codeformat. Helperfunction to set TypeName value. + * + * @param name name in codeformat + * @param value value to fill in codeformat + */ + public void addArgument(String name, TypeName value) { + codeArguments.add(new MethodCodeTypePlaceholder(name, value)); + addCodeBlockImport(value); + } + + /** + * Getter for Codeformat. + * + * @return codeformat + */ + public String getCodeFormat() { + return codeFormat; + } + + /** + * Getter for arguments in Codeformat. + * + * @return argument values + */ + @SuppressWarnings("java:S1452") + public List> getCodeArguments() { + return codeArguments; + } + + /** + * Checks if this code DTO has code content. + * + * @return true if code format is not null and not blank + */ + public boolean hasCode() { + return !StringUtils.isBlank(codeFormat); + } + + /** + * Returns the set of imports used in the code body that aren't covered by arguments. + * + * @return set of imports used in code body + */ + public Set getCodeBlockImports() { + return codeBlockImports; + } + + /** + * Adding an import for a type used in the code block. + * + * @param typeName type to import + */ + public void addCodeBlockImport(TypeName typeName) { + if (typeName instanceof TypeNameGeneric generic) { + addCodeBlockImport(generic.getRawType()); + generic.getInnerTypeArguments().forEach(this::addCodeBlockImport); + } else if (typeName instanceof TypeNameArray array) { + addCodeBlockImport(array.getTypeOfArray()); + } else { + this.codeBlockImports.add(new RegularImport(typeName)); + } + } + + /** + * Adding an import for a type used in the code block (convenience overload accepting Class). + * + * @param clazz the class to import + */ + public void addCodeBlockImport(Class clazz) { + addCodeBlockImport(TypeName.of(clazz)); + } + + /** + * Adds a static import for a method/field used in the code block (convenience method). + * + * @param clazz the class containing the static member + * @param memberName the name of the static member + */ + public void addStaticImport(Class clazz, String memberName) { + this.codeBlockImports.add(new StaticImport(TypeName.of(clazz), memberName)); + } + + /** + * Adds type imports for a TypeName and its generic type arguments (convenience method). + * + * @param type the type to add imports for + */ + public void addTypeImports(TypeName type) { + if (type == null) { + return; + } + + // Add the main type + addCodeBlockImport(type); + + // Add generic type arguments recursively + if (type instanceof TypeNameGeneric genericType) { + genericType.getInnerTypeArguments().forEach(this::addTypeImports); + } + } + + /** + * Appends additional code to the existing code format with string formatting support. + * + *

This method concatenates the provided formatted code string to the current code format, + * separated by a newline for proper formatting. This is useful for building up method bodies + * incrementally, especially when constructing complex code with multiple sections. + * + *

Supports the same formatting syntax as {@link String#format(String, Object...)}, allowing + * for dynamic value insertion using placeholders like %s, %d, etc. + * + * @param formatted the code fragment with format placeholders to append to the existing code + * format + * @param args the arguments to be formatted into the string + */ + public void append(String formatted, Object... args) { + String formattedCode = args.length > 0 ? String.format(formatted, args) : formatted; + if (StringUtils.isEmpty(codeFormat)) { + codeFormat = formattedCode; + } else { + codeFormat += "\n" + formattedCode; + } + } +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodCodeDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodCodeDto.java index d4d26099..71c75c4a 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodCodeDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodCodeDto.java @@ -24,170 +24,7 @@ package org.javahelpers.simple.builders.processor.model.method; -import java.util.ArrayList; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; -import org.apache.commons.lang3.StringUtils; -import org.javahelpers.simple.builders.processor.model.imports.ImportStatement; -import org.javahelpers.simple.builders.processor.model.imports.RegularImport; -import org.javahelpers.simple.builders.processor.model.imports.StaticImport; -import org.javahelpers.simple.builders.processor.model.type.TypeName; -import org.javahelpers.simple.builders.processor.model.type.TypeNameArray; -import org.javahelpers.simple.builders.processor.model.type.TypeNameGeneric; - /** DTO for holding information of code implementation. */ -public class MethodCodeDto { - /** Format of code. Holding placeholder for dynamic values. */ - private String codeFormat; - - /** List of placeholders in CodeFormat. Containing dynamic values too. */ - private final List> codeArguments = new ArrayList<>(); - - /** Types used in the code body that aren't covered by arguments. */ - private final Set codeBlockImports = new LinkedHashSet<>(); - - /** - * Setting format of code. - * - * @param codeFormat Codeformat - */ - public void setCodeFormat(String codeFormat) { - this.codeFormat = codeFormat; - } - - /** - * Adding an argument for Codeformat. Helperfunction to set text value. - * - * @param name name in codeformat - * @param value value to fill in codeformat - */ - public void addArgument(String name, String value) { - codeArguments.add(new MethodCodeStringPlaceholder(name, value)); - } - - /** - * Adding an argument for Codeformat. Helperfunction to set TypeName value. - * - * @param name name in codeformat - * @param value value to fill in codeformat - */ - public void addArgument(String name, TypeName value) { - codeArguments.add(new MethodCodeTypePlaceholder(name, value)); - addCodeBlockImport(value); - } - - /** - * Getter for Codeformat. - * - * @return codeformat - */ - public String getCodeFormat() { - return codeFormat; - } - - /** - * Getter for arguments in Codeformat. - * - * @return argument values - */ - @SuppressWarnings("java:S1452") - public List> getCodeArguments() { - return codeArguments; - } - - /** - * Checks if this code DTO has code content. - * - * @return true if code format is not null and not blank - */ - public boolean hasCode() { - return !StringUtils.isBlank(codeFormat); - } - - /** - * Returns the set of imports used in the code body that aren't covered by arguments. - * - * @return set of imports used in code body - */ - public Set getCodeBlockImports() { - return codeBlockImports; - } - - /** - * Adding an import for a type used in the code block. - * - * @param typeName type to import - */ - public void addCodeBlockImport(TypeName typeName) { - if (typeName instanceof TypeNameGeneric generic) { - addCodeBlockImport(generic.getRawType()); - generic.getInnerTypeArguments().forEach(this::addCodeBlockImport); - } else if (typeName instanceof TypeNameArray array) { - addCodeBlockImport(array.getTypeOfArray()); - } else { - this.codeBlockImports.add(new RegularImport(typeName)); - } - } - - /** - * Adding an import for a type used in the code block (convenience overload accepting Class). - * - * @param clazz the class to import - */ - public void addCodeBlockImport(Class clazz) { - addCodeBlockImport(TypeName.of(clazz)); - } - - /** - * Adds a static import for a method/field used in the code block (convenience method). - * - * @param clazz the class containing the static member - * @param memberName the name of the static member - */ - public void addStaticImport(Class clazz, String memberName) { - this.codeBlockImports.add(new StaticImport(TypeName.of(clazz), memberName)); - } - - /** - * Adds type imports for a TypeName and its generic type arguments (convenience method). - * - * @param type the type to add imports for - */ - public void addTypeImports(TypeName type) { - if (type == null) { - return; - } - - // Add the main type - addCodeBlockImport(type); - - // Add generic type arguments recursively - if (type instanceof TypeNameGeneric genericType) { - genericType.getInnerTypeArguments().forEach(this::addTypeImports); - } - } - - /** - * Appends additional code to the existing code format with string formatting support. - * - *

This method concatenates the provided formatted code string to the current code format, - * separated by a newline for proper formatting. This is useful for building up method bodies - * incrementally, especially when constructing complex code with multiple sections. - * - *

Supports the same formatting syntax as {@link String#format(String, Object...)}, allowing - * for dynamic value insertion using placeholders like %s, %d, etc. - * - * @param formatted the code fragment with format placeholders to append to the existing code - * format - * @param args the arguments to be formatted into the string - */ - public void append(String formatted, Object... args) { - String formattedCode = args.length > 0 ? String.format(formatted, args) : formatted; - if (StringUtils.isEmpty(codeFormat)) { - codeFormat = formattedCode; - } else { - codeFormat += "\n" + formattedCode; - } - } +public class MethodCodeDto extends CodeTemplateDto { + // All functionality inherited from CodeTemplateDto } diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderConfigurationReaderTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderConfigurationReaderTest.java index f88df074..eb764cb8 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderConfigurationReaderTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderConfigurationReaderTest.java @@ -474,6 +474,12 @@ public class PersonDto { * This builder provides a fluent API for creating instances of test.PersonDto with method chaining and validation. Use * the static {@code create()} method to obtain a new builder instance, configure the desired properties using the * setter methods, and then call {@code build()} to create the final DTO. + * + *

Example:

+ * + *
{@code
+         * PersonDto result = PersonDtoMinimalBuilder.create().withName("example value").build();
+         * }
*/ public class PersonDtoMinimalBuilder implements IBuilderBase { @@ -505,6 +511,12 @@ public PersonDtoMinimalBuilder(PersonDto instance) { /** * Creating a new builder for {@code test.PersonDto}. * + *

Example:

+ * + *
{@code
+           * PersonDtoMinimalBuilder builder = PersonDtoMinimalBuilder.create();
+           * }
+ * * @return builder for {@code test.PersonDto} */ public static PersonDtoMinimalBuilder create() { @@ -514,6 +526,12 @@ public static PersonDtoMinimalBuilder create() { /** * Sets the value for name. * + *

Example:

+ * + *
{@code
+           * builder.withName("example value");
+           * }
+ * * @param name name * @return current instance of builder */ @@ -535,6 +553,12 @@ public PersonDtoMinimalBuilder withTags(List tags) { /** * Builds the configured DTO instance. + * + *

Example:

+ * + *
{@code
+           * PersonDto result = builder.build();
+           * }
*/ @Override public PersonDto build() { diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderJavadocExampleTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderJavadocExampleTest.java index d4ccf3a2..c20d40c7 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderJavadocExampleTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderJavadocExampleTest.java @@ -94,16 +94,15 @@ void shouldGenerateClassJavadocExampleWithKitchenSinkChain() { generatedCode, """ *

Example:

+ * *
{@code
         * BookDto result = BookDtoBuilder.create()
-        *     .title("example value")
-        *     .titleSupplier(() -> "example value")
         *     .pages(42)
-        *     .pagesSupplier(() -> 42)
-        *     .tags(List.of("example value"))
-        *     .tagsSupplier(() -> List.of("example value"))
+        *     .pages(() -> 42)
         *     .tags(t -> t.add("example value"))
         *     .add2Tags("example value")
+        *     .title("example value")
+        *     .title(() -> "example value")
         *     .build();
         * }
"""); @@ -134,10 +133,13 @@ void shouldGenerateMethodJavadocExampleForBasicStringSetter() { generatedCode, """ * Sets the value for teamname. + * *

Example:

+ * *
{@code
         * builder.teamname("example value");
         * }
+ * * @param teamname teamname * @return current instance of builder */ @@ -169,10 +171,13 @@ void shouldGenerateMethodJavadocExampleForPrimitiveSetter() { generatedCode, """ * Sets the value for amount. + * *

Example:

+ * *
{@code
         * builder.amount(42);
         * }
+ * * @param amount amount * @return current instance of builder */ @@ -204,8 +209,9 @@ void shouldGenerateMethodJavadocExampleForSupplier() { generatedCode, """ *

Example:

+ * *
{@code
-        * builder.titleSupplier(() -> "example value");
+        * builder.title(() -> "example value");
         * }
"""); } @@ -234,10 +240,13 @@ void shouldGenerateMethodJavadocExampleForAddToCollection() { generatedCode, """ * Adds a single element to tags. + * *

Example:

+ * *
{@code
         * builder.add2Tags("example value");
         * }
+ * * @param element the element to add * @return current instance of builder */ @@ -269,6 +278,7 @@ void shouldGenerateMethodJavadocExampleForListConsumer() { generatedCode, """ *

Example:

+ * *
{@code
         * builder.tags(t -> t.add("example value"));
         * }
@@ -299,6 +309,7 @@ void shouldGenerateMethodJavadocExampleForCreate() { generatedCode, """ *

Example:

+ * *
{@code
         * CreateDtoBuilder builder = CreateDtoBuilder.create();
         * }
@@ -329,6 +340,7 @@ void shouldGenerateMethodJavadocExampleForBuild() { generatedCode, """ *

Example:

+ * *
{@code
         * BuildDto result = builder.build();
         * }
@@ -416,20 +428,12 @@ public class HelperPlain { public HelperPlain() {} } String generatedCode = loadGeneratedSource(compilation, builderClassName); assertGenerationSucceeded(compilation, builderClassName, generatedCode); - // Class-level example contains ONLY the resolvable field's lines ProcessorAsserts.assertContaining( generatedCode, - """ - *

Example:

- *
{@code
-        * MixedDto result = MixedDtoBuilder.create()
-        *     .title("example value")
-        *     .titleSupplier(() -> "example value")
-        *     .build();
-        * }
- """); + "MixedDto result = MixedDtoBuilder.create().title(\"example value\")" + + ".title(() -> \"example value\").build();"); - // ...but NO `.helper(...)` line in the class example chain + // ...but NO `.helper(...)` call in the class example chain ProcessorAsserts.assertNotContaining(generatedCode, ".helper(null)", ".helper(\""); } 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 026fc763..a3b4af88 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 @@ -113,13 +113,13 @@ void shouldLogDebugMessagesWhenVerboseModeEnabled() { "[DEBUG] │ │ │ └─ Adding field: name (type: java.lang.String)", "[DEBUG] │ │ └─ Processed 1 possible setters: added 1 fields, skipped 0", "[DEBUG] │ ├─ Processing class based enhancer", - "[DEBUG] │ │ ├─ Applying: ClassJavaDocEnhancer (priority: 200)", "[DEBUG] │ │ ├─ Applying: GeneratedAnnotationEnhancer (priority: 120)", "[DEBUG] │ │ ├─ Applying: BuilderImplementationAnnotationEnhancer (priority: 115)", "[DEBUG] │ │ ├─ Applying: CoreMethodsEnhancer (priority: 100)", "[DEBUG] │ │ ├─ Applying: WithInterfaceEnhancer (priority: 95)", "[DEBUG] │ │ ├─ Applying: InterfaceEnhancer (priority: 90)", "[DEBUG] │ │ ├─ Applying: ConditionalEnhancer (priority: 80)", + "[DEBUG] │ │ ├─ Applying: ClassJavaDocEnhancer (priority: 10)", "[DEBUG] │ │ └─ Applied 8 builder enhancers", "[DEBUG] │ ├─ Finalizing builder definition", "[DEBUG] │ │ └─ Finalized: 1 class fields, 9 methods, 2 constructors", diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/ComprehensiveFeatureIntegrationTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/ComprehensiveFeatureIntegrationTest.java index 4cd832b2..e6ee17a1 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/ComprehensiveFeatureIntegrationTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/ComprehensiveFeatureIntegrationTest.java @@ -147,7 +147,6 @@ public PersonDto(String name, int age, Optional email, 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.ArrayList; import java.util.HashSet; import java.util.LinkedList; @@ -176,6 +175,24 @@ public PersonDto(String name, int age, Optional email, * This builder provides a fluent API for creating instances of test.PersonDto with method chaining and validation. Use * the static {@code create()} method to obtain a new builder instance, configure the desired properties using the * setter methods, and then call {@code build()} to create the final DTO. + * + *

Example:

+ * + *
{@code
+         * PersonDto result = PersonDtoBuilder.create()
+         *     .name("example value")
+         *     .name(() -> "example value")
+         *     .age(42)
+         *     .age(() -> 42)
+         *     .nicknames(t -> t.add("example value"))
+         *     .add2Nicknames("example value")
+         *     .add2Tags("example value")
+         *     .previousAddresses(t -> t.add("example value"))
+         *     .add2PreviousAddresses("example value")
+         *     .phoneNumbers(t -> t.add("example value"))
+         *     .add2PhoneNumbers("example value")
+         *     .build();
+         * }
*/ @Generated("Generated by org.javahelpers.simple.builders.processor.BuilderProcessor") @BuilderImplementation(forClass = PersonDto.class) @@ -185,42 +202,34 @@ public class PersonDtoBuilder implements IBuilderBase { * Tracked value for name: name. */ private TrackedValue name = unsetValue(); - /** * Tracked value for age: age. */ private TrackedValue age = unsetValue(); - /** * Tracked value for email: email. */ private TrackedValue> email = unsetValue(); - /** * Tracked value for nicknames: nicknames. */ private TrackedValue> nicknames = unsetValue(); - /** * Tracked value for tags: tags. */ private TrackedValue> tags = unsetValue(); - /** * Tracked value for metadata: metadata. */ private TrackedValue> metadata = unsetValue(); - /** * Tracked value for address: address. */ private TrackedValue address = unsetValue(); - /** * Tracked value for previousAddresses: previousAddresses. */ private TrackedValue> previousAddresses = unsetValue(); - /** * Tracked value for phoneNumbers: phoneNumbers. */ @@ -252,6 +261,12 @@ public PersonDtoBuilder(PersonDto instance) { /** * Creating a new builder for {@code test.PersonDto}. * + *

Example:

+ * + *
{@code
+           * PersonDtoBuilder builder = PersonDtoBuilder.create();
+           * }
+ * * @return builder for {@code test.PersonDto} */ public static PersonDtoBuilder create() { @@ -261,6 +276,12 @@ public static PersonDtoBuilder create() { /** * Adds a single element to nicknames. * + *

Example:

+ * + *
{@code
+           * builder.add2Nicknames("example value");
+           * }
+ * * @param element the element to add * @return current instance of builder */ @@ -279,6 +300,12 @@ public PersonDtoBuilder add2Nicknames(String element) { /** * Adds a single element to phoneNumbers. * + *

Example:

+ * + *
{@code
+           * builder.add2PhoneNumbers("example value");
+           * }
+ * * @param element the element to add * @return current instance of builder */ @@ -297,6 +324,12 @@ public PersonDtoBuilder add2PhoneNumbers(String element) { /** * Adds a single element to previousAddresses. * + *

Example:

+ * + *
{@code
+           * builder.add2PreviousAddresses("example value");
+           * }
+ * * @param element the element to add * @return current instance of builder */ @@ -315,6 +348,12 @@ public PersonDtoBuilder add2PreviousAddresses(AddressDto element) { /** * Adds a single element to tags. * + *

Example:

+ * + *
{@code
+           * builder.add2Tags("example value");
+           * }
+ * * @param element the element to add * @return current instance of builder */ @@ -370,6 +409,12 @@ public PersonDtoBuilder address(Supplier addressSupplier) { /** * Sets the value for age. * + *

Example:

+ * + *
{@code
+           * builder.age(42);
+           * }
+ * * @param age age * @return current instance of builder */ @@ -381,6 +426,12 @@ public PersonDtoBuilder age(int age) { /** * Sets the value for age by invoking the provided supplier. * + *

Example:

+ * + *
{@code
+           * builder.age(() -> 42);
+           * }
+ * * @param ageSupplier supplier for age * @return current instance of builder */ @@ -499,6 +550,12 @@ public PersonDtoBuilder metadata(Supplier> metadataSupplier) /** * Sets the value for name. * + *

Example:

+ * + *
{@code
+           * builder.name("example value");
+           * }
+ * * @param name name * @return current instance of builder */ @@ -523,6 +580,12 @@ public PersonDtoBuilder name(Consumer nameStringBuilderConsumer) /** * Sets the value for name by invoking the provided supplier. * + *

Example:

+ * + *
{@code
+           * builder.name(() -> "example value");
+           * }
+ * * @param nameSupplier supplier for name * @return current instance of builder */ @@ -569,6 +632,12 @@ public PersonDtoBuilder nicknames(List nicknames) { /** * Sets the value for nicknames using a builder consumer that produces the value. * + *

Example:

+ * + *
{@code
+           * builder.nicknames(t -> t.add("example value"));
+           * }
+ * * @param nicknamesBuilderConsumer consumer providing an instance of a builder for nicknames * @return current instance of builder */ @@ -617,6 +686,12 @@ public PersonDtoBuilder phoneNumbers(LinkedList phoneNumbers) { /** * Sets the value for phoneNumbers using a builder consumer that produces the value. * + *

Example:

+ * + *
{@code
+           * builder.phoneNumbers(t -> t.add("example value"));
+           * }
+ * * @param phoneNumbersBuilderConsumer consumer providing an instance of a builder for phoneNumbers * @return current instance of builder */ @@ -665,6 +740,12 @@ public PersonDtoBuilder previousAddresses(List previousAddresses) { /** * Sets the value for previousAddresses using a builder consumer that produces the value. * + *

Example:

+ * + *
{@code
+           * builder.previousAddresses(t -> t.add("example value"));
+           * }
+ * * @param previousAddressesBuilderConsumer consumer providing an instance of a builder for previousAddresses * @return current instance of builder */ @@ -769,6 +850,12 @@ public PersonDtoBuilder conditional(BooleanSupplier condition, ConsumerExample: + * + *
{@code
+           * PersonDto result = builder.build();
+           * }
*/ @Override public PersonDto build() { From e68256669bae6a2fa29c875dae8967368243cb18 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Tue, 5 May 2026 20:13:39 +0200 Subject: [PATCH 03/23] Using exampleChainFragment for JavaDoc-Example of method and class --- .../builder/ClassJavaDocEnhancer.java | 43 +++++++++++++------ .../field/AddToCollectionGenerator.java | 19 ++------ .../field/BasicSetterGenerator.java | 25 +++-------- .../field/ListConsumerGenerator.java | 19 ++------ .../field/SupplierMethodGenerator.java | 25 +++-------- .../model/core/BuilderDefinitionDto.java | 37 ---------------- .../processor/model/method/MethodDto.java | 31 +++++++++++++ 7 files changed, 84 insertions(+), 115 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ClassJavaDocEnhancer.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ClassJavaDocEnhancer.java index ef573261..ef637c70 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ClassJavaDocEnhancer.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ClassJavaDocEnhancer.java @@ -26,7 +26,9 @@ import org.javahelpers.simple.builders.processor.generators.BuilderEnhancer; import org.javahelpers.simple.builders.processor.model.core.BuilderDefinitionDto; +import org.javahelpers.simple.builders.processor.model.javadoc.JavadocCodeBlockDto; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; +import org.javahelpers.simple.builders.processor.model.method.MethodDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.processing.ProcessingContext; @@ -88,22 +90,39 @@ public boolean appliesTo( public void enhanceBuilder(BuilderDefinitionDto builderDto, ProcessingContext context) { TypeName targetType = builderDto.getBuildingTargetTypeName(); JavadocDto javadoc = createClassJavadoc(targetType); + String indentionString = " "; - // Finalize class-level example if any lines were contributed - if (builderDto.getClassExampleBlock() != null && builderDto.getClassExampleBlock().hasCode()) { - // Prepend opening line + // Synthesise example blocks from the fluent-chain fragments stored on methods. This keeps the + // per-method "builder.field(value);" example and the class-level kitchen-sink chain in sync + // with a single source of truth (the fragment on the MethodDto). + // Note: Methods are stored in FieldDto objects at this point (before finalizeDefinition). + JavadocCodeBlockDto classExampleBlock = new JavadocCodeBlockDto(); + for (org.javahelpers.simple.builders.processor.model.core.FieldDto field : + builderDto.getAllFieldsForBuilder()) { + for (MethodDto method : field.getMethods()) { + String fragment = method.getExampleChainFragment(); + if (fragment == null) { + continue; + } + // Method-level example: wrap fragment as "builder;" + JavadocCodeBlockDto methodExample = new JavadocCodeBlockDto(); + methodExample.setCodeFormat("builder%s;".formatted(fragment)); + if (method.getJavadoc() != null) { + method.getJavadoc().addExample(methodExample); + } + + // Class-level aggregation: indent and collect all fragments + classExampleBlock.append("%s%s", indentionString, fragment); + } + } + + if (classExampleBlock.hasCode()) { String builderTypeName = builderDto.getBuilderTypeName().getClassName(); String openingLine = "%s result = %s.create()".formatted(targetType.getClassName(), builderTypeName); - builderDto - .getClassExampleBlock() - .setCodeFormat(openingLine + "\n" + builderDto.getClassExampleBlock().getCodeFormat()); - - // Append closing line - builderDto.getClassExampleBlock().append(".build();"); - - // Add the example block to the class Javadoc - javadoc.addExample(builderDto.getClassExampleBlock()); + classExampleBlock.setCodeFormat(openingLine + "\n" + classExampleBlock.getCodeFormat()); + classExampleBlock.append("%s.build();", indentionString); + javadoc.addExample(classExampleBlock); } builderDto.setClassJavadoc(javadoc); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/AddToCollectionGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/AddToCollectionGenerator.java index 0a2601c2..4136309a 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/AddToCollectionGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/AddToCollectionGenerator.java @@ -32,7 +32,6 @@ import org.javahelpers.simple.builders.processor.generators.MethodGenerator; import org.javahelpers.simple.builders.processor.generators.util.JavadocConstants; import org.javahelpers.simple.builders.processor.model.core.FieldDto; -import org.javahelpers.simple.builders.processor.model.javadoc.JavadocCodeBlockDto; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; import org.javahelpers.simple.builders.processor.model.method.MethodDto; import org.javahelpers.simple.builders.processor.model.method.MethodParameterDto; @@ -188,20 +187,10 @@ private MethodDto createAddToCollectionMethod( .addParam("element", "the element to add") .addReturn(JavadocConstants.RETURN_BUILDER_INSTANCE)); - // Add method-level example for add-to-collection method - if (methodDto.getJavadoc() != null) { - JavadocCodeBlockDto exampleBlock = new JavadocCodeBlockDto(); - exampleBlock.setCodeFormat( - "builder.%s(\"example value\");".formatted(methodDto.getMethodName())); - methodDto.getJavadoc().addExample(exampleBlock); - } - - // Contribute to class-level example - if (context.getCurrentBuilderDto() != null) { - context - .getCurrentBuilderDto() - .addClassExampleLine(" .%s(\"example value\")".formatted(methodDto.getMethodName())); - } + // Store the fluent-chain fragment so the class-level enhancer can synthesise both the + // method-level example block and the class-level kitchen-sink chain from one source of truth. + methodDto.setExampleChainFragment( + ".%s(\"example value\")".formatted(methodDto.getMethodName())); return methodDto; } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/BasicSetterGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/BasicSetterGenerator.java index 7076c97c..db16c033 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/BasicSetterGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/BasicSetterGenerator.java @@ -28,11 +28,9 @@ import java.util.Collections; import java.util.List; -import java.util.Optional; import org.javahelpers.simple.builders.processor.generators.MethodGenerator; import org.javahelpers.simple.builders.processor.generators.util.JavadocExampleValues; import org.javahelpers.simple.builders.processor.model.core.FieldDto; -import org.javahelpers.simple.builders.processor.model.javadoc.JavadocCodeBlockDto; import org.javahelpers.simple.builders.processor.model.method.MethodDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.processing.ProcessingContext; @@ -94,22 +92,13 @@ public List generateMethods( createBuilderMethodForFieldWithTransform( field, null, field.getFieldType(), builderType, context); - // Add method-level example if we have an example value for the field type - Optional exampleValue = JavadocExampleValues.getExampleValue(field.getFieldType()); - if (exampleValue.isPresent() && setterMethod.getJavadoc() != null) { - JavadocCodeBlockDto exampleBlock = new JavadocCodeBlockDto(); - exampleBlock.setCodeFormat( - "builder.%s(%s);".formatted(setterMethod.getMethodName(), exampleValue.get())); - setterMethod.getJavadoc().addExample(exampleBlock); - } - - // Contribute to class-level example if we have an example value - if (exampleValue.isPresent() && context.getCurrentBuilderDto() != null) { - String methodName = setterMethod.getMethodName(); - context - .getCurrentBuilderDto() - .addClassExampleLine(" .%s(%s)".formatted(methodName, exampleValue.get())); - } + // Store the fluent-chain fragment so the class-level enhancer can synthesise both the + // method-level example block and the class-level kitchen-sink chain from one source of truth. + JavadocExampleValues.getExampleValue(field.getFieldType()) + .ifPresent( + example -> + setterMethod.setExampleChainFragment( + ".%s(%s)".formatted(setterMethod.getMethodName(), example))); return Collections.singletonList(setterMethod); } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ListConsumerGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ListConsumerGenerator.java index 0d78788e..9a717cdd 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ListConsumerGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ListConsumerGenerator.java @@ -35,7 +35,6 @@ import org.javahelpers.simple.builders.core.builders.ArrayListBuilderWithElementBuilders; import org.javahelpers.simple.builders.processor.generators.MethodGenerator; import org.javahelpers.simple.builders.processor.model.core.FieldDto; -import org.javahelpers.simple.builders.processor.model.javadoc.JavadocCodeBlockDto; import org.javahelpers.simple.builders.processor.model.method.MethodDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.model.type.TypeNameGeneric; @@ -170,19 +169,9 @@ public List generateMethods( */ private void addExampleToListConsumer( MethodDto method, String fieldName, ProcessingContext context) { - if (method.getJavadoc() != null) { - JavadocCodeBlockDto exampleBlock = new JavadocCodeBlockDto(); - exampleBlock.setCodeFormat( - "builder.%s(t -> t.add(\"example value\"));".formatted(method.getMethodName())); - method.getJavadoc().addExample(exampleBlock); - } - - // Contribute to class-level example - if (context.getCurrentBuilderDto() != null) { - context - .getCurrentBuilderDto() - .addClassExampleLine( - " .%s(t -> t.add(\"example value\"))".formatted(method.getMethodName())); - } + // Store the fluent-chain fragment so the class-level enhancer can synthesise both the + // method-level example block and the class-level kitchen-sink chain from one source of truth. + method.setExampleChainFragment( + ".%s(t -> t.add(\"example value\"))".formatted(method.getMethodName())); } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/SupplierMethodGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/SupplierMethodGenerator.java index c7bd27d2..bbab2213 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/SupplierMethodGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/SupplierMethodGenerator.java @@ -29,13 +29,11 @@ import java.util.Collections; import java.util.List; -import java.util.Optional; import java.util.function.Supplier; import org.javahelpers.simple.builders.processor.generators.MethodGenerator; import org.javahelpers.simple.builders.processor.generators.util.JavadocConstants; import org.javahelpers.simple.builders.processor.generators.util.JavadocExampleValues; import org.javahelpers.simple.builders.processor.model.core.FieldDto; -import org.javahelpers.simple.builders.processor.model.javadoc.JavadocCodeBlockDto; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; import org.javahelpers.simple.builders.processor.model.method.MethodDto; import org.javahelpers.simple.builders.processor.model.method.MethodParameterDto; @@ -154,22 +152,13 @@ private MethodDto createFieldSupplier( .addParam(parameterName, "supplier for %s", fieldJavaDoc) .addReturn(JavadocConstants.RETURN_BUILDER_INSTANCE)); - // Add method-level example if we have an example value for the field type - Optional exampleValue = JavadocExampleValues.getExampleValue(fieldType); - if (exampleValue.isPresent()) { - JavadocCodeBlockDto exampleBlock = new JavadocCodeBlockDto(); - exampleBlock.setCodeFormat( - "builder.%s(() -> %s);".formatted(methodDto.getMethodName(), exampleValue.get())); - methodDto.getJavadoc().addExample(exampleBlock); - } - - // Contribute to class-level example if we have an example value - if (exampleValue.isPresent() && context.getCurrentBuilderDto() != null) { - context - .getCurrentBuilderDto() - .addClassExampleLine( - " .%s(() -> %s)".formatted(methodDto.getMethodName(), exampleValue.get())); - } + // Store the fluent-chain fragment so the class-level enhancer can synthesise both the + // method-level example block and the class-level kitchen-sink chain from one source of truth. + JavadocExampleValues.getExampleValue(fieldType) + .ifPresent( + example -> + methodDto.setExampleChainFragment( + ".%s(() -> %s)".formatted(methodDto.getMethodName(), example))); return methodDto; } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderDefinitionDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderDefinitionDto.java index 935fde3d..7bc198e2 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderDefinitionDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderDefinitionDto.java @@ -26,7 +26,6 @@ import java.util.LinkedList; import java.util.List; -import org.javahelpers.simple.builders.processor.model.javadoc.JavadocCodeBlockDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; /** @@ -55,9 +54,6 @@ public class BuilderDefinitionDto extends GenerationTargetClassDto { /** Configuration for builder generation. */ private BuilderConfiguration configuration; - /** Code block for class-level Javadoc example. */ - private JavadocCodeBlockDto classExampleBlock; - /** * Getting type of builder. Delegates to base class typeName. * @@ -178,37 +174,4 @@ public BuilderConfiguration getConfiguration() { public void setConfiguration(BuilderConfiguration configuration) { this.configuration = configuration; } - - /** - * Gets the class-level Javadoc example code block. - * - * @return the class example block, or null if not set - */ - public JavadocCodeBlockDto getClassExampleBlock() { - return classExampleBlock; - } - - /** - * Sets the class-level Javadoc example code block. - * - * @param classExampleBlock the class example block - */ - public void setClassExampleBlock(JavadocCodeBlockDto classExampleBlock) { - this.classExampleBlock = classExampleBlock; - } - - /** - * Adds a line to the class-level Javadoc example code block. - * - *

If the class example block doesn't exist, it will be created. This method appends the line - * to the existing code format. - * - * @param line the line to add to the class example - */ - public void addClassExampleLine(String line) { - if (classExampleBlock == null) { - classExampleBlock = new JavadocCodeBlockDto(); - } - classExampleBlock.append(line); - } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodDto.java index 896986f9..935537cb 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodDto.java @@ -76,6 +76,14 @@ public class MethodDto { /** Definition of inner implementation for method. */ private final MethodCodeDto methodCodeDto = new MethodCodeDto(); + /** + * Fluent-chain fragment describing how this method is invoked in Javadoc examples (e.g., {@code + * .title("example value")}). When present, downstream enhancers use it to synthesise the + * method-level example block (as {@code builder;}) and to aggregate the class-level + * kitchen-sink chain. A {@code null} value means the method should not appear in either example. + */ + private String exampleChainFragment; + /** Default constructor. */ public MethodDto() { // Default constructor @@ -184,6 +192,29 @@ public MethodCodeDto getMethodCodeDto() { return methodCodeDto; } + /** + * Returns the fluent-chain fragment for Javadoc examples (e.g. {@code .title("example value")}) + * or {@code null} if this method should not participate in example generation. + * + * @return the fragment or {@code null} + */ + public String getExampleChainFragment() { + return exampleChainFragment; + } + + /** + * Stores the fluent-chain fragment describing how this method is invoked in examples. + * + *

The fragment must start with {@code .} and contain just the method invocation, e.g. {@code + * .title("example value")}. Downstream enhancers synthesise the method-level example block (as + * {@code builder;}) and the class-level kitchen-sink chain from it. + * + * @param exampleChainFragment the fragment or {@code null} to clear + */ + public void setExampleChainFragment(String exampleChainFragment) { + this.exampleChainFragment = exampleChainFragment; + } + /** * Checks if the constructor has a code block. * From 773442f4314f7bcf23f8b781e82db2e0ebe6f2e6 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Tue, 5 May 2026 20:30:39 +0200 Subject: [PATCH 04/23] Improving code which adds exampleCodeFragment by doing it with MethodGeneratorUtil --- .../builder/ClassJavaDocEnhancer.java | 9 +-- .../field/AddToCollectionGenerator.java | 7 +- .../field/BasicSetterGenerator.java | 12 +-- .../field/ListConsumerGenerator.java | 8 +- .../field/SupplierMethodGenerator.java | 15 ++-- .../generators/util/MethodGeneratorUtil.java | 77 +++++++++++++++++++ .../processor/model/method/MethodDto.java | 16 +++- 7 files changed, 109 insertions(+), 35 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ClassJavaDocEnhancer.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ClassJavaDocEnhancer.java index ef637c70..f4592c4a 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ClassJavaDocEnhancer.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ClassJavaDocEnhancer.java @@ -104,15 +104,8 @@ public void enhanceBuilder(BuilderDefinitionDto builderDto, ProcessingContext co if (fragment == null) { continue; } - // Method-level example: wrap fragment as "builder;" - JavadocCodeBlockDto methodExample = new JavadocCodeBlockDto(); - methodExample.setCodeFormat("builder%s;".formatted(fragment)); - if (method.getJavadoc() != null) { - method.getJavadoc().addExample(methodExample); - } - // Class-level aggregation: indent and collect all fragments - classExampleBlock.append("%s%s", indentionString, fragment); + classExampleBlock.append("%s.%s".formatted(indentionString, fragment)); } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/AddToCollectionGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/AddToCollectionGenerator.java index 4136309a..e5e5239e 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/AddToCollectionGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/AddToCollectionGenerator.java @@ -24,7 +24,9 @@ package org.javahelpers.simple.builders.processor.generators.field; -import static org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil.*; +import static org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil.TRACKED_VALUE_TYPE; +import static org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil.addExampleChainFragmentWithHardcodedValue; +import static org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil.getMethodAccessModifier; import java.util.ArrayList; import java.util.List; @@ -189,8 +191,7 @@ private MethodDto createAddToCollectionMethod( // Store the fluent-chain fragment so the class-level enhancer can synthesise both the // method-level example block and the class-level kitchen-sink chain from one source of truth. - methodDto.setExampleChainFragment( - ".%s(\"example value\")".formatted(methodDto.getMethodName())); + addExampleChainFragmentWithHardcodedValue(methodDto, "\"example value\""); return methodDto; } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/BasicSetterGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/BasicSetterGenerator.java index db16c033..366e58fc 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/BasicSetterGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/BasicSetterGenerator.java @@ -24,12 +24,10 @@ package org.javahelpers.simple.builders.processor.generators.field; -import static org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil.*; - import java.util.Collections; import java.util.List; import org.javahelpers.simple.builders.processor.generators.MethodGenerator; -import org.javahelpers.simple.builders.processor.generators.util.JavadocExampleValues; +import org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil; import org.javahelpers.simple.builders.processor.model.core.FieldDto; import org.javahelpers.simple.builders.processor.model.method.MethodDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; @@ -89,16 +87,12 @@ public List generateMethods( FieldDto field, TypeName builderType, ProcessingContext context) { MethodDto setterMethod = - createBuilderMethodForFieldWithTransform( + MethodGeneratorUtil.createBuilderMethodForFieldWithTransform( field, null, field.getFieldType(), builderType, context); // Store the fluent-chain fragment so the class-level enhancer can synthesise both the // method-level example block and the class-level kitchen-sink chain from one source of truth. - JavadocExampleValues.getExampleValue(field.getFieldType()) - .ifPresent( - example -> - setterMethod.setExampleChainFragment( - ".%s(%s)".formatted(setterMethod.getMethodName(), example))); + MethodGeneratorUtil.addExampleChainFragment(setterMethod, field.getFieldType()); return Collections.singletonList(setterMethod); } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ListConsumerGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ListConsumerGenerator.java index 9a717cdd..54922e9e 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ListConsumerGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ListConsumerGenerator.java @@ -25,7 +25,9 @@ package org.javahelpers.simple.builders.processor.generators.field; import static org.javahelpers.simple.builders.processor.analysis.JavaLangMapper.map2TypeName; -import static org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil.*; +import static org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil.addExampleChainFragmentCustom; +import static org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil.createFieldConsumerWithBuilder; +import static org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil.createFieldConsumerWithElementBuilders; import java.util.Collections; import java.util.List; @@ -171,7 +173,7 @@ private void addExampleToListConsumer( MethodDto method, String fieldName, ProcessingContext context) { // Store the fluent-chain fragment so the class-level enhancer can synthesise both the // method-level example block and the class-level kitchen-sink chain from one source of truth. - method.setExampleChainFragment( - ".%s(t -> t.add(\"example value\"))".formatted(method.getMethodName())); + addExampleChainFragmentCustom( + method, "%s(t -> t.add(\"example value\"))".formatted(method.getMethodName())); } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/SupplierMethodGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/SupplierMethodGenerator.java index bbab2213..386f83b0 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/SupplierMethodGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/SupplierMethodGenerator.java @@ -25,14 +25,17 @@ package org.javahelpers.simple.builders.processor.generators.field; import static org.javahelpers.simple.builders.processor.analysis.JavaLangMapper.map2TypeName; -import static org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil.*; +import static org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil.SUFFIX_SUPPLIER; +import static org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil.TRACKED_VALUE_TYPE; +import static org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil.addExampleChainFragmentWithSupplier; +import static org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil.generateBuilderMethodName; +import static org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil.getMethodAccessModifier; import java.util.Collections; import java.util.List; import java.util.function.Supplier; import org.javahelpers.simple.builders.processor.generators.MethodGenerator; import org.javahelpers.simple.builders.processor.generators.util.JavadocConstants; -import org.javahelpers.simple.builders.processor.generators.util.JavadocExampleValues; import org.javahelpers.simple.builders.processor.model.core.FieldDto; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; import org.javahelpers.simple.builders.processor.model.method.MethodDto; @@ -152,13 +155,7 @@ private MethodDto createFieldSupplier( .addParam(parameterName, "supplier for %s", fieldJavaDoc) .addReturn(JavadocConstants.RETURN_BUILDER_INSTANCE)); - // Store the fluent-chain fragment so the class-level enhancer can synthesise both the - // method-level example block and the class-level kitchen-sink chain from one source of truth. - JavadocExampleValues.getExampleValue(fieldType) - .ifPresent( - example -> - methodDto.setExampleChainFragment( - ".%s(() -> %s)".formatted(methodDto.getMethodName(), example))); + addExampleChainFragmentWithSupplier(methodDto, fieldType); return methodDto; } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java index 759d3595..fcf4475f 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java @@ -379,4 +379,81 @@ public static MethodDto createSimpleFieldConsumer( return methodDto; } + + /** + * Adds the fluent-chain fragment for Javadoc examples to a method. + * + *

This helper method retrieves an example value for the given field type and formats it as a + * fluent-chain fragment (e.g., {@code methodName(exampleValue)}). The fragment is stored on the + * MethodDto for later use by the ClassJavaDocEnhancer to synthesize both method-level and + * class-level Javadoc examples. + * + *

If no example value is available for the field type, the fragment is not added. + * + * @param methodDto the method DTO to add the fragment to + * @param fieldType the field type to get an example value for + */ + public static void addExampleChainFragment(MethodDto methodDto, TypeName fieldType) { + JavadocExampleValues.getExampleValue(fieldType) + .ifPresent( + example -> + methodDto.setExampleChainFragment( + "%s(%s)".formatted(methodDto.getMethodName(), example))); + } + + /** + * Adds the fluent-chain fragment for Javadoc examples to a method using a supplier pattern. + * + *

This helper method retrieves an example value for the given field type and formats it as a + * fluent-chain fragment with a supplier (e.g., {@code methodName(() -> exampleValue)}). The + * fragment is stored on the MethodDto for later use by the ClassJavaDocEnhancer to synthesize + * both method-level and class-level Javadoc examples. + * + *

If no example value is available for the field type, the fragment is not added. + * + * @param methodDto the method DTO to add the fragment to + * @param fieldType the field type to get an example value for + */ + public static void addExampleChainFragmentWithSupplier(MethodDto methodDto, TypeName fieldType) { + JavadocExampleValues.getExampleValue(fieldType) + .ifPresent( + example -> + methodDto.setExampleChainFragment( + "%s(() -> %s)".formatted(methodDto.getMethodName(), example))); + } + + /** + * Adds the fluent-chain fragment for Javadoc examples to a method with a hardcoded example value. + * + *

This helper method formats a hardcoded example value as a fluent-chain fragment (e.g., + * {@code methodName("example value")}). The fragment is stored on the MethodDto for later use by + * the ClassJavaDocEnhancer to synthesize both method-level and class-level Javadoc examples. + * + *

This is useful for generators that use a fixed example value rather than deriving it from + * the field type. + * + * @param methodDto the method DTO to add the fragment to + * @param exampleValue the hardcoded example value to use + */ + public static void addExampleChainFragmentWithHardcodedValue( + MethodDto methodDto, String exampleValue) { + methodDto.setExampleChainFragment("%s(%s)".formatted(methodDto.getMethodName(), exampleValue)); + } + + /** + * Adds the fluent-chain fragment for Javadoc examples to a method with a custom fragment format. + * + *

This helper method allows generators to specify a completely custom fragment format for + * special cases (e.g., lambda expressions). The fragment is stored on the MethodDto for later use + * by the ClassJavaDocEnhancer to synthesize both method-level and class-level Javadoc examples. + * + *

This is useful for generators with unique fragment patterns that don't fit the standard + * value or supplier patterns. + * + * @param methodDto the method DTO to add the fragment to + * @param fragment the custom fragment to add (e.g., {@code methodName(t -> t.add("value"))}) + */ + public static void addExampleChainFragmentCustom(MethodDto methodDto, String fragment) { + methodDto.setExampleChainFragment(fragment); + } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodDto.java index 935537cb..509804ae 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodDto.java @@ -205,14 +205,24 @@ public String getExampleChainFragment() { /** * Stores the fluent-chain fragment describing how this method is invoked in examples. * - *

The fragment must start with {@code .} and contain just the method invocation, e.g. {@code - * .title("example value")}. Downstream enhancers synthesise the method-level example block (as - * {@code builder;}) and the class-level kitchen-sink chain from it. + *

The fragment contains just the method invocation, e.g. {@code title("example value")}. + * Downstream enhancers synthesise the method-level example block (as {@code builder.;}) + * and the class-level kitchen-sink chain from it. + * + *

Automatically adds a method-level example to the javadoc if javadoc exists and has no + * existing examples (to avoid overriding manually set examples). * * @param exampleChainFragment the fragment or {@code null} to clear */ public void setExampleChainFragment(String exampleChainFragment) { this.exampleChainFragment = exampleChainFragment; + // Automatically add method-level example to javadoc if javadoc exists and has no examples + if (exampleChainFragment != null && javadoc != null && javadoc.getCodeBlocks().isEmpty()) { + org.javahelpers.simple.builders.processor.model.javadoc.JavadocCodeBlockDto methodExample = + new org.javahelpers.simple.builders.processor.model.javadoc.JavadocCodeBlockDto(); + methodExample.setCodeFormat("builder.%s;".formatted(exampleChainFragment)); + javadoc.addExample(methodExample); + } } /** From 09fc62afd6bf0ec43d7b0522b7c87954b0a54fbf Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Tue, 5 May 2026 21:25:53 +0200 Subject: [PATCH 05/23] Improving handling of templates in generation of example values --- .../field/ArrayConversionGenerator.java | 4 ++ .../field/ListConsumerGenerator.java | 15 ++--- .../field/OptionalHelperGenerator.java | 3 + .../field/StringBuilderConsumerGenerator.java | 5 ++ .../field/StringFormatHelperGenerator.java | 4 ++ .../field/VarArgsHelperGenerator.java | 15 +++-- .../generators/util/MethodGeneratorUtil.java | 66 +++++++++++++------ 7 files changed, 79 insertions(+), 33 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ArrayConversionGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ArrayConversionGenerator.java index 379172ad..b0802716 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ArrayConversionGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ArrayConversionGenerator.java @@ -135,6 +135,10 @@ private MethodDto createFieldSetterForArrayFromList( new JavadocDto("Sets the value for %s.", fieldName) .addParam(parameter.getParameterName(), fieldJavadocDesc) .addReturn(JavadocConstants.RETURN_BUILDER_INSTANCE)); + + // Add example fragment for array conversion method + addExampleChainFragment(methodDto, elementType); + return methodDto; } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ListConsumerGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ListConsumerGenerator.java index 54922e9e..0d1933a1 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ListConsumerGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ListConsumerGenerator.java @@ -25,7 +25,7 @@ package org.javahelpers.simple.builders.processor.generators.field; import static org.javahelpers.simple.builders.processor.analysis.JavaLangMapper.map2TypeName; -import static org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil.addExampleChainFragmentCustom; +import static org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil.addExampleChainFragmentTemplate; import static org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil.createFieldConsumerWithBuilder; import static org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil.createFieldConsumerWithElementBuilders; @@ -137,7 +137,7 @@ public List generateMethods( field, collectionBuilderType, elementBuilderType.get(), builderType, context); // Add method-level example for list consumer - addExampleToListConsumer(method, field.getOriginalFieldName(), context); + addExampleToListConsumer(method, elementType, context); return List.of(method); } else if (context.getConfiguration().shouldUseArrayListBuilder()) { @@ -154,7 +154,7 @@ public List generateMethods( context); // Add method-level example for list consumer - addExampleToListConsumer(method, field.getOriginalFieldName(), context); + addExampleToListConsumer(method, elementType, context); return List.of(method); } @@ -166,14 +166,11 @@ public List generateMethods( * Adds method-level example and contributes to class-level example for list consumer methods. * * @param method the method to add the example to - * @param fieldName the field name + * @param elementType the element type * @param context the processing context */ private void addExampleToListConsumer( - MethodDto method, String fieldName, ProcessingContext context) { - // Store the fluent-chain fragment so the class-level enhancer can synthesise both the - // method-level example block and the class-level kitchen-sink chain from one source of truth. - addExampleChainFragmentCustom( - method, "%s(t -> t.add(\"example value\"))".formatted(method.getMethodName())); + MethodDto method, TypeName elementType, ProcessingContext context) { + addExampleChainFragmentTemplate(method, "#{methodName}(t -> t.add(#{exampleValue}))", elementType); } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/OptionalHelperGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/OptionalHelperGenerator.java index d60bb31e..6a71a0d9 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/OptionalHelperGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/OptionalHelperGenerator.java @@ -109,6 +109,9 @@ public List generateMethods( // Add code block import for Optional.ofNullable method.getMethodCodeDto().addCodeBlockImport(Optional.class); + // Add example fragment for Optional helper method + MethodGeneratorUtil.addExampleChainFragment(method, innerType); + return Collections.singletonList(method); } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringBuilderConsumerGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringBuilderConsumerGenerator.java index 49eea86f..1a8d3849 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringBuilderConsumerGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringBuilderConsumerGenerator.java @@ -162,6 +162,11 @@ private MethodDto createStringBuilderConsumer( .addParam( parameter.getParameterName(), "consumer providing an instance of %s", fieldJavadoc) .addReturn(JavadocConstants.RETURN_BUILDER_INSTANCE)); + + // Add example fragment for StringBuilder consumer method + addExampleChainFragmentTemplate( + methodDto, "#{methodName}(sb -> sb.append(#{exampleValue}))", TypeName.of(String.class)); + return methodDto; } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringFormatHelperGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringFormatHelperGenerator.java index 5ee2fd9f..e1332be0 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringFormatHelperGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringFormatHelperGenerator.java @@ -204,6 +204,10 @@ private MethodDto createStringFormatMethodWithTransform( argsParam.getParameterName(), "Arguments referenced by the format specifiers in the format string.") .addReturn(JavadocConstants.RETURN_BUILDER_INSTANCE)); + + // Add example fragment for String format method + addExampleChainFragmentTemplate(methodDto, "#{methodName}(\"Hello %s\", \"World\")"); + return methodDto; } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/VarArgsHelperGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/VarArgsHelperGenerator.java index 3ef380d2..2b3bf611 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/VarArgsHelperGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/VarArgsHelperGenerator.java @@ -157,13 +157,20 @@ private MethodDto createFieldSetterByVarArgs( MethodGeneratorUtil.createBuilderMethodForFieldWithTransform( field, transform, parameterType, builderType, context); - // Add code block imports for collection factory methods - if (fieldType instanceof TypeNameList) { + // Add code block imports and example fragments for collection factory methods + if (fieldType instanceof TypeNameList listType) { method.getMethodCodeDto().addCodeBlockImport(List.class); - } else if (fieldType instanceof TypeNameSet) { + MethodGeneratorUtil.addExampleChainFragment(method, listType.getElementType()); + } else if (fieldType instanceof TypeNameSet setType) { method.getMethodCodeDto().addCodeBlockImport(Set.class); - } else if (fieldType instanceof TypeNameMap) { + MethodGeneratorUtil.addExampleChainFragment(method, setType.getElementType()); + } else if (fieldType instanceof TypeNameMap mapType) { method.getMethodCodeDto().addCodeBlockImport(Map.class); + // For maps, only add example if key type is String + if (mapType.getKeyType().getClassName().equals("String")) { + MethodGeneratorUtil.addExampleChainFragmentTemplate( + method, "#{methodName}(Map.entry(\"key\", #{exampleValue}))", mapType.getValueType()); + } } return method; diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java index fcf4475f..c99ce208 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java @@ -394,11 +394,7 @@ public static MethodDto createSimpleFieldConsumer( * @param fieldType the field type to get an example value for */ public static void addExampleChainFragment(MethodDto methodDto, TypeName fieldType) { - JavadocExampleValues.getExampleValue(fieldType) - .ifPresent( - example -> - methodDto.setExampleChainFragment( - "%s(%s)".formatted(methodDto.getMethodName(), example))); + addExampleChainFragmentTemplate(methodDto, "#{methodName}(#{exampleValue})", fieldType); } /** @@ -414,12 +410,9 @@ public static void addExampleChainFragment(MethodDto methodDto, TypeName fieldTy * @param methodDto the method DTO to add the fragment to * @param fieldType the field type to get an example value for */ - public static void addExampleChainFragmentWithSupplier(MethodDto methodDto, TypeName fieldType) { - JavadocExampleValues.getExampleValue(fieldType) - .ifPresent( - example -> - methodDto.setExampleChainFragment( - "%s(() -> %s)".formatted(methodDto.getMethodName(), example))); + public static void addExampleChainFragmentWithSupplier( + MethodDto methodDto, TypeName fieldType) { + addExampleChainFragmentTemplate(methodDto, "#{methodName}(() -> #{exampleValue})", fieldType); } /** @@ -437,23 +430,56 @@ public static void addExampleChainFragmentWithSupplier(MethodDto methodDto, Type */ public static void addExampleChainFragmentWithHardcodedValue( MethodDto methodDto, String exampleValue) { - methodDto.setExampleChainFragment("%s(%s)".formatted(methodDto.getMethodName(), exampleValue)); + addExampleChainFragmentTemplate( + methodDto, "#{methodName}(%s)".formatted(exampleValue)); } /** - * Adds the fluent-chain fragment for Javadoc examples to a method with a custom fragment format. + * Adds the fluent-chain fragment for Javadoc examples to a method using a template string. * - *

This helper method allows generators to specify a completely custom fragment format for - * special cases (e.g., lambda expressions). The fragment is stored on the MethodDto for later use - * by the ClassJavaDocEnhancer to synthesize both method-level and class-level Javadoc examples. + *

This helper method replaces placeholders in the template string with actual values: + *

    + *
  • {@code #{methodName}} - replaced with the method name
  • + *
  • {@code #{exampleValue}} - replaced with an example value for the given field type (if fieldType is provided)
  • + *
+ * + *

The fragment is stored on the MethodDto for later use by the ClassJavaDocEnhancer to synthesize + * both method-level and class-level Javadoc examples. * - *

This is useful for generators with unique fragment patterns that don't fit the standard - * value or supplier patterns. + * @param methodDto the method DTO to add the fragment to + * @param template the template string with placeholders (e.g., {@code #{methodName}(#{exampleValue})}) + * @param fieldType the field type to get an example value for (can be {@code null}) + */ + public static void addExampleChainFragmentTemplate( + MethodDto methodDto, String template, TypeName fieldType) { + String fragment = template.replace("#{methodName}", methodDto.getMethodName()); + + if (fieldType != null) { + String exampleValue = + JavadocExampleValues.getExampleValue(fieldType).orElse(null); + if (exampleValue != null) { + fragment = fragment.replace("#{exampleValue}", exampleValue); + methodDto.setExampleChainFragment(fragment); + } + } + } + + /** + * Adds the fluent-chain fragment for Javadoc examples to a method using a template string. + * + *

This helper method replaces placeholders in the template string with actual values: + *

    + *
  • {@code #{methodName}} - replaced with the method name
  • + *
+ * + *

The fragment is stored on the MethodDto for later use by the ClassJavaDocEnhancer to synthesize + * both method-level and class-level Javadoc examples. * * @param methodDto the method DTO to add the fragment to - * @param fragment the custom fragment to add (e.g., {@code methodName(t -> t.add("value"))}) + * @param template the template string with placeholders (e.g., {@code #{methodName}(sb -> sb.append("text"))}) */ - public static void addExampleChainFragmentCustom(MethodDto methodDto, String fragment) { + public static void addExampleChainFragmentTemplate(MethodDto methodDto, String template) { + String fragment = template.replace("#{methodName}", methodDto.getMethodName()); methodDto.setExampleChainFragment(fragment); } } From ea59804b4e394d4b91c05a9ac4f7c8f5998ab27b Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Fri, 31 Jul 2026 20:44:23 +0200 Subject: [PATCH 06/23] Adding generation of example blocks --- .../roaster/RoasterCodeGenerator.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java index a718849a..68b759b8 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java @@ -46,6 +46,7 @@ import org.javahelpers.simple.builders.processor.model.core.ClassFieldDto; import org.javahelpers.simple.builders.processor.model.core.GenerationTargetClassDto; import org.javahelpers.simple.builders.processor.model.imports.ImportStatement; +import org.javahelpers.simple.builders.processor.model.javadoc.JavadocCodeBlockDto; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocTagDto; import org.javahelpers.simple.builders.processor.model.method.ConstructorDto; @@ -411,6 +412,28 @@ private void applyJavadoc( source.getJavaDoc().addTagValue(tag.getFullTagName(), ""); } } + + // Render code example blocks + for (JavadocCodeBlockDto codeBlock : javadoc.getCodeBlocks()) { + if (codeBlock != null && codeBlock.hasCode()) { + // Resolve placeholders in the code block + String resolvedCode = resolveCodeTemplate(codeBlock); + // Pre-prefix every line of the code body with " * " so it survives Roaster's + // preformatted-block handling (Roaster does not auto-add asterisk prefix inside

).
+        String prefixedCode =
+            java.util.Arrays.stream(resolvedCode.split("\n", -1))
+                .map(line -> " * " + line)
+                .collect(java.util.stream.Collectors.joining("\n"));
+        // Add the code example to the Javadoc with a blank line separator before 

. + String currentText = source.getJavaDoc().getText(); + String exampleText = "

Example:

{@code\n" + prefixedCode + "\n * }
"; + if (StringUtils.isNotBlank(currentText)) { + source.getJavaDoc().setText(currentText + "\n\n" + exampleText); + } else { + source.getJavaDoc().setText(exampleText); + } + } + } } private void applyAnnotations( From 4906a6494199e70084dbe785bcaf192066dd17a9 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Fri, 31 Jul 2026 22:11:22 +0200 Subject: [PATCH 07/23] Extending documentations in helper functions of builders --- .../builders/example/BookDtoBuilder.java | 42 ++++++ .../example/JacksonIntegrationDtoBuilder.java | 14 ++ .../example/MannschaftDtoBuilder.java | 21 ++- .../builders/example/PersonDtoBuilder.java | 56 ++++++++ .../example/ProductRecordBuilder.java | 28 ++++ .../builders/example/SponsorDtoBuilder.java | 19 ++- .../field/AddToCollectionGenerator.java | 4 +- .../field/ListConsumerGenerator.java | 3 +- .../field/StringBuilderConsumerGenerator.java | 3 +- .../field/VarArgsHelperGenerator.java | 4 +- .../generators/util/JavadocExampleValues.java | 78 +++++++++- .../generators/util/MethodGeneratorUtil.java | 61 +++++--- .../BuilderConfigurationReaderTest.java | 13 +- .../processor/BuilderJavadocExampleTest.java | 30 ++-- .../ComprehensiveFeatureIntegrationTest.java | 133 ++++++++++++++++-- 15 files changed, 449 insertions(+), 60 deletions(-) diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java index a566416c..27adba00 100644 --- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java @@ -29,10 +29,16 @@ * .available(true) * .category('x') * .discount(3.14f) + * .exactPrice(BigDecimal.valueOf(3.14)) + * .genres(Set.of("example value")) * .isbn("example value") + * .lastUpdated(LocalDateTime.now()) + * .metadata(Map.of("key", "example value")) * .pages(42) * .price(3.14) + * .publishDate(LocalDate.now()) * .salesCount(42L) + * .tags(List.of("example value")) * .title("example value") * .build(); * }
@@ -246,6 +252,12 @@ public BookDtoBuilder edition(short edition) { /** * Sets the value for exactPrice. * + *

Example:

+ * + *
{@code
+   * builder.exactPrice(BigDecimal.valueOf(3.14));
+   * }
+ * * @param exactPrice the exact book price to set * @return current instance of builder */ @@ -257,6 +269,12 @@ public BookDtoBuilder exactPrice(BigDecimal exactPrice) { /** * Sets the value for genres. * + *

Example:

+ * + *
{@code
+   * builder.genres(Set.of("example value"));
+   * }
+ * * @param genres the set of genres to set * @return current instance of builder */ @@ -285,6 +303,12 @@ public BookDtoBuilder isbn(String isbn) { /** * Sets the value for lastUpdated. * + *

Example:

+ * + *
{@code
+   * builder.lastUpdated(LocalDateTime.now());
+   * }
+ * * @param lastUpdated the last update timestamp to set * @return current instance of builder */ @@ -296,6 +320,12 @@ public BookDtoBuilder lastUpdated(LocalDateTime lastUpdated) { /** * Sets the value for metadata. * + *

Example:

+ * + *
{@code
+   * builder.metadata(Map.of("key", "example value"));
+   * }
+ * * @param metadata the metadata map to set * @return current instance of builder */ @@ -341,6 +371,12 @@ public BookDtoBuilder price(double price) { /** * Sets the value for publishDate. * + *

Example:

+ * + *
{@code
+   * builder.publishDate(LocalDate.now());
+   * }
+ * * @param publishDate the publication date to set * @return current instance of builder */ @@ -402,6 +438,12 @@ public BookDtoBuilder subtitle(Optional subtitle) { /** * Sets the value for tags. * + *

Example:

+ * + *
{@code
+   * builder.tags(List.of("example value"));
+   * }
+ * * @param tags the list of tags to set * @return current instance of builder */ diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/JacksonIntegrationDtoBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/JacksonIntegrationDtoBuilder.java index b369df0d..18fda69d 100644 --- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/JacksonIntegrationDtoBuilder.java +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/JacksonIntegrationDtoBuilder.java @@ -27,7 +27,9 @@ *
{@code
  * JacksonIntegrationDto result = JacksonIntegrationDtoBuilder.create()
  *     .name("example value")
+ *     .name("Hello %s", "World")
  *     .name(() -> "example value")
+ *     .name(sb -> sb.append("text"))
  *     .age(42)
  *     .age(() -> 42)
  *     .build();
@@ -132,6 +134,12 @@ public JacksonIntegrationDtoBuilder name(String name) {
   /**
    * Sets the value for name by executing the provided consumer.
    * 
+   * 

Example:

+ * + *
{@code
+   * builder.name(sb -> sb.append("text"));
+   * }
+ * * @param nameStringBuilderConsumer consumer providing an instance of name * @return current instance of builder */ @@ -163,6 +171,12 @@ public JacksonIntegrationDtoBuilder name(Supplier nameSupplier) { * Sets the String value for name by using String.format(format, args). See * {@link String#format(String, Object...)} for details. * + *

Example:

+ * + *
{@code
+   * builder.name("Hello %s", "World");
+   * }
+ * * @param format A format string * @param args Arguments referenced by the format specifiers in the format string. * @return current instance of builder diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/MannschaftDtoBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/MannschaftDtoBuilder.java index a85453ad..0c55b8b9 100644 --- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/MannschaftDtoBuilder.java +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/MannschaftDtoBuilder.java @@ -28,8 +28,9 @@ *
{@code
  * MannschaftDto result = MannschaftDtoBuilder.create()
  *     .name("example value")
+ *     .name("Hello %s", "World")
  *     .name(() -> "example value")
- *     .add2Sponsoren("example value")
+ *     .name(sb -> sb.append("text"))
  *     .build();
  * }
*/ @@ -80,12 +81,6 @@ public static MannschaftDtoBuilder create() { /** * Adds a single element to sponsoren. * - *

Example:

- * - *
{@code
-   * builder.add2Sponsoren("example value");
-   * }
- * * @param element the element to add * @return current instance of builder */ @@ -121,6 +116,12 @@ public MannschaftDtoBuilder name(String name) { /** * Sets the value for name by executing the provided consumer. * + *

Example:

+ * + *
{@code
+   * builder.name(sb -> sb.append("text"));
+   * }
+ * * @param nameStringBuilderConsumer consumer providing an instance of name * @return current instance of builder */ @@ -152,6 +153,12 @@ public MannschaftDtoBuilder name(Supplier nameSupplier) { * Sets the String value for name by using String.format(format, args). See * {@link String#format(String, Object...)} for details. * + *

Example:

+ * + *
{@code
+   * builder.name("Hello %s", "World");
+   * }
+ * * @param format A format string * @param args Arguments referenced by the format specifiers in the format string. * @return current instance of builder diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/PersonDtoBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/PersonDtoBuilder.java index 33f7fa0e..69f02817 100644 --- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/PersonDtoBuilder.java +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/PersonDtoBuilder.java @@ -29,11 +29,19 @@ *
{@code
  * PersonDto result = PersonDtoBuilder.create()
  *     .name("example value")
+ *     .name("Hello %s", "World")
  *     .name(() -> "example value")
+ *     .name(sb -> sb.append("text"))
+ *     .birthdate(LocalDate.now())
+ *     .birthdate(() -> LocalDate.now())
+ *     .nickNames(List.of("example value"))
+ *     .nickNames(() -> List.of("example value"))
  *     .nickNames(t -> t.add("example value"))
+ *     .nickNames("example value", "example value")
  *     .add2NickNames("example value")
  *     .nickNames2("example value")
  *     .nickNames2(() -> "example value")
+ *     .nickNames2("example value")
  *     .build();
  * }
*/ @@ -122,6 +130,12 @@ public PersonDtoBuilder add2NickNames(String element) { /** * Sets the value for birthdate. * + *

Example:

+ * + *
{@code
+   * builder.birthdate(LocalDate.now());
+   * }
+ * * @param birthdate birthdate * @return current instance of builder */ @@ -133,6 +147,12 @@ public PersonDtoBuilder birthdate(LocalDate birthdate) { /** * Sets the value for birthdate by invoking the provided supplier. * + *

Example:

+ * + *
{@code
+   * builder.birthdate(() -> LocalDate.now());
+   * }
+ * * @param birthdateSupplier supplier for birthdate * @return current instance of builder */ @@ -198,6 +218,12 @@ public PersonDtoBuilder name(String name) { /** * Sets the value for name by executing the provided consumer. * + *

Example:

+ * + *
{@code
+   * builder.name(sb -> sb.append("text"));
+   * }
+ * * @param nameStringBuilderConsumer consumer providing an instance of name * @return current instance of builder */ @@ -229,6 +255,12 @@ public PersonDtoBuilder name(Supplier nameSupplier) { * Sets the String value for name by using String.format(format, args). See * {@link String#format(String, Object...)} for details. * + *

Example:

+ * + *
{@code
+   * builder.name("Hello %s", "World");
+   * }
+ * * @param format A format string * @param args Arguments referenced by the format specifiers in the format string. * @return current instance of builder @@ -241,6 +273,12 @@ public PersonDtoBuilder name(String format, Object... args) { /** * Sets the value for nickNames. * + *

Example:

+ * + *
{@code
+   * builder.nickNames("example value", "example value");
+   * }
+ * * @param nickNames nickNames * @return current instance of builder */ @@ -252,6 +290,12 @@ public PersonDtoBuilder nickNames(String... nickNames) { /** * Sets the value for nickNames. * + *

Example:

+ * + *
{@code
+   * builder.nickNames(List.of("example value"));
+   * }
+ * * @param nickNames nickNames * @return current instance of builder */ @@ -284,6 +328,12 @@ public PersonDtoBuilder nickNames(Consumer> nickNamesBu /** * Sets the value for nickNames by invoking the provided supplier. * + *

Example:

+ * + *
{@code
+   * builder.nickNames(() -> List.of("example value"));
+   * }
+ * * @param nickNamesSupplier supplier for nickNames * @return current instance of builder */ @@ -312,6 +362,12 @@ public PersonDtoBuilder nickNames2(String... nickNames2) { /** * Sets the value for nickNames2. * + *

Example:

+ * + *
{@code
+   * builder.nickNames2("example value");
+   * }
+ * * @param nickNames2 nickNames2 * @return current instance of builder */ diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/ProductRecordBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/ProductRecordBuilder.java index 2de9ef62..ee195d57 100644 --- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/ProductRecordBuilder.java +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/ProductRecordBuilder.java @@ -25,11 +25,15 @@ *
{@code
  * ProductRecord result = ProductRecordBuilder.create()
  *     .name("example value")
+ *     .name("Hello %s", "World")
  *     .name(() -> "example value")
+ *     .name(sb -> sb.append("text"))
  *     .price(3.14)
  *     .price(() -> 3.14)
  *     .category("example value")
+ *     .category("Hello %s", "World")
  *     .category(() -> "example value")
+ *     .category(sb -> sb.append("text"))
  *     .build();
  * }
*/ @@ -102,6 +106,12 @@ public ProductRecordBuilder category(String category) { /** * Sets the value for category by executing the provided consumer. * + *

Example:

+ * + *
{@code
+   * builder.category(sb -> sb.append("text"));
+   * }
+ * * @param categoryStringBuilderConsumer consumer providing an instance of category * @return current instance of builder */ @@ -133,6 +143,12 @@ public ProductRecordBuilder category(Supplier categorySupplier) { * Sets the String value for category by using String.format(format, args). See * {@link String#format(String, Object...)} for details. * + *

Example:

+ * + *
{@code
+   * builder.category("Hello %s", "World");
+   * }
+ * * @param format A format string * @param args Arguments referenced by the format specifiers in the format string. * @return current instance of builder @@ -162,6 +178,12 @@ public ProductRecordBuilder name(String name) { /** * Sets the value for name by executing the provided consumer. * + *

Example:

+ * + *
{@code
+   * builder.name(sb -> sb.append("text"));
+   * }
+ * * @param nameStringBuilderConsumer consumer providing an instance of name * @return current instance of builder */ @@ -193,6 +215,12 @@ public ProductRecordBuilder name(Supplier nameSupplier) { * Sets the String value for name by using String.format(format, args). See * {@link String#format(String, Object...)} for details. * + *

Example:

+ * + *
{@code
+   * builder.name("Hello %s", "World");
+   * }
+ * * @param format A format string * @param args Arguments referenced by the format specifiers in the format string. * @return current instance of builder diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SponsorDtoBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SponsorDtoBuilder.java index 0447ae9e..094a4570 100644 --- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SponsorDtoBuilder.java +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SponsorDtoBuilder.java @@ -23,7 +23,12 @@ *

Example:

* *
{@code
- * SponsorDto result = SponsorDtoBuilder.create().name("example value").name(() -> "example value").build();
+ * SponsorDto result = SponsorDtoBuilder.create()
+ *     .name("example value")
+ *     .name("Hello %s", "World")
+ *     .name(() -> "example value")
+ *     .name(sb -> sb.append("text"))
+ *     .build();
  * }
*/ @Generated("Generated by org.javahelpers.simple.builders.processor.BuilderProcessor") @@ -85,6 +90,12 @@ public SponsorDtoBuilder name(String name) { /** * Sets the value for name by executing the provided consumer. * + *

Example:

+ * + *
{@code
+   * builder.name(sb -> sb.append("text"));
+   * }
+ * * @param nameStringBuilderConsumer consumer providing an instance of name * @return current instance of builder */ @@ -116,6 +127,12 @@ public SponsorDtoBuilder name(Supplier nameSupplier) { * Sets the String value for name by using String.format(format, args). See * {@link String#format(String, Object...)} for details. * + *

Example:

+ * + *
{@code
+   * builder.name("Hello %s", "World");
+   * }
+ * * @param format A format string * @param args Arguments referenced by the format specifiers in the format string. * @return current instance of builder diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/AddToCollectionGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/AddToCollectionGenerator.java index e5e5239e..03320818 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/AddToCollectionGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/AddToCollectionGenerator.java @@ -25,7 +25,7 @@ package org.javahelpers.simple.builders.processor.generators.field; import static org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil.TRACKED_VALUE_TYPE; -import static org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil.addExampleChainFragmentWithHardcodedValue; +import static org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil.addExampleChainFragment; import static org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil.getMethodAccessModifier; import java.util.ArrayList; @@ -191,7 +191,7 @@ private MethodDto createAddToCollectionMethod( // Store the fluent-chain fragment so the class-level enhancer can synthesise both the // method-level example block and the class-level kitchen-sink chain from one source of truth. - addExampleChainFragmentWithHardcodedValue(methodDto, "\"example value\""); + addExampleChainFragment(methodDto, elementType); return methodDto; } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ListConsumerGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ListConsumerGenerator.java index 0d1933a1..19b6e5d9 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ListConsumerGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ListConsumerGenerator.java @@ -171,6 +171,7 @@ public List generateMethods( */ private void addExampleToListConsumer( MethodDto method, TypeName elementType, ProcessingContext context) { - addExampleChainFragmentTemplate(method, "#{methodName}(t -> t.add(#{exampleValue}))", elementType); + addExampleChainFragmentTemplate( + method, "#{methodName}(t -> t.add(#{exampleValue}))", elementType); } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringBuilderConsumerGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringBuilderConsumerGenerator.java index 1a8d3849..54cbbc23 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringBuilderConsumerGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringBuilderConsumerGenerator.java @@ -164,8 +164,7 @@ private MethodDto createStringBuilderConsumer( .addReturn(JavadocConstants.RETURN_BUILDER_INSTANCE)); // Add example fragment for StringBuilder consumer method - addExampleChainFragmentTemplate( - methodDto, "#{methodName}(sb -> sb.append(#{exampleValue}))", TypeName.of(String.class)); + addExampleChainFragmentTemplate(methodDto, "#{methodName}(sb -> sb.append(\"text\"))"); return methodDto; } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/VarArgsHelperGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/VarArgsHelperGenerator.java index 2b3bf611..b02dca46 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/VarArgsHelperGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/VarArgsHelperGenerator.java @@ -160,10 +160,10 @@ private MethodDto createFieldSetterByVarArgs( // Add code block imports and example fragments for collection factory methods if (fieldType instanceof TypeNameList listType) { method.getMethodCodeDto().addCodeBlockImport(List.class); - MethodGeneratorUtil.addExampleChainFragment(method, listType.getElementType()); + MethodGeneratorUtil.addExampleChainFragmentVarArgs(method, listType.getElementType()); } else if (fieldType instanceof TypeNameSet setType) { method.getMethodCodeDto().addCodeBlockImport(Set.class); - MethodGeneratorUtil.addExampleChainFragment(method, setType.getElementType()); + MethodGeneratorUtil.addExampleChainFragmentVarArgs(method, setType.getElementType()); } else if (fieldType instanceof TypeNameMap mapType) { method.getMethodCodeDto().addCodeBlockImport(Map.class); // For maps, only add example if key type is String diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/JavadocExampleValues.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/JavadocExampleValues.java index 18e61aca..949dcfd4 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/JavadocExampleValues.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/JavadocExampleValues.java @@ -27,8 +27,11 @@ import java.util.Map; import java.util.Optional; import org.javahelpers.simple.builders.processor.model.type.TypeName; +import org.javahelpers.simple.builders.processor.model.type.TypeNameList; +import org.javahelpers.simple.builders.processor.model.type.TypeNameMap; import org.javahelpers.simple.builders.processor.model.type.TypeNamePrimitive; import org.javahelpers.simple.builders.processor.model.type.TypeNamePrimitive.PrimitiveTypeEnum; +import org.javahelpers.simple.builders.processor.model.type.TypeNameSet; /** * Provides default example values for Javadoc code examples. @@ -46,6 +49,12 @@ public final class JavadocExampleValues { private static final String FLOAT_EXAMPLE = "3.14f"; private static final String BOOLEAN_EXAMPLE = "true"; private static final String CHAR_EXAMPLE = "'x'"; + private static final String BIGINTEGER_EXAMPLE = "BigInteger.valueOf(42)"; + private static final String BIGDECIMAL_EXAMPLE = "BigDecimal.valueOf(3.14)"; + private static final String UUID_EXAMPLE = "UUID.randomUUID()"; + private static final String LOCALDATE_EXAMPLE = "LocalDate.now()"; + private static final String LOCALTIME_EXAMPLE = "LocalTime.now()"; + private static final String LOCALDATETIME_EXAMPLE = "LocalDateTime.now()"; /** Map of primitive type enums to their example values. */ private static final Map PRIMITIVE_EXAMPLES = @@ -57,6 +66,24 @@ public final class JavadocExampleValues { PrimitiveTypeEnum.BOOLEAN, BOOLEAN_EXAMPLE, PrimitiveTypeEnum.CHAR, CHAR_EXAMPLE); + /** Map of wrapper and common JDK type FQNs to their example values. */ + private static final Map COMMON_TYPE_EXAMPLES = + Map.ofEntries( + // Wrapper types (mirror primitives) + Map.entry("java.lang.Integer", INT_EXAMPLE), + Map.entry("java.lang.Long", LONG_EXAMPLE), + Map.entry("java.lang.Double", DOUBLE_EXAMPLE), + Map.entry("java.lang.Float", FLOAT_EXAMPLE), + Map.entry("java.lang.Boolean", BOOLEAN_EXAMPLE), + Map.entry("java.lang.Character", CHAR_EXAMPLE), + // Common JDK types + Map.entry("java.math.BigInteger", BIGINTEGER_EXAMPLE), + Map.entry("java.math.BigDecimal", BIGDECIMAL_EXAMPLE), + Map.entry("java.util.UUID", UUID_EXAMPLE), + Map.entry("java.time.LocalDate", LOCALDATE_EXAMPLE), + Map.entry("java.time.LocalTime", LOCALTIME_EXAMPLE), + Map.entry("java.time.LocalDateTime", LOCALDATETIME_EXAMPLE)); + private JavadocExampleValues() { // Utility class - prevent instantiation } @@ -69,16 +96,65 @@ private JavadocExampleValues() { *
    *
  • Primitive types (int, long, double, float, boolean, char) *
  • String type (returns {@value STRING_EXAMPLE}) + *
  • Wrapper types (Integer, Long, Double, Float, Boolean, Character) + *
  • Common JDK types (BigInteger, BigDecimal, UUID, LocalDate, LocalTime, LocalDateTime) + *
  • Collection types (List, Set) of any supported element type (e.g., {@code List.of("example + * value")}, {@code Set.of(42)}) + *
  • Map types where the key type is String and the value type is supported (e.g., {@code + * Map.of("key", "example value")}) *
* * @param typeName the type name to get an example value for * @return an Optional containing the example value, or empty if no example is available */ public static Optional getExampleValue(TypeName typeName) { + return resolvePrimitive(typeName) + .or(() -> resolveCollection(typeName)) + .or(() -> resolveCommonType(typeName)) + .or(() -> resolveString(typeName)); + } + + private static Optional resolvePrimitive(TypeName typeName) { if (typeName instanceof TypeNamePrimitive primitive) { return Optional.ofNullable(PRIMITIVE_EXAMPLES.get(primitive.getType())); } - // Check for String type + return Optional.empty(); + } + + private static Optional resolveCollection(TypeName typeName) { + if (typeName instanceof TypeNameList listType && listType.isParameterized()) { + return getExampleValue(listType.getElementType()) + .map(elementExample -> "List.of(" + elementExample + ")"); + } + if (typeName instanceof TypeNameSet setType && setType.isParameterized()) { + return getExampleValue(setType.getElementType()) + .map(elementExample -> "Set.of(" + elementExample + ")"); + } + if (typeName instanceof TypeNameMap mapType && mapType.isParameterized()) { + return resolveMap(mapType); + } + return Optional.empty(); + } + + private static Optional resolveMap(TypeNameMap mapType) { + TypeName keyType = mapType.getKeyType(); + if ("java.lang.String".equals(keyType.getFullQualifiedName()) + || "String".equals(keyType.getClassName())) { + return getExampleValue(mapType.getValueType()) + .map(valueExample -> "Map.of(\"key\", " + valueExample + ")"); + } + return Optional.empty(); + } + + private static Optional resolveCommonType(TypeName typeName) { + String fqn = typeName.getFullQualifiedName(); + if (fqn != null && COMMON_TYPE_EXAMPLES.containsKey(fqn)) { + return Optional.of(COMMON_TYPE_EXAMPLES.get(fqn)); + } + return Optional.empty(); + } + + private static Optional resolveString(TypeName typeName) { if ("java.lang.String".equals(typeName.getFullQualifiedName()) || "String".equals(typeName.getClassName())) { return Optional.of(STRING_EXAMPLE); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java index c99ce208..317651c7 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java @@ -397,6 +397,25 @@ public static void addExampleChainFragment(MethodDto methodDto, TypeName fieldTy addExampleChainFragmentTemplate(methodDto, "#{methodName}(#{exampleValue})", fieldType); } + /** + * Adds the fluent-chain fragment for Javadoc examples to a varargs method. + * + *

This helper method retrieves an example value for the given element type and formats it as a + * fluent-chain fragment showing two values (e.g., {@code methodName(value1, value2)}) to make it + * clear that the method accepts multiple arguments. The fragment is stored on the MethodDto for + * later use by the ClassJavaDocEnhancer to synthesize both method-level and class-level Javadoc + * examples. + * + *

If no example value is available for the element type, the fragment is not added. + * + * @param methodDto the method DTO to add the fragment to + * @param elementType the element type to get an example value for + */ + public static void addExampleChainFragmentVarArgs(MethodDto methodDto, TypeName elementType) { + addExampleChainFragmentTemplate( + methodDto, "#{methodName}(#{exampleValue}, #{exampleValue})", elementType); + } + /** * Adds the fluent-chain fragment for Javadoc examples to a method using a supplier pattern. * @@ -410,8 +429,7 @@ public static void addExampleChainFragment(MethodDto methodDto, TypeName fieldTy * @param methodDto the method DTO to add the fragment to * @param fieldType the field type to get an example value for */ - public static void addExampleChainFragmentWithSupplier( - MethodDto methodDto, TypeName fieldType) { + public static void addExampleChainFragmentWithSupplier(MethodDto methodDto, TypeName fieldType) { addExampleChainFragmentTemplate(methodDto, "#{methodName}(() -> #{exampleValue})", fieldType); } @@ -430,53 +448,58 @@ public static void addExampleChainFragmentWithSupplier( */ public static void addExampleChainFragmentWithHardcodedValue( MethodDto methodDto, String exampleValue) { - addExampleChainFragmentTemplate( - methodDto, "#{methodName}(%s)".formatted(exampleValue)); + addExampleChainFragmentTemplate(methodDto, "#{methodName}(%s)".formatted(exampleValue)); } /** * Adds the fluent-chain fragment for Javadoc examples to a method using a template string. * *

This helper method replaces placeholders in the template string with actual values: + * *

    - *
  • {@code #{methodName}} - replaced with the method name
  • - *
  • {@code #{exampleValue}} - replaced with an example value for the given field type (if fieldType is provided)
  • + *
  • {@code #{methodName}} - replaced with the method name + *
  • {@code #{exampleValue}} - replaced with an example value for the given field type (if + * fieldType is provided) *
* - *

The fragment is stored on the MethodDto for later use by the ClassJavaDocEnhancer to synthesize - * both method-level and class-level Javadoc examples. + *

The fragment is stored on the MethodDto for later use by the ClassJavaDocEnhancer to + * synthesize both method-level and class-level Javadoc examples. * * @param methodDto the method DTO to add the fragment to - * @param template the template string with placeholders (e.g., {@code #{methodName}(#{exampleValue})}) + * @param template the template string with placeholders (e.g., {@code + * #{methodName}(#{exampleValue})}) * @param fieldType the field type to get an example value for (can be {@code null}) */ public static void addExampleChainFragmentTemplate( MethodDto methodDto, String template, TypeName fieldType) { String fragment = template.replace("#{methodName}", methodDto.getMethodName()); - + if (fieldType != null) { - String exampleValue = - JavadocExampleValues.getExampleValue(fieldType).orElse(null); - if (exampleValue != null) { - fragment = fragment.replace("#{exampleValue}", exampleValue); - methodDto.setExampleChainFragment(fragment); + java.util.Optional exampleValue = JavadocExampleValues.getExampleValue(fieldType); + if (exampleValue.isEmpty()) { + return; } + fragment = fragment.replace("#{exampleValue}", exampleValue.get()); } + + methodDto.setExampleChainFragment(fragment); } /** * Adds the fluent-chain fragment for Javadoc examples to a method using a template string. * *

This helper method replaces placeholders in the template string with actual values: + * *

    - *
  • {@code #{methodName}} - replaced with the method name
  • + *
  • {@code #{methodName}} - replaced with the method name *
* - *

The fragment is stored on the MethodDto for later use by the ClassJavaDocEnhancer to synthesize - * both method-level and class-level Javadoc examples. + *

The fragment is stored on the MethodDto for later use by the ClassJavaDocEnhancer to + * synthesize both method-level and class-level Javadoc examples. * * @param methodDto the method DTO to add the fragment to - * @param template the template string with placeholders (e.g., {@code #{methodName}(sb -> sb.append("text"))}) + * @param template the template string with placeholders (e.g., {@code #{methodName}(sb -> + * sb.append("text"))}) */ public static void addExampleChainFragmentTemplate(MethodDto methodDto, String template) { String fragment = template.replace("#{methodName}", methodDto.getMethodName()); diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderConfigurationReaderTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderConfigurationReaderTest.java index eb764cb8..ad135297 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderConfigurationReaderTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderConfigurationReaderTest.java @@ -478,8 +478,11 @@ public class PersonDto { *

Example:

* *
{@code
-         * PersonDto result = PersonDtoMinimalBuilder.create().withName("example value").build();
-         * }
+ * PersonDto result = PersonDtoMinimalBuilder.create() + * .withName("example value") + * .withTags(List.of("example value")) + * .build(); + * }
*/ public class PersonDtoMinimalBuilder implements IBuilderBase { @@ -543,6 +546,12 @@ public PersonDtoMinimalBuilder withName(String name) { /** * Sets the value for tags. * + *

Example:

+ * + *
{@code
+           * builder.withTags(List.of("example value"));
+           * }
+ * * @param tags tags * @return current instance of builder */ diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderJavadocExampleTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderJavadocExampleTest.java index c20d40c7..9c9218fe 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderJavadocExampleTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderJavadocExampleTest.java @@ -87,22 +87,24 @@ void shouldGenerateClassJavadocExampleWithKitchenSinkChain() { assertGenerationSucceeded(compilation, builderClassName, generatedCode); // The generated class javadoc must contain the full kitchen-sink chain, - // with fields in DTO declaration order (title, pages, tags) and within each - // field the generator lines in priority order (BasicSetter=100, Supplier=60, - // ListConsumer=53, AddToCollection=30). + // with fields in alphabetical order (pages, tags, title) and within each + // field the generator lines in priority order. ProcessorAsserts.assertContaining( generatedCode, """ - *

Example:

- * *
{@code
         * BookDto result = BookDtoBuilder.create()
         *     .pages(42)
         *     .pages(() -> 42)
+        *     .tags(List.of("example value"))
+        *     .tags(() -> List.of("example value"))
         *     .tags(t -> t.add("example value"))
+        *     .tags("example value", "example value")
         *     .add2Tags("example value")
         *     .title("example value")
+        *     .title("Hello %s", "World")
         *     .title(() -> "example value")
+        *     .title(sb -> sb.append("text"))
         *     .build();
         * }
"""); @@ -428,13 +430,23 @@ public class HelperPlain { public HelperPlain() {} } String generatedCode = loadGeneratedSource(compilation, builderClassName); assertGenerationSucceeded(compilation, builderClassName, generatedCode); + // The class-level kitchen-sink chain includes ONLY the resolvable field (title). + // The helper field (HelperPlain) has no example value and must be omitted. ProcessorAsserts.assertContaining( generatedCode, - "MixedDto result = MixedDtoBuilder.create().title(\"example value\")" - + ".title(() -> \"example value\").build();"); + """ + *
{@code
+        * MixedDto result = MixedDtoBuilder.create()
+        *     .title("example value")
+        *     .title("Hello %s", "World")
+        *     .title(() -> "example value")
+        *     .title(sb -> sb.append("text"))
+        *     .build();
+        * }
+ """); - // ...but NO `.helper(...)` call in the class example chain - ProcessorAsserts.assertNotContaining(generatedCode, ".helper(null)", ".helper(\""); + // No `.helper(...)` call in the class example chain + ProcessorAsserts.assertNotContaining(generatedCode, ".helper("); } @Test diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/ComprehensiveFeatureIntegrationTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/ComprehensiveFeatureIntegrationTest.java index e6ee17a1..e96364e8 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/ComprehensiveFeatureIntegrationTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/ComprehensiveFeatureIntegrationTest.java @@ -181,15 +181,30 @@ public PersonDto(String name, int age, Optional email, *
{@code
          * PersonDto result = PersonDtoBuilder.create()
          *     .name("example value")
+         *     .name("Hello %s", "World")
          *     .name(() -> "example value")
+         *     .name(sb -> sb.append("text"))
          *     .age(42)
          *     .age(() -> 42)
+         *     .email("Hello %s", "World")
+         *     .email("example value")
+         *     .email(sb -> sb.append("text"))
+         *     .nicknames(List.of("example value"))
+         *     .nicknames(() -> List.of("example value"))
          *     .nicknames(t -> t.add("example value"))
+         *     .nicknames("example value", "example value")
          *     .add2Nicknames("example value")
+         *     .tags(Set.of("example value"))
+         *     .tags(() -> Set.of("example value"))
+         *     .tags("example value", "example value")
          *     .add2Tags("example value")
-         *     .previousAddresses(t -> t.add("example value"))
-         *     .add2PreviousAddresses("example value")
+         *     .metadata(Map.of("key", "example value"))
+         *     .metadata(() -> Map.of("key", "example value"))
+         *     .metadata(Map.entry("key", "example value"))
+         *     .phoneNumbers(List.of("example value"))
+         *     .phoneNumbers(() -> List.of("example value"))
          *     .phoneNumbers(t -> t.add("example value"))
+         *     .phoneNumbers("example value", "example value")
          *     .add2PhoneNumbers("example value")
          *     .build();
          * }
@@ -324,12 +339,6 @@ public PersonDtoBuilder add2PhoneNumbers(String element) { /** * Adds a single element to previousAddresses. * - *

Example:

- * - *
{@code
-           * builder.add2PreviousAddresses("example value");
-           * }
- * * @param element the element to add * @return current instance of builder */ @@ -443,6 +452,12 @@ public PersonDtoBuilder age(Supplier ageSupplier) { /** * Sets the value for email. * + *

Example:

+ * + *
{@code
+           * builder.email("example value");
+           * }
+ * * @param email email * @return current instance of builder */ @@ -465,6 +480,12 @@ public PersonDtoBuilder email(Optional email) { /** * Sets the value for email by executing the provided consumer. * + *

Example:

+ * + *
{@code
+           * builder.email(sb -> sb.append("text"));
+           * }
+ * * @param emailStringBuilderConsumer consumer providing an instance of email * @return current instance of builder */ @@ -490,6 +511,12 @@ public PersonDtoBuilder email(Supplier> emailSupplier) { * Sets the String value for email by using String.format(format, args). See * {@link String#format(String, Object...)} for details. * + *

Example:

+ * + *
{@code
+           * builder.email("Hello %s", "World");
+           * }
+ * * @param format A format string * @param args Arguments referenced by the format specifiers in the format string. * @return current instance of builder @@ -502,6 +529,12 @@ public PersonDtoBuilder email(String format, Object... args) { /** * Sets the value for metadata. * + *

Example:

+ * + *
{@code
+           * builder.metadata(Map.entry("key", "example value"));
+           * }
+ * * @param metadata metadata * @return current instance of builder */ @@ -513,6 +546,12 @@ public PersonDtoBuilder metadata(Entry... metadata) { /** * Sets the value for metadata. * + *

Example:

+ * + *
{@code
+           * builder.metadata(Map.of("key", "example value"));
+           * }
+ * * @param metadata metadata * @return current instance of builder */ @@ -539,6 +578,12 @@ public PersonDtoBuilder metadata(Consumer> metada /** * Sets the value for metadata by invoking the provided supplier. * + *

Example:

+ * + *
{@code
+           * builder.metadata(() -> Map.of("key", "example value"));
+           * }
+ * * @param metadataSupplier supplier for metadata * @return current instance of builder */ @@ -567,6 +612,12 @@ public PersonDtoBuilder name(String name) { /** * Sets the value for name by executing the provided consumer. * + *

Example:

+ * + *
{@code
+           * builder.name(sb -> sb.append("text"));
+           * }
+ * * @param nameStringBuilderConsumer consumer providing an instance of name * @return current instance of builder */ @@ -598,6 +649,12 @@ public PersonDtoBuilder name(Supplier nameSupplier) { * Sets the String value for name by using String.format(format, args). See * {@link String#format(String, Object...)} for details. * + *

Example:

+ * + *
{@code
+           * builder.name("Hello %s", "World");
+           * }
+ * * @param format A format string * @param args Arguments referenced by the format specifiers in the format string. * @return current instance of builder @@ -610,6 +667,12 @@ public PersonDtoBuilder name(String format, Object... args) { /** * Sets the value for nicknames. * + *

Example:

+ * + *
{@code
+           * builder.nicknames("example value", "example value");
+           * }
+ * * @param nicknames nicknames * @return current instance of builder */ @@ -621,6 +684,12 @@ public PersonDtoBuilder nicknames(String... nicknames) { /** * Sets the value for nicknames. * + *

Example:

+ * + *
{@code
+           * builder.nicknames(List.of("example value"));
+           * }
+ * * @param nicknames nicknames * @return current instance of builder */ @@ -653,6 +722,12 @@ public PersonDtoBuilder nicknames(Consumer> nicknamesBu /** * Sets the value for nicknames by invoking the provided supplier. * + *

Example:

+ * + *
{@code
+           * builder.nicknames(() -> List.of("example value"));
+           * }
+ * * @param nicknamesSupplier supplier for nicknames * @return current instance of builder */ @@ -664,6 +739,12 @@ public PersonDtoBuilder nicknames(Supplier> nicknamesSupplier) { /** * Sets the value for phoneNumbers. * + *

Example:

+ * + *
{@code
+           * builder.phoneNumbers("example value", "example value");
+           * }
+ * * @param phoneNumbers phoneNumbers * @return current instance of builder */ @@ -675,6 +756,12 @@ public PersonDtoBuilder phoneNumbers(String... phoneNumbers) { /** * Sets the value for phoneNumbers. * + *

Example:

+ * + *
{@code
+           * builder.phoneNumbers(List.of("example value"));
+           * }
+ * * @param phoneNumbers phoneNumbers * @return current instance of builder */ @@ -707,6 +794,12 @@ public PersonDtoBuilder phoneNumbers(Consumer> phoneNum /** * Sets the value for phoneNumbers by invoking the provided supplier. * + *

Example:

+ * + *
{@code
+           * builder.phoneNumbers(() -> List.of("example value"));
+           * }
+ * * @param phoneNumbersSupplier supplier for phoneNumbers * @return current instance of builder */ @@ -740,12 +833,6 @@ public PersonDtoBuilder previousAddresses(List previousAddresses) { /** * Sets the value for previousAddresses using a builder consumer that produces the value. * - *

Example:

- * - *
{@code
-           * builder.previousAddresses(t -> t.add("example value"));
-           * }
- * * @param previousAddressesBuilderConsumer consumer providing an instance of a builder for previousAddresses * @return current instance of builder */ @@ -774,6 +861,12 @@ public PersonDtoBuilder previousAddresses(Supplier> previousAdd /** * Sets the value for tags. * + *

Example:

+ * + *
{@code
+           * builder.tags("example value", "example value");
+           * }
+ * * @param tags tags * @return current instance of builder */ @@ -785,6 +878,12 @@ public PersonDtoBuilder tags(String... tags) { /** * Sets the value for tags. * + *

Example:

+ * + *
{@code
+           * builder.tags(Set.of("example value"));
+           * }
+ * * @param tags tags * @return current instance of builder */ @@ -811,6 +910,12 @@ public PersonDtoBuilder tags(Consumer> tagsBuilderConsume /** * Sets the value for tags by invoking the provided supplier. * + *

Example:

+ * + *
{@code
+           * builder.tags(() -> Set.of("example value"));
+           * }
+ * * @param tagsSupplier supplier for tags * @return current instance of builder */ From 7f39eff378db6ebdb48f61ca4eb6cc107c77bb71 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Fri, 31 Jul 2026 23:28:15 +0200 Subject: [PATCH 08/23] Extending example code to be generated for non-default classes too. For that it checks if there is a builder or a parameterless constructor --- .../builders/example/BookDtoBuilder.java | 7 ++++ .../example/MannschaftDtoBuilder.java | 7 ++++ .../builders/example/PersonDtoBuilder.java | 21 +++++++++++ .../field/ListConsumerGenerator.java | 23 ++++++++---- .../field/NestedBuilderConsumerGenerator.java | 7 ++++ .../field/SetConsumerGenerator.java | 26 ++++++++++++++ .../generators/util/JavadocExampleValues.java | 20 ++++++++++- .../generators/util/MethodGeneratorUtil.java | 14 +++++--- .../processor/BuilderJavadocExampleTest.java | 3 +- .../ComprehensiveFeatureIntegrationTest.java | 35 +++++++++++++++++++ 10 files changed, 151 insertions(+), 12 deletions(-) diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java index 27adba00..b37d062b 100644 --- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java @@ -37,6 +37,7 @@ * .pages(42) * .price(3.14) * .publishDate(LocalDate.now()) + * .publisher(PersonDtoBuilder.create().build()) * .salesCount(42L) * .tags(List.of("example value")) * .title("example value") @@ -388,6 +389,12 @@ public BookDtoBuilder publishDate(LocalDate publishDate) { /** * Sets the value for publisher. * + *

Example:

+ * + *
{@code
+   * builder.publisher(PersonDtoBuilder.create().build());
+   * }
+ * * @param publisher the publisher to set * @return current instance of builder */ diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/MannschaftDtoBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/MannschaftDtoBuilder.java index 0c55b8b9..230161e7 100644 --- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/MannschaftDtoBuilder.java +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/MannschaftDtoBuilder.java @@ -31,6 +31,7 @@ * .name("Hello %s", "World") * .name(() -> "example value") * .name(sb -> sb.append("text")) + * .sponsoren(t -> t.add(sponsorDtoBuilder -> sponsorDtoBuilder)) * .build(); * } */ @@ -193,6 +194,12 @@ public MannschaftDtoBuilder sponsoren(Set sponsoren) { /** * Sets the value for sponsoren using a builder consumer that produces the value. * + *

Example:

+ * + *
{@code
+   * builder.sponsoren(t -> t.add(sponsorDtoBuilder -> sponsorDtoBuilder));
+   * }
+ * * @param sponsorenBuilderConsumer consumer providing an instance of a builder for sponsoren * @return current instance of builder */ diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/PersonDtoBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/PersonDtoBuilder.java index 69f02817..7f0ff5e5 100644 --- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/PersonDtoBuilder.java +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/PersonDtoBuilder.java @@ -34,6 +34,9 @@ * .name(sb -> sb.append("text")) * .birthdate(LocalDate.now()) * .birthdate(() -> LocalDate.now()) + * .mannschaft(new MannschaftDto()) + * .mannschaft(MannschaftDto::new) + * .mannschaft(mannschaftDtoBuilder -> mannschaftDtoBuilder) * .nickNames(List.of("example value")) * .nickNames(() -> List.of("example value")) * .nickNames(t -> t.add("example value")) @@ -164,6 +167,12 @@ public PersonDtoBuilder birthdate(Supplier birthdateSupplier) { /** * Sets the value for mannschaft. * + *

Example:

+ * + *
{@code
+   * builder.mannschaft(new MannschaftDto());
+   * }
+ * * @param mannschaft mannschaft * @return current instance of builder */ @@ -175,6 +184,12 @@ public PersonDtoBuilder mannschaft(MannschaftDto mannschaft) { /** * Sets the value for mannschaft using a builder consumer that produces the value. * + *

Example:

+ * + *
{@code
+   * builder.mannschaft(mannschaftDtoBuilder -> mannschaftDtoBuilder);
+   * }
+ * * @param mannschaftBuilderConsumer consumer providing an instance of a builder for mannschaft * @return current instance of builder */ @@ -190,6 +205,12 @@ public PersonDtoBuilder mannschaft(Consumer mannschaftBuil /** * Sets the value for mannschaft by invoking the provided supplier. * + *

Example:

+ * + *
{@code
+   * builder.mannschaft(MannschaftDto::new);
+   * }
+ * * @param mannschaftSupplier supplier for mannschaft * @return current instance of builder */ diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ListConsumerGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ListConsumerGenerator.java index 19b6e5d9..df8fc205 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ListConsumerGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ListConsumerGenerator.java @@ -33,6 +33,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import org.apache.commons.lang3.StringUtils; import org.javahelpers.simple.builders.core.builders.ArrayListBuilder; import org.javahelpers.simple.builders.core.builders.ArrayListBuilderWithElementBuilders; import org.javahelpers.simple.builders.processor.generators.MethodGenerator; @@ -137,7 +138,7 @@ public List generateMethods( field, collectionBuilderType, elementBuilderType.get(), builderType, context); // Add method-level example for list consumer - addExampleToListConsumer(method, elementType, context); + addExampleToListConsumerWithBuilder(method, elementBuilderType.get()); return List.of(method); } else if (context.getConfiguration().shouldUseArrayListBuilder()) { @@ -154,7 +155,7 @@ public List generateMethods( context); // Add method-level example for list consumer - addExampleToListConsumer(method, elementType, context); + addExampleToListConsumerWithSimpleValue(method, elementType); return List.of(method); } @@ -163,14 +164,24 @@ public List generateMethods( } /** - * Adds method-level example and contributes to class-level example for list consumer methods. + * Adds example for list consumer methods where the element type has its own builder. + * + * @param method the method to add the example to + * @param elementBuilderType the element builder type + */ + private void addExampleToListConsumerWithBuilder(MethodDto method, TypeName elementBuilderType) { + String builderVar = StringUtils.uncapitalize(elementBuilderType.getClassName()); + addExampleChainFragmentTemplate( + method, "#{methodName}(t -> t.add(" + builderVar + " -> " + builderVar + "))"); + } + + /** + * Adds example for list consumer methods where the element type is a simple value. * * @param method the method to add the example to * @param elementType the element type - * @param context the processing context */ - private void addExampleToListConsumer( - MethodDto method, TypeName elementType, ProcessingContext context) { + private void addExampleToListConsumerWithSimpleValue(MethodDto method, TypeName elementType) { addExampleChainFragmentTemplate( method, "#{methodName}(t -> t.add(#{exampleValue}))", elementType); } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/NestedBuilderConsumerGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/NestedBuilderConsumerGenerator.java index ba32d321..0856ee79 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/NestedBuilderConsumerGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/NestedBuilderConsumerGenerator.java @@ -30,6 +30,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import org.apache.commons.lang3.StringUtils; import org.javahelpers.simple.builders.processor.generators.MethodGenerator; import org.javahelpers.simple.builders.processor.model.core.FieldDto; import org.javahelpers.simple.builders.processor.model.method.MethodDto; @@ -112,6 +113,12 @@ public List generateMethods( Map.of(), builderType, context); + + // Add example fragment showing the consumer lambda pattern + String builderVar = StringUtils.uncapitalize(fieldBuilderType.getClassName()); + addExampleChainFragmentTemplate( + method, "#{methodName}(" + builderVar + " -> " + builderVar + ")"); + return List.of(method); } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/SetConsumerGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/SetConsumerGenerator.java index a02cf029..3ad220ed 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/SetConsumerGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/SetConsumerGenerator.java @@ -31,6 +31,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import org.apache.commons.lang3.StringUtils; import org.javahelpers.simple.builders.core.builders.HashSetBuilder; import org.javahelpers.simple.builders.core.builders.HashSetBuilderWithElementBuilders; import org.javahelpers.simple.builders.processor.generators.MethodGenerator; @@ -131,6 +132,7 @@ public List generateMethods( MethodDto method = createFieldConsumerWithElementBuilders( field, collectionBuilderType, elementBuilderType.get(), builderType, context); + addExampleToSetConsumerWithBuilder(method, elementBuilderType.get()); return List.of(method); } else if (context.getConfiguration().shouldUseHashSetBuilder()) { TypeName hashSetBuilderType = map2TypeName(HashSetBuilder.class); @@ -144,9 +146,33 @@ public List generateMethods( Map.of(), builderType, context); + addExampleToSetConsumerWithSimpleValue(method, elementType); return List.of(method); } return Collections.emptyList(); } + + /** + * Adds example for set consumer methods where the element type has its own builder. + * + * @param method the method to add the example to + * @param elementBuilderType the element builder type + */ + private void addExampleToSetConsumerWithBuilder(MethodDto method, TypeName elementBuilderType) { + String builderVar = StringUtils.uncapitalize(elementBuilderType.getClassName()); + addExampleChainFragmentTemplate( + method, "#{methodName}(t -> t.add(" + builderVar + " -> " + builderVar + "))"); + } + + /** + * Adds example for set consumer methods where the element type is a simple value. + * + * @param method the method to add the example to + * @param elementType the element type + */ + private void addExampleToSetConsumerWithSimpleValue(MethodDto method, TypeName elementType) { + addExampleChainFragmentTemplate( + method, "#{methodName}(t -> t.add(#{exampleValue}))", elementType); + } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/JavadocExampleValues.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/JavadocExampleValues.java index 949dcfd4..ad0f7f8b 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/JavadocExampleValues.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/JavadocExampleValues.java @@ -102,6 +102,9 @@ private JavadocExampleValues() { * value")}, {@code Set.of(42)}) *
  • Map types where the key type is String and the value type is supported (e.g., {@code * Map.of("key", "example value")}) + *
  • Types with an empty constructor (e.g., {@code new AddressDto()}) + *
  • Types with a {@code @SimpleBuilder} annotation but no empty constructor (e.g., {@code + * AddressDtoBuilder.create().build()}) * * * @param typeName the type name to get an example value for @@ -111,7 +114,9 @@ public static Optional getExampleValue(TypeName typeName) { return resolvePrimitive(typeName) .or(() -> resolveCollection(typeName)) .or(() -> resolveCommonType(typeName)) - .or(() -> resolveString(typeName)); + .or(() -> resolveString(typeName)) + .or(() -> resolveEmptyConstructor(typeName)) + .or(() -> resolveBuilderType(typeName)); } private static Optional resolvePrimitive(TypeName typeName) { @@ -161,4 +166,17 @@ private static Optional resolveString(TypeName typeName) { } return Optional.empty(); } + + private static Optional resolveEmptyConstructor(TypeName typeName) { + if (typeName.hasEmptyConstructor()) { + return Optional.of("new " + typeName.getClassName() + "()"); + } + return Optional.empty(); + } + + private static Optional resolveBuilderType(TypeName typeName) { + return typeName + .getBuilderType() + .map(builderType -> builderType.getClassName() + ".create().build()"); + } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java index 317651c7..4bd9e080 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java @@ -420,9 +420,10 @@ public static void addExampleChainFragmentVarArgs(MethodDto methodDto, TypeName * Adds the fluent-chain fragment for Javadoc examples to a method using a supplier pattern. * *

    This helper method retrieves an example value for the given field type and formats it as a - * fluent-chain fragment with a supplier (e.g., {@code methodName(() -> exampleValue)}). The - * fragment is stored on the MethodDto for later use by the ClassJavaDocEnhancer to synthesize - * both method-level and class-level Javadoc examples. + * fluent-chain fragment with a supplier. For types with an empty constructor, a method reference + * is used (e.g., {@code methodName(Type::new)}); otherwise a lambda is used (e.g., {@code + * methodName(() -> exampleValue)}). The fragment is stored on the MethodDto for later use by the + * ClassJavaDocEnhancer to synthesize both method-level and class-level Javadoc examples. * *

    If no example value is available for the field type, the fragment is not added. * @@ -430,7 +431,12 @@ public static void addExampleChainFragmentVarArgs(MethodDto methodDto, TypeName * @param fieldType the field type to get an example value for */ public static void addExampleChainFragmentWithSupplier(MethodDto methodDto, TypeName fieldType) { - addExampleChainFragmentTemplate(methodDto, "#{methodName}(() -> #{exampleValue})", fieldType); + if (fieldType.hasEmptyConstructor()) { + addExampleChainFragmentTemplate( + methodDto, "#{methodName}(" + fieldType.getClassName() + "::new)"); + } else { + addExampleChainFragmentTemplate(methodDto, "#{methodName}(() -> #{exampleValue})", fieldType); + } } /** diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderJavadocExampleTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderJavadocExampleTest.java index 9c9218fe..cb34c28d 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderJavadocExampleTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderJavadocExampleTest.java @@ -423,7 +423,7 @@ void shouldOmitClassExampleLineForUnresolvableField() { ProcessorTestUtils.forSource( """ package test; - public class HelperPlain { public HelperPlain() {} } + public class HelperPlain { public HelperPlain(String arg) {} } """); Compilation compilation = compile(dto, helper); @@ -432,6 +432,7 @@ public class HelperPlain { public HelperPlain() {} } // The class-level kitchen-sink chain includes ONLY the resolvable field (title). // The helper field (HelperPlain) has no example value and must be omitted. + // HelperPlain has only a parameterized constructor (no empty constructor) and no builder. ProcessorAsserts.assertContaining( generatedCode, """ diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/ComprehensiveFeatureIntegrationTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/ComprehensiveFeatureIntegrationTest.java index e96364e8..2e2b267d 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/ComprehensiveFeatureIntegrationTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/ComprehensiveFeatureIntegrationTest.java @@ -196,11 +196,16 @@ public PersonDto(String name, int age, Optional email, * .add2Nicknames("example value") * .tags(Set.of("example value")) * .tags(() -> Set.of("example value")) + * .tags(t -> t.add("example value")) * .tags("example value", "example value") * .add2Tags("example value") * .metadata(Map.of("key", "example value")) * .metadata(() -> Map.of("key", "example value")) * .metadata(Map.entry("key", "example value")) + * .address(AddressDtoBuilder.create().build()) + * .address(() -> AddressDtoBuilder.create().build()) + * .address(addressDtoBuilder -> addressDtoBuilder) + * .previousAddresses(t -> t.add(addressDtoBuilder -> addressDtoBuilder)) * .phoneNumbers(List.of("example value")) * .phoneNumbers(() -> List.of("example value")) * .phoneNumbers(t -> t.add("example value")) @@ -381,6 +386,12 @@ public PersonDtoBuilder add2Tags(String element) { /** * Sets the value for address. * + *

    Example:

    + * + *
    {@code
    +           * builder.address(AddressDtoBuilder.create().build());
    +           * }
    + * * @param address address * @return current instance of builder */ @@ -392,6 +403,12 @@ public PersonDtoBuilder address(AddressDto address) { /** * Sets the value for address using a builder consumer that produces the value. * + *

    Example:

    + * + *
    {@code
    +           * builder.address(addressDtoBuilder -> addressDtoBuilder);
    +           * }
    + * * @param addressBuilderConsumer consumer providing an instance of a builder for address * @return current instance of builder */ @@ -407,6 +424,12 @@ public PersonDtoBuilder address(Consumer addressBuilderConsum /** * Sets the value for address by invoking the provided supplier. * + *

    Example:

    + * + *
    {@code
    +           * builder.address(() -> AddressDtoBuilder.create().build());
    +           * }
    + * * @param addressSupplier supplier for address * @return current instance of builder */ @@ -833,6 +856,12 @@ public PersonDtoBuilder previousAddresses(List previousAddresses) { /** * Sets the value for previousAddresses using a builder consumer that produces the value. * + *

    Example:

    + * + *
    {@code
    +           * builder.previousAddresses(t -> t.add(addressDtoBuilder -> addressDtoBuilder));
    +           * }
    + * * @param previousAddressesBuilderConsumer consumer providing an instance of a builder for previousAddresses * @return current instance of builder */ @@ -895,6 +924,12 @@ public PersonDtoBuilder tags(Set tags) { /** * Sets the value for tags using a builder consumer that produces the value. * + *

    Example:

    + * + *
    {@code
    +           * builder.tags(t -> t.add("example value"));
    +           * }
    + * * @param tagsBuilderConsumer consumer providing an instance of a builder for tags * @return current instance of builder */ From 8f35c461fd8aae46bd05eda46083612f18de59f3 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 2 Aug 2026 13:32:50 +0200 Subject: [PATCH 09/23] Refactoring code structure to have specific classes for builder- and method-definition and to map them afterwards to generation specific classes --- .../custom/StringValidationGenerator.java | 6 +- .../builders/processor/BuilderProcessor.java | 6 +- .../processor/analysis/JavaLangAnalyser.java | 4 +- .../processor/generators/MethodGenerator.java | 5 +- .../builder/ClassJavaDocEnhancer.java | 6 +- .../builder/ConditionalEnhancer.java | 25 +- .../builder/CoreMethodsEnhancer.java | 26 +- .../builder/WithInterfaceEnhancer.java | 21 +- .../field/AddToCollectionGenerator.java | 16 +- .../field/ArrayBuilderConsumerGenerator.java | 14 +- .../field/ArrayConversionGenerator.java | 13 +- .../field/BasicSetterGenerator.java | 6 +- .../field/FieldInstanceConsumerGenerator.java | 7 +- .../field/ListConsumerGenerator.java | 14 +- .../field/MapConsumerGenerator.java | 6 +- .../field/NestedBuilderConsumerGenerator.java | 6 +- .../field/OptionalHelperGenerator.java | 6 +- .../field/SetConsumerGenerator.java | 14 +- .../field/StringBuilderConsumerGenerator.java | 13 +- .../field/StringFormatHelperGenerator.java | 17 +- .../field/SupplierMethodGenerator.java | 16 +- .../field/VarArgsHelperGenerator.java | 10 +- .../registry/GeneratorRegistry.java | 9 +- .../generators/util/MethodGeneratorUtil.java | 66 +- .../model/annotation/AnnotationDto.java | 5 +- .../model/annotation/InterfaceName.java | 10 +- .../model/core/BuilderDefinitionDto.java | 220 ++++++- .../core/BuilderToGenerationTypeMapper.java | 176 ++++++ .../processor/model/core/ClassFieldDto.java | 3 +- .../processor/model/core/FieldDto.java | 11 +- .../model/core/GenerationTargetClassDto.java | 28 +- .../processor/model/javadoc/JavadocDto.java | 17 + .../model/method/BuilderMethodDto.java | 582 ++++++++++++++++++ .../processor/model/method/MethodDto.java | 90 +-- .../model/type/BuilderNestedTypeDto.java | 113 ++++ .../processor/model/type/NestedTypeDto.java | 6 +- .../processing/BuilderDefinitionCreator.java | 23 +- .../CustomizingDocumentationTest.java | 46 +- 38 files changed, 1365 insertions(+), 297 deletions(-) create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderToGenerationTypeMapper.java create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/BuilderMethodDto.java create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/BuilderNestedTypeDto.java diff --git a/example-custom-generator/src/main/java/org/javahelpers/simple/builders/example/custom/StringValidationGenerator.java b/example-custom-generator/src/main/java/org/javahelpers/simple/builders/example/custom/StringValidationGenerator.java index ef197823..054780ce 100644 --- a/example-custom-generator/src/main/java/org/javahelpers/simple/builders/example/custom/StringValidationGenerator.java +++ b/example-custom-generator/src/main/java/org/javahelpers/simple/builders/example/custom/StringValidationGenerator.java @@ -27,7 +27,7 @@ import java.util.List; import org.apache.commons.lang3.StringUtils; import org.javahelpers.simple.builders.processor.model.core.FieldDto; -import org.javahelpers.simple.builders.processor.model.method.MethodDto; +import org.javahelpers.simple.builders.processor.model.method.BuilderMethodDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; import org.javahelpers.simple.builders.processor.generators.MethodGenerator; @@ -66,7 +66,7 @@ public boolean appliesTo(FieldDto field, TypeName dtoType, ProcessingContext con } @Override - public List generateMethods(FieldDto field, TypeName builderType, ProcessingContext context) { + public List generateMethods(FieldDto field, TypeName builderType, ProcessingContext context) { String fieldInDto = field.getOriginalFieldName(); String fieldInBuilder = field.getFieldNameInBuilder(); String methodName = "validate" + StringUtils.capitalize(fieldInDto); @@ -79,7 +79,7 @@ public List generateMethods(FieldDto field, TypeName builderType, Pro fieldInBuilder, fieldInBuilder, StringUtils.capitalize(fieldInDto) ); - MethodDto validationMethod = new MethodDto(methodName, builderType); + BuilderMethodDto validationMethod = new BuilderMethodDto(methodName, builderType); validationMethod.setCode(methodBody); validationMethod.setJavadoc( new JavadocDto("Validates that the " + fieldInDto + " field is not null or empty.") 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 07530341..ecbb6109 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 @@ -24,6 +24,7 @@ package org.javahelpers.simple.builders.processor; +import static org.javahelpers.simple.builders.processor.model.core.BuilderToGenerationTypeMapper.toRenderingDto; import static org.javahelpers.simple.builders.processor.processing.BuilderDefinitionCreator.extractFromElement; import com.google.auto.service.AutoService; @@ -209,7 +210,8 @@ private void process(Element annotatedElement, BuilderConfiguration config) throws BuilderException { context.initConfigurationForProcessingTarget(config); BuilderDefinitionDto builderDef = extractFromElement(annotatedElement, context); - codeGenerator.generateClass(builderDef); + GenerationTargetClassDto renderingDto = toRenderingDto(builderDef); + codeGenerator.generateClass(renderingDto); // Collect info for Jackson Module if enabled jacksonModuleGenerator.addEntry(builderDef, annotatedElement); @@ -219,7 +221,7 @@ private void process(Element annotatedElement, BuilderConfiguration config) context.debugEndOperation( "Generated builder with %d fields and %d methods for %s", builderDef.getAllFieldsForBuilder().size(), - builderDef.getMethods().size(), + renderingDto.getMethods().size(), builderDef.getBuilderTypeName().getClassName()); } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangAnalyser.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangAnalyser.java index 56a30ce6..1fa3b5c2 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangAnalyser.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangAnalyser.java @@ -382,11 +382,11 @@ public static Optional findGetterForField( if (dtoType == null || fieldName == null || fieldTypeMirror == null) { return Optional.empty(); } - List classMethods = ElementFilter.methodsIn(context.getAllMembers(dtoType)); + List methods = ElementFilter.methodsIn(context.getAllMembers(dtoType)); // Check for accessor methods: // Record-style (fieldName), boolean-style (isXxx), or standard (getXxx) - for (ExecutableElement candidate : classMethods) { + for (ExecutableElement candidate : methods) { String name = candidate.getSimpleName().toString(); if (Strings.CI.equalsAny(name, fieldName, "is" + fieldName, "get" + fieldName) && candidate.getParameters().isEmpty() diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/MethodGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/MethodGenerator.java index c6e90652..69c8db66 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/MethodGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/MethodGenerator.java @@ -26,7 +26,7 @@ import java.util.List; import org.javahelpers.simple.builders.processor.model.core.FieldDto; -import org.javahelpers.simple.builders.processor.model.method.MethodDto; +import org.javahelpers.simple.builders.processor.model.method.BuilderMethodDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.processing.ProcessingContext; @@ -84,5 +84,6 @@ public non-sealed interface MethodGenerator extends Generator { * @param context the processing context containing configuration and utilities * @return list of generated methods (may be empty but should not be null) */ - List generateMethods(FieldDto field, TypeName builderType, ProcessingContext context); + List generateMethods( + FieldDto field, TypeName builderType, ProcessingContext context); } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ClassJavaDocEnhancer.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ClassJavaDocEnhancer.java index f4592c4a..fc9ee902 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ClassJavaDocEnhancer.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ClassJavaDocEnhancer.java @@ -28,7 +28,7 @@ import org.javahelpers.simple.builders.processor.model.core.BuilderDefinitionDto; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocCodeBlockDto; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; -import org.javahelpers.simple.builders.processor.model.method.MethodDto; +import org.javahelpers.simple.builders.processor.model.method.BuilderMethodDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.processing.ProcessingContext; @@ -94,12 +94,12 @@ public void enhanceBuilder(BuilderDefinitionDto builderDto, ProcessingContext co // Synthesise example blocks from the fluent-chain fragments stored on methods. This keeps the // per-method "builder.field(value);" example and the class-level kitchen-sink chain in sync - // with a single source of truth (the fragment on the MethodDto). + // with a single source of truth (the fragment on the BuilderMethodDto). // Note: Methods are stored in FieldDto objects at this point (before finalizeDefinition). JavadocCodeBlockDto classExampleBlock = new JavadocCodeBlockDto(); for (org.javahelpers.simple.builders.processor.model.core.FieldDto field : builderDto.getAllFieldsForBuilder()) { - for (MethodDto method : field.getMethods()) { + for (BuilderMethodDto method : field.getMethods()) { String fragment = method.getExampleChainFragment(); if (fragment == null) { continue; diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ConditionalEnhancer.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ConditionalEnhancer.java index 94a38881..9cacada7 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ConditionalEnhancer.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ConditionalEnhancer.java @@ -30,7 +30,7 @@ import org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil; import org.javahelpers.simple.builders.processor.model.core.BuilderDefinitionDto; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; -import org.javahelpers.simple.builders.processor.model.method.MethodDto; +import org.javahelpers.simple.builders.processor.model.method.BuilderMethodDto; import org.javahelpers.simple.builders.processor.model.method.MethodParameterDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.processing.ProcessingContext; @@ -103,11 +103,11 @@ public boolean appliesTo( @Override public void enhanceBuilder(BuilderDefinitionDto builderDto, ProcessingContext context) { // Add conditional(BooleanSupplier, Consumer, Consumer) method - MethodDto conditionalMethod = createConditionalMethod(builderDto); + BuilderMethodDto conditionalMethod = createConditionalMethod(builderDto); builderDto.addMethod(conditionalMethod); // Add conditional(BooleanSupplier, Consumer) method - MethodDto conditionalPositiveMethod = createConditionalPositiveOnlyMethod(builderDto); + BuilderMethodDto conditionalPositiveMethod = createConditionalPositiveOnlyMethod(builderDto); builderDto.addMethod(conditionalPositiveMethod); context.debug( @@ -115,10 +115,10 @@ public void enhanceBuilder(BuilderDefinitionDto builderDto, ProcessingContext co } /** Creates the conditional(BooleanSupplier, Consumer, Consumer) method. */ - private MethodDto createConditionalMethod(BuilderDefinitionDto builderDto) { - MethodDto method = new MethodDto("conditional", builderDto.getBuilderTypeName()); + private BuilderMethodDto createConditionalMethod(BuilderDefinitionDto builderDto) { + BuilderMethodDto method = new BuilderMethodDto("conditional", builderDto.getBuilderTypeName()); method.setOrdering(ORDERING_CONDITIONAL); - method.setPriority(MethodDto.PRIORITY_HIGHEST); + method.setPriority(BuilderMethodDto.PRIORITY_HIGHEST); method.setModifier(AccessModifier.PUBLIC); // Add parameters @@ -147,10 +147,10 @@ private MethodDto createConditionalMethod(BuilderDefinitionDto builderDto) { } /** Creates the conditional(BooleanSupplier, Consumer) method. */ - private MethodDto createConditionalPositiveOnlyMethod(BuilderDefinitionDto builderDto) { - MethodDto method = new MethodDto("conditional", builderDto.getBuilderTypeName()); + private BuilderMethodDto createConditionalPositiveOnlyMethod(BuilderDefinitionDto builderDto) { + BuilderMethodDto method = new BuilderMethodDto("conditional", builderDto.getBuilderTypeName()); method.setOrdering(ORDERING_CONDITIONAL_POSITIVE_ONLY); - method.setPriority(MethodDto.PRIORITY_HIGHEST); + method.setPriority(BuilderMethodDto.PRIORITY_HIGHEST); method.setModifier(AccessModifier.PUBLIC); // Add parameters @@ -169,7 +169,8 @@ private MethodDto createConditionalPositiveOnlyMethod(BuilderDefinitionDto build } /** Adds parameters for the conditional(BooleanSupplier, Consumer, Consumer) method. */ - private void addConditionalPositiveNegativeParameters(MethodDto method, TypeName builderType) { + private void addConditionalPositiveNegativeParameters( + BuilderMethodDto method, TypeName builderType) { // BooleanSupplier condition parameter addParameter(method, "condition", JavaLangMapper.map2TypeName(BooleanSupplier.class)); // Consumer trueCase parameter @@ -179,7 +180,7 @@ private void addConditionalPositiveNegativeParameters(MethodDto method, TypeName } /** Adds parameters for the conditional(BooleanSupplier, Consumer) method. */ - private void addConditionalPositiveOnlyParameters(MethodDto method, TypeName builderType) { + private void addConditionalPositiveOnlyParameters(BuilderMethodDto method, TypeName builderType) { // BooleanSupplier condition parameter addParameter(method, "condition", JavaLangMapper.map2TypeName(BooleanSupplier.class)); // Consumer yesCondition parameter @@ -187,7 +188,7 @@ private void addConditionalPositiveOnlyParameters(MethodDto method, TypeName bui } /** Adds a parameter to the method. */ - private void addParameter(MethodDto method, String name, TypeName type) { + private void addParameter(BuilderMethodDto method, String name, TypeName type) { MethodParameterDto parameter = new MethodParameterDto(); parameter.setParameterName(name); parameter.setParameterTypeName(type); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java index 4fabadaf..65ce8500 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java @@ -34,7 +34,7 @@ import org.javahelpers.simple.builders.processor.model.core.FieldDto; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocCodeBlockDto; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; -import org.javahelpers.simple.builders.processor.model.method.MethodDto; +import org.javahelpers.simple.builders.processor.model.method.BuilderMethodDto; import org.javahelpers.simple.builders.processor.model.type.GenericParameterDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.processing.ProcessingContext; @@ -101,26 +101,26 @@ public boolean appliesTo( @Override public void enhanceBuilder(BuilderDefinitionDto builderDto, ProcessingContext context) { // Add build() method - MethodDto buildMethod = createBuildMethod(builderDto); + BuilderMethodDto buildMethod = createBuildMethod(builderDto); builderDto.addMethod(buildMethod); // Add static create() method - MethodDto createMethod = createStaticCreateMethod(builderDto); + BuilderMethodDto createMethod = createStaticCreateMethod(builderDto); builderDto.addMethod(createMethod); // Add toString() method - MethodDto toStringMethod = createToStringMethod(builderDto); + BuilderMethodDto toStringMethod = createToStringMethod(builderDto); builderDto.addMethod(toStringMethod); } /** Creates the build() method. */ - protected MethodDto createBuildMethod(BuilderDefinitionDto builderDto) { + protected BuilderMethodDto createBuildMethod(BuilderDefinitionDto builderDto) { TypeName returnType = MethodGeneratorUtil.createGenericTypeName( builderDto.getBuildingTargetTypeName(), builderDto.getGenerics()); - MethodDto method = new MethodDto("build", returnType); + BuilderMethodDto method = new BuilderMethodDto("build", returnType); method.setOrdering(ORDERING_BUILD); - method.setPriority(MethodDto.PRIORITY_HIGHEST); + method.setPriority(BuilderMethodDto.PRIORITY_HIGHEST); method.setModifier(AccessModifier.PUBLIC); // Add @Override annotation only if implementing IBuilderBase interface @@ -212,13 +212,13 @@ protected MethodDto createBuildMethod(BuilderDefinitionDto builderDto) { } /** Creates the static create() method. */ - protected MethodDto createStaticCreateMethod(BuilderDefinitionDto builderDto) { + protected BuilderMethodDto createStaticCreateMethod(BuilderDefinitionDto builderDto) { TypeName returnType = MethodGeneratorUtil.createGenericTypeName( builderDto.getBuilderTypeName(), builderDto.getGenerics()); - MethodDto method = new MethodDto("create", returnType); + BuilderMethodDto method = new BuilderMethodDto("create", returnType); method.setOrdering(ORDERING_CREATE); - method.setPriority(MethodDto.PRIORITY_HIGHEST); + method.setPriority(BuilderMethodDto.PRIORITY_HIGHEST); method.setModifier(AccessModifier.PUBLIC); method.setStatic(true); @@ -255,10 +255,10 @@ protected MethodDto createStaticCreateMethod(BuilderDefinitionDto builderDto) { } /** Creates the toString() method. */ - protected MethodDto createToStringMethod(BuilderDefinitionDto builderDto) { - MethodDto method = new MethodDto("toString", TypeName.of(String.class)); + protected BuilderMethodDto createToStringMethod(BuilderDefinitionDto builderDto) { + BuilderMethodDto method = new BuilderMethodDto("toString", TypeName.of(String.class)); method.setOrdering(ORDERING_TO_STRING); - method.setPriority(MethodDto.PRIORITY_HIGHEST); + method.setPriority(BuilderMethodDto.PRIORITY_HIGHEST); method.setModifier(AccessModifier.PUBLIC); AnnotationDto overrideAnnotation = new AnnotationDto(); overrideAnnotation.setAnnotationType(JavaLangMapper.map2TypeName(Override.class)); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/WithInterfaceEnhancer.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/WithInterfaceEnhancer.java index 5344d746..c0d07504 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/WithInterfaceEnhancer.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/WithInterfaceEnhancer.java @@ -29,8 +29,9 @@ import org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil; import org.javahelpers.simple.builders.processor.model.core.BuilderDefinitionDto; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; -import org.javahelpers.simple.builders.processor.model.method.MethodDto; +import org.javahelpers.simple.builders.processor.model.method.BuilderMethodDto; import org.javahelpers.simple.builders.processor.model.method.MethodParameterDto; +import org.javahelpers.simple.builders.processor.model.type.BuilderNestedTypeDto; import org.javahelpers.simple.builders.processor.model.type.NestedTypeDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.model.type.TypeNameGeneric; @@ -100,7 +101,7 @@ public boolean appliesTo( @Override public void enhanceBuilder(BuilderDefinitionDto builderDto, ProcessingContext context) { - NestedTypeDto withInterface = createWithInterface(builderDto); + BuilderNestedTypeDto withInterface = createWithInterface(builderDto); builderDto.addNestedType(withInterface); } @@ -110,8 +111,8 @@ public void enhanceBuilder(BuilderDefinitionDto builderDto, ProcessingContext co * @param builderDto the builder definition * @return the nested type DTO for the With interface */ - private NestedTypeDto createWithInterface(BuilderDefinitionDto builderDto) { - NestedTypeDto withInterface = new NestedTypeDto(); + private BuilderNestedTypeDto createWithInterface(BuilderDefinitionDto builderDto) { + BuilderNestedTypeDto withInterface = new BuilderNestedTypeDto(); withInterface.setTypeName("With"); withInterface.setKind(NestedTypeDto.NestedTypeKind.INTERFACE); withInterface.setVisibility(AccessModifier.PUBLIC); @@ -120,11 +121,11 @@ private NestedTypeDto createWithInterface(BuilderDefinitionDto builderDto) { "Interface that can be implemented by the DTO to provide fluent modification methods.")); // Create the first method: DtoType with(Consumer b) - MethodDto withConsumerMethod = createWithConsumerMethod(builderDto); + BuilderMethodDto withConsumerMethod = createWithConsumerMethod(builderDto); withInterface.addMethod(withConsumerMethod); // Create the second method: BuilderType with() - MethodDto withBuilderMethod = createWithBuilderMethod(builderDto); + BuilderMethodDto withBuilderMethod = createWithBuilderMethod(builderDto); withInterface.addMethod(withBuilderMethod); return withInterface; @@ -136,10 +137,10 @@ private NestedTypeDto createWithInterface(BuilderDefinitionDto builderDto) { * @param builderDef the builder definition * @return the method definition */ - private MethodDto createWithConsumerMethod(BuilderDefinitionDto builderDef) { + private BuilderMethodDto createWithConsumerMethod(BuilderDefinitionDto builderDef) { // Return type is the DTO type TypeName dtoType = builderDef.getBuildingTargetTypeName(); - MethodDto method = new MethodDto("with", dtoType); + BuilderMethodDto method = new BuilderMethodDto("with", dtoType); // Parameter: Consumer b MethodParameterDto parameter = new MethodParameterDto(); @@ -183,9 +184,9 @@ private MethodDto createWithConsumerMethod(BuilderDefinitionDto builderDef) { * @param builderDef the builder definition * @return the method definition */ - private MethodDto createWithBuilderMethod(BuilderDefinitionDto builderDef) { + private BuilderMethodDto createWithBuilderMethod(BuilderDefinitionDto builderDef) { // Return type is the Builder type - MethodDto method = new MethodDto("with", builderDef.getBuilderTypeName()); + BuilderMethodDto method = new BuilderMethodDto("with", builderDef.getBuilderTypeName()); // Add implementation with validation to catch wrong implementations method.setCode( diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/AddToCollectionGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/AddToCollectionGenerator.java index 03320818..8407de81 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/AddToCollectionGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/AddToCollectionGenerator.java @@ -35,7 +35,7 @@ import org.javahelpers.simple.builders.processor.generators.util.JavadocConstants; import org.javahelpers.simple.builders.processor.model.core.FieldDto; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; -import org.javahelpers.simple.builders.processor.model.method.MethodDto; +import org.javahelpers.simple.builders.processor.model.method.BuilderMethodDto; import org.javahelpers.simple.builders.processor.model.method.MethodParameterDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.model.type.TypeNameList; @@ -102,14 +102,14 @@ public boolean appliesTo(FieldDto field, TypeName dtoType, ProcessingContext con } @Override - public List generateMethods( + public List generateMethods( FieldDto field, TypeName builderType, ProcessingContext context) { - List methods = new ArrayList<>(); + List methods = new ArrayList<>(); TypeName fieldType = field.getFieldType(); if (fieldType instanceof TypeNameList listType && listType.isParameterized()) { - MethodDto addMethod = + BuilderMethodDto addMethod = createAddToCollectionMethod( field.getOriginalFieldName(), field.getFieldNameInBuilder(), @@ -119,7 +119,7 @@ public List generateMethods( context); methods.add(addMethod); } else if (fieldType instanceof TypeNameSet setType && setType.isParameterized()) { - MethodDto addMethod = + BuilderMethodDto addMethod = createAddToCollectionMethod( field.getOriginalFieldName(), field.getFieldNameInBuilder(), @@ -133,7 +133,7 @@ public List generateMethods( return methods; } - private MethodDto createAddToCollectionMethod( + private BuilderMethodDto createAddToCollectionMethod( String originalFieldName, String fieldNameInBuilder, TypeName fieldType, @@ -141,7 +141,7 @@ private MethodDto createAddToCollectionMethod( TypeName builderType, ProcessingContext context) { String methodName = "add2" + StringUtils.capitalize(originalFieldName); - MethodDto methodDto = new MethodDto(methodName, builderType); + BuilderMethodDto methodDto = new BuilderMethodDto(methodName, builderType); MethodParameterDto parameter = new MethodParameterDto(); parameter.setParameterName("element"); @@ -182,7 +182,7 @@ private MethodDto createAddToCollectionMethod( methodDto.addArgument("elementType", elementType); methodDto.addArgument("builderFieldWrapper", TRACKED_VALUE_TYPE); methodDto.getMethodCodeDto().addCodeBlockImport(collectionImplType); - methodDto.setPriority(MethodDto.PRIORITY_MEDIUM); + methodDto.setPriority(BuilderMethodDto.PRIORITY_MEDIUM); methodDto.setJavadoc( new JavadocDto("Adds a single element to %s.", originalFieldName) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ArrayBuilderConsumerGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ArrayBuilderConsumerGenerator.java index d6d02486..6c951c5a 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ArrayBuilderConsumerGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ArrayBuilderConsumerGenerator.java @@ -33,7 +33,7 @@ import org.javahelpers.simple.builders.processor.generators.util.JavadocConstants; import org.javahelpers.simple.builders.processor.model.core.FieldDto; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; -import org.javahelpers.simple.builders.processor.model.method.MethodDto; +import org.javahelpers.simple.builders.processor.model.method.BuilderMethodDto; import org.javahelpers.simple.builders.processor.model.method.MethodParameterDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.model.type.TypeNameArray; @@ -95,7 +95,7 @@ public boolean appliesTo(FieldDto field, TypeName dtoType, ProcessingContext con } @Override - public List generateMethods( + public List generateMethods( FieldDto field, TypeName builderType, ProcessingContext context) { TypeName fieldType = field.getFieldType(); @@ -107,14 +107,14 @@ public List generateMethods( TypeName elementType = arrayType.getTypeOfArray(); TypeName collectionBuilderType = map2TypeName(ArrayListBuilder.class); - MethodDto method = + BuilderMethodDto method = createFieldConsumerWithArrayBuilder( field, collectionBuilderType, elementType, builderType, context); return List.of(method); } - private MethodDto createFieldConsumerWithArrayBuilder( + private BuilderMethodDto createFieldConsumerWithArrayBuilder( FieldDto field, TypeName collectionBuilderType, TypeName elementType, @@ -129,10 +129,8 @@ private MethodDto createFieldConsumerWithArrayBuilder( parameter.setParameterName(fieldName + BUILDER_SUFFIX + SUFFIX_CONSUMER); parameter.setParameterTypeName(consumerType); - MethodDto methodDto = - new MethodDto(generateBuilderMethodName(fieldName, context), returnBuilderType); + BuilderMethodDto methodDto = createBuilderMethod(fieldName, returnBuilderType, context); methodDto.addParameter(parameter); - methodDto.setModifier(getMethodAccessModifier(context)); methodDto.setCode( """ $helperType:T builder = this.$fieldName:N.isSet() @@ -151,7 +149,7 @@ private MethodDto createFieldConsumerWithArrayBuilder( methodDto.addArgument("helperType", builderTypeGeneric); methodDto.addArgument("builderFieldWrapper", TRACKED_VALUE_TYPE); methodDto.addArgument("elementType", elementType); - methodDto.setPriority(MethodDto.PRIORITY_MEDIUM); + methodDto.setPriority(BuilderMethodDto.PRIORITY_MEDIUM); methodDto.setJavadoc( new JavadocDto( "Sets the value for %s using the fluent builder consumer.", fieldName) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ArrayConversionGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ArrayConversionGenerator.java index b0802716..772925af 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ArrayConversionGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ArrayConversionGenerator.java @@ -32,7 +32,7 @@ import org.javahelpers.simple.builders.processor.generators.util.JavadocConstants; import org.javahelpers.simple.builders.processor.model.core.FieldDto; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; -import org.javahelpers.simple.builders.processor.model.method.MethodDto; +import org.javahelpers.simple.builders.processor.model.method.BuilderMethodDto; import org.javahelpers.simple.builders.processor.model.method.MethodParameterDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.model.type.TypeNameArray; @@ -87,7 +87,7 @@ public boolean appliesTo(FieldDto field, TypeName dtoType, ProcessingContext con } @Override - public List generateMethods( + public List generateMethods( FieldDto field, TypeName builderType, ProcessingContext context) { TypeName fieldType = field.getFieldType(); @@ -99,13 +99,13 @@ public List generateMethods( TypeName elementType = arrayType.getTypeOfArray(); TypeNameGeneric listType = new TypeNameGeneric(map2TypeName(List.class), elementType); - MethodDto method = + BuilderMethodDto method = createFieldSetterForArrayFromList(field, listType, elementType, builderType, context); return List.of(method); } - private MethodDto createFieldSetterForArrayFromList( + private BuilderMethodDto createFieldSetterForArrayFromList( FieldDto field, TypeName listType, TypeName elementType, @@ -117,9 +117,8 @@ private MethodDto createFieldSetterForArrayFromList( parameter.setParameterName(fieldName); parameter.setParameterTypeName(listType); - MethodDto methodDto = new MethodDto(generateBuilderMethodName(fieldName, context), builderType); + BuilderMethodDto methodDto = createBuilderMethod(fieldName, builderType, context); methodDto.addParameter(parameter); - methodDto.setModifier(getMethodAccessModifier(context)); methodDto.setCode( """ this.$fieldName:N = $builderFieldWrapper:T.changedValue($dtoMethodParams:N.toArray(new $elementType:T[0])); @@ -129,7 +128,7 @@ private MethodDto createFieldSetterForArrayFromList( methodDto.addArgument("dtoMethodParams", fieldName); methodDto.addArgument("builderFieldWrapper", TRACKED_VALUE_TYPE); methodDto.addArgument("elementType", elementType); - methodDto.setPriority(MethodDto.PRIORITY_HIGH); + methodDto.setPriority(BuilderMethodDto.PRIORITY_HIGH); String fieldJavadocDesc = field.getJavaDocDescriptionOrFieldName(); methodDto.setJavadoc( new JavadocDto("Sets the value for %s.", fieldName) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/BasicSetterGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/BasicSetterGenerator.java index 366e58fc..9f66352f 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/BasicSetterGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/BasicSetterGenerator.java @@ -29,7 +29,7 @@ import org.javahelpers.simple.builders.processor.generators.MethodGenerator; import org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil; import org.javahelpers.simple.builders.processor.model.core.FieldDto; -import org.javahelpers.simple.builders.processor.model.method.MethodDto; +import org.javahelpers.simple.builders.processor.model.method.BuilderMethodDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.processing.ProcessingContext; @@ -83,10 +83,10 @@ public boolean appliesTo(FieldDto field, TypeName dtoType, ProcessingContext con } @Override - public List generateMethods( + public List generateMethods( FieldDto field, TypeName builderType, ProcessingContext context) { - MethodDto setterMethod = + BuilderMethodDto setterMethod = MethodGeneratorUtil.createBuilderMethodForFieldWithTransform( field, null, field.getFieldType(), builderType, context); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/FieldInstanceConsumerGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/FieldInstanceConsumerGenerator.java index 756f3e90..09f5bbd2 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/FieldInstanceConsumerGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/FieldInstanceConsumerGenerator.java @@ -30,7 +30,7 @@ import java.util.List; import org.javahelpers.simple.builders.processor.generators.MethodGenerator; import org.javahelpers.simple.builders.processor.model.core.FieldDto; -import org.javahelpers.simple.builders.processor.model.method.MethodDto; +import org.javahelpers.simple.builders.processor.model.method.BuilderMethodDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.model.type.TypeNameList; import org.javahelpers.simple.builders.processor.model.type.TypeNameMap; @@ -127,13 +127,14 @@ private boolean hasSpecificCollectionConsumer(FieldDto field, ProcessingContext } @Override - public List generateMethods( + public List generateMethods( FieldDto field, TypeName builderType, ProcessingContext context) { if (!field.getFieldType().hasEmptyConstructor()) { return Collections.emptyList(); } - MethodDto method = createSimpleFieldConsumer(field, field.getFieldType(), builderType, context); + BuilderMethodDto method = + createSimpleFieldConsumer(field, field.getFieldType(), builderType, context); return Collections.singletonList(method); } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ListConsumerGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ListConsumerGenerator.java index df8fc205..45c1d24c 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ListConsumerGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ListConsumerGenerator.java @@ -38,7 +38,7 @@ import org.javahelpers.simple.builders.core.builders.ArrayListBuilderWithElementBuilders; import org.javahelpers.simple.builders.processor.generators.MethodGenerator; import org.javahelpers.simple.builders.processor.model.core.FieldDto; -import org.javahelpers.simple.builders.processor.model.method.MethodDto; +import org.javahelpers.simple.builders.processor.model.method.BuilderMethodDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.model.type.TypeNameGeneric; import org.javahelpers.simple.builders.processor.model.type.TypeNameList; @@ -116,7 +116,7 @@ public boolean appliesTo(FieldDto field, TypeName dtoType, ProcessingContext con } @Override - public List generateMethods( + public List generateMethods( FieldDto field, TypeName builderType, ProcessingContext context) { if (!(field.getFieldType() instanceof TypeNameList fieldTypeGeneric && fieldTypeGeneric.isParameterized())) { @@ -133,7 +133,7 @@ public List generateMethods( map2TypeName(ArrayListBuilderWithElementBuilders.class), elementType, elementBuilderType.get()); - MethodDto method = + BuilderMethodDto method = createFieldConsumerWithElementBuilders( field, collectionBuilderType, elementBuilderType.get(), builderType, context); @@ -144,7 +144,7 @@ public List generateMethods( } else if (context.getConfiguration().shouldUseArrayListBuilder()) { TypeName arrayListBuilderType = map2TypeName(ArrayListBuilder.class); TypeNameGeneric builderTypeGeneric = new TypeNameGeneric(arrayListBuilderType, elementType); - MethodDto method = + BuilderMethodDto method = createFieldConsumerWithBuilder( field, builderTypeGeneric, @@ -169,7 +169,8 @@ public List generateMethods( * @param method the method to add the example to * @param elementBuilderType the element builder type */ - private void addExampleToListConsumerWithBuilder(MethodDto method, TypeName elementBuilderType) { + private void addExampleToListConsumerWithBuilder( + BuilderMethodDto method, TypeName elementBuilderType) { String builderVar = StringUtils.uncapitalize(elementBuilderType.getClassName()); addExampleChainFragmentTemplate( method, "#{methodName}(t -> t.add(" + builderVar + " -> " + builderVar + "))"); @@ -181,7 +182,8 @@ private void addExampleToListConsumerWithBuilder(MethodDto method, TypeName elem * @param method the method to add the example to * @param elementType the element type */ - private void addExampleToListConsumerWithSimpleValue(MethodDto method, TypeName elementType) { + private void addExampleToListConsumerWithSimpleValue( + BuilderMethodDto method, TypeName elementType) { addExampleChainFragmentTemplate( method, "#{methodName}(t -> t.add(#{exampleValue}))", elementType); } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/MapConsumerGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/MapConsumerGenerator.java index 3f3181e4..ac80efb4 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/MapConsumerGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/MapConsumerGenerator.java @@ -34,7 +34,7 @@ import org.javahelpers.simple.builders.processor.generators.MethodGenerator; import org.javahelpers.simple.builders.processor.model.core.BuilderConfiguration; import org.javahelpers.simple.builders.processor.model.core.FieldDto; -import org.javahelpers.simple.builders.processor.model.method.MethodDto; +import org.javahelpers.simple.builders.processor.model.method.BuilderMethodDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.model.type.TypeNameGeneric; import org.javahelpers.simple.builders.processor.model.type.TypeNameMap; @@ -131,7 +131,7 @@ public boolean appliesTo( * @return list of generated methods */ @Override - public List generateMethods( + public List generateMethods( final FieldDto field, final TypeName builderType, final ProcessingContext context) { if (!(field.getFieldType() instanceof TypeNameMap fieldTypeGeneric && fieldTypeGeneric.isParameterized())) { @@ -143,7 +143,7 @@ public List generateMethods( map2TypeName(HashMapBuilder.class), fieldTypeGeneric.getKeyType(), fieldTypeGeneric.getValueType()); - MethodDto mapConsumerWithBuilder = + BuilderMethodDto mapConsumerWithBuilder = createFieldConsumerWithBuilder( field, builderTargetTypeName, diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/NestedBuilderConsumerGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/NestedBuilderConsumerGenerator.java index 0856ee79..7c645ccb 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/NestedBuilderConsumerGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/NestedBuilderConsumerGenerator.java @@ -33,7 +33,7 @@ import org.apache.commons.lang3.StringUtils; import org.javahelpers.simple.builders.processor.generators.MethodGenerator; import org.javahelpers.simple.builders.processor.model.core.FieldDto; -import org.javahelpers.simple.builders.processor.model.method.MethodDto; +import org.javahelpers.simple.builders.processor.model.method.BuilderMethodDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.processing.ProcessingContext; @@ -96,7 +96,7 @@ public boolean appliesTo(FieldDto field, TypeName dtoType, ProcessingContext con } @Override - public List generateMethods( + public List generateMethods( FieldDto field, TypeName builderType, ProcessingContext context) { Optional fieldBuilderOpt = field.getFieldType().getBuilderType(); if (fieldBuilderOpt.isEmpty()) { @@ -104,7 +104,7 @@ public List generateMethods( } TypeName fieldBuilderType = fieldBuilderOpt.get(); - MethodDto method = + BuilderMethodDto method = createFieldConsumerWithBuilder( field, fieldBuilderType, diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/OptionalHelperGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/OptionalHelperGenerator.java index 6a71a0d9..8e4771df 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/OptionalHelperGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/OptionalHelperGenerator.java @@ -33,7 +33,7 @@ import org.javahelpers.simple.builders.processor.generators.MethodGenerator; import org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil; import org.javahelpers.simple.builders.processor.model.core.FieldDto; -import org.javahelpers.simple.builders.processor.model.method.MethodDto; +import org.javahelpers.simple.builders.processor.model.method.BuilderMethodDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.model.type.TypeNameGeneric; import org.javahelpers.simple.builders.processor.processing.ProcessingContext; @@ -91,7 +91,7 @@ public boolean appliesTo(FieldDto field, TypeName dtoType, ProcessingContext con } @Override - public List generateMethods( + public List generateMethods( FieldDto field, TypeName builderType, ProcessingContext context) { TypeNameGeneric genericType = (TypeNameGeneric) field.getFieldType(); @@ -102,7 +102,7 @@ public List generateMethods( } TypeName innerType = innerTypes.get(0); - MethodDto method = + BuilderMethodDto method = MethodGeneratorUtil.createBuilderMethodForFieldWithTransform( field, "Optional.ofNullable(%s)", innerType, builderType, context); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/SetConsumerGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/SetConsumerGenerator.java index 3ad220ed..6ccf30b5 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/SetConsumerGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/SetConsumerGenerator.java @@ -36,7 +36,7 @@ import org.javahelpers.simple.builders.core.builders.HashSetBuilderWithElementBuilders; import org.javahelpers.simple.builders.processor.generators.MethodGenerator; import org.javahelpers.simple.builders.processor.model.core.FieldDto; -import org.javahelpers.simple.builders.processor.model.method.MethodDto; +import org.javahelpers.simple.builders.processor.model.method.BuilderMethodDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.model.type.TypeNameGeneric; import org.javahelpers.simple.builders.processor.model.type.TypeNameSet; @@ -112,7 +112,7 @@ public boolean appliesTo(FieldDto field, TypeName dtoType, ProcessingContext con } @Override - public List generateMethods( + public List generateMethods( FieldDto field, TypeName builderType, ProcessingContext context) { if (!(field.getFieldType() instanceof TypeNameSet fieldTypeGeneric && fieldTypeGeneric.isParameterized())) { @@ -129,7 +129,7 @@ public List generateMethods( map2TypeName(HashSetBuilderWithElementBuilders.class), elementType, elementBuilderType.get()); - MethodDto method = + BuilderMethodDto method = createFieldConsumerWithElementBuilders( field, collectionBuilderType, elementBuilderType.get(), builderType, context); addExampleToSetConsumerWithBuilder(method, elementBuilderType.get()); @@ -137,7 +137,7 @@ public List generateMethods( } else if (context.getConfiguration().shouldUseHashSetBuilder()) { TypeName hashSetBuilderType = map2TypeName(HashSetBuilder.class); TypeNameGeneric builderTypeGeneric = new TypeNameGeneric(hashSetBuilderType, elementType); - MethodDto method = + BuilderMethodDto method = createFieldConsumerWithBuilder( field, builderTypeGeneric, @@ -159,7 +159,8 @@ public List generateMethods( * @param method the method to add the example to * @param elementBuilderType the element builder type */ - private void addExampleToSetConsumerWithBuilder(MethodDto method, TypeName elementBuilderType) { + private void addExampleToSetConsumerWithBuilder( + BuilderMethodDto method, TypeName elementBuilderType) { String builderVar = StringUtils.uncapitalize(elementBuilderType.getClassName()); addExampleChainFragmentTemplate( method, "#{methodName}(t -> t.add(" + builderVar + " -> " + builderVar + "))"); @@ -171,7 +172,8 @@ private void addExampleToSetConsumerWithBuilder(MethodDto method, TypeName eleme * @param method the method to add the example to * @param elementType the element type */ - private void addExampleToSetConsumerWithSimpleValue(MethodDto method, TypeName elementType) { + private void addExampleToSetConsumerWithSimpleValue( + BuilderMethodDto method, TypeName elementType) { addExampleChainFragmentTemplate( method, "#{methodName}(t -> t.add(#{exampleValue}))", elementType); } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringBuilderConsumerGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringBuilderConsumerGenerator.java index 54cbbc23..1fafcccf 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringBuilderConsumerGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringBuilderConsumerGenerator.java @@ -34,7 +34,7 @@ import org.javahelpers.simple.builders.processor.generators.util.JavadocConstants; import org.javahelpers.simple.builders.processor.model.core.FieldDto; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; -import org.javahelpers.simple.builders.processor.model.method.MethodDto; +import org.javahelpers.simple.builders.processor.model.method.BuilderMethodDto; import org.javahelpers.simple.builders.processor.model.method.MethodParameterDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.model.type.TypeNameArray; @@ -105,13 +105,13 @@ public boolean appliesTo(FieldDto field, TypeName dtoType, ProcessingContext con } @Override - public List generateMethods( + public List generateMethods( FieldDto field, TypeName builderType, ProcessingContext context) { String transform = isOptionalString(field.getFieldType()) ? "Optional.of(builder.toString())" : "builder.toString()"; - MethodDto method = + BuilderMethodDto method = createStringBuilderConsumer( field.getOriginalFieldName(), field.getFieldNameInBuilder(), @@ -128,7 +128,7 @@ public List generateMethods( return List.of(method); } - private MethodDto createStringBuilderConsumer( + private BuilderMethodDto createStringBuilderConsumer( String fieldName, String fieldNameInBuilder, String fieldJavadoc, @@ -140,9 +140,8 @@ private MethodDto createStringBuilderConsumer( MethodParameterDto parameter = new MethodParameterDto(); parameter.setParameterName(fieldName + "StringBuilderConsumer"); parameter.setParameterTypeName(consumerType); - MethodDto methodDto = new MethodDto(generateBuilderMethodName(fieldName, context), builderType); + BuilderMethodDto methodDto = createBuilderMethod(fieldName, builderType, context); methodDto.addParameter(parameter); - methodDto.setModifier(getMethodAccessModifier(context)); methodDto.setCode( """ StringBuilder builder = new StringBuilder(); @@ -155,7 +154,7 @@ private MethodDto createStringBuilderConsumer( methodDto.addArgument("transform", transform); methodDto.addArgument("builderFieldWrapper", TRACKED_VALUE_TYPE); methodDto.setReturnType(builderType); - methodDto.setPriority(MethodDto.PRIORITY_LOW); + methodDto.setPriority(BuilderMethodDto.PRIORITY_LOW); methodDto.setJavadoc( new JavadocDto( "Sets the value for %s by executing the provided consumer.", fieldName) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringFormatHelperGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringFormatHelperGenerator.java index e1332be0..32eeeaef 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringFormatHelperGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringFormatHelperGenerator.java @@ -38,7 +38,7 @@ import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto; import org.javahelpers.simple.builders.processor.model.core.FieldDto; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; -import org.javahelpers.simple.builders.processor.model.method.MethodDto; +import org.javahelpers.simple.builders.processor.model.method.BuilderMethodDto; import org.javahelpers.simple.builders.processor.model.method.MethodParameterDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.model.type.TypeNameArray; @@ -112,14 +112,14 @@ public boolean appliesTo(FieldDto field, TypeName dtoType, ProcessingContext con } @Override - public List generateMethods( + public List generateMethods( FieldDto field, TypeName builderType, ProcessingContext context) { - List methods = new ArrayList<>(); + List methods = new ArrayList<>(); TypeName fieldType = field.getFieldType(); if (isString(fieldType) && !(fieldType instanceof TypeNameArray)) { - MethodDto method = + BuilderMethodDto method = createStringFormatMethodWithTransform( field.getOriginalFieldName(), field.getFieldNameInBuilder(), @@ -132,7 +132,7 @@ public List generateMethods( TypeNameGeneric genericType = (TypeNameGeneric) fieldType; List innerTypes = genericType.getInnerTypeArguments(); if (!innerTypes.isEmpty() && isString(innerTypes.get(0))) { - MethodDto method = + BuilderMethodDto method = createStringFormatMethodWithTransform( field.getOriginalFieldName(), field.getFieldNameInBuilder(), @@ -162,7 +162,7 @@ public List generateMethods( * @param context processing context * @return the method DTO for the String.format helper */ - private MethodDto createStringFormatMethodWithTransform( + private BuilderMethodDto createStringFormatMethodWithTransform( String fieldName, String fieldNameInBuilder, String transform, @@ -182,10 +182,9 @@ private MethodDto createStringFormatMethodWithTransform( argsParam.setParameterName("args"); argsParam.setParameterTypeName(new TypeNameArray(TypeName.of(Object.class))); - MethodDto methodDto = new MethodDto(generateBuilderMethodName(fieldName, context), builderType); + BuilderMethodDto methodDto = createBuilderMethod(fieldName, builderType, context); methodDto.addParameter(formatParam); methodDto.addParameter(argsParam); - methodDto.setModifier(getMethodAccessModifier(context)); methodDto.setCode( """ this.$fieldName:N = $builderFieldWrapper:T.changedValue($transform:N); @@ -194,7 +193,7 @@ private MethodDto createStringFormatMethodWithTransform( methodDto.addArgument("fieldName", fieldNameInBuilder); methodDto.addArgument("transform", transform); methodDto.addArgument("builderFieldWrapper", TRACKED_VALUE_TYPE); - methodDto.setPriority(MethodDto.PRIORITY_HIGH); + methodDto.setPriority(BuilderMethodDto.PRIORITY_HIGH); methodDto.setJavadoc( new JavadocDto( "Sets the String value for %s by using String.format(format, args).\nSee {@link String#format(String, Object...)} for details.", diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/SupplierMethodGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/SupplierMethodGenerator.java index 386f83b0..66878747 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/SupplierMethodGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/SupplierMethodGenerator.java @@ -28,8 +28,7 @@ import static org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil.SUFFIX_SUPPLIER; import static org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil.TRACKED_VALUE_TYPE; import static org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil.addExampleChainFragmentWithSupplier; -import static org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil.generateBuilderMethodName; -import static org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil.getMethodAccessModifier; +import static org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil.createBuilderMethod; import java.util.Collections; import java.util.List; @@ -38,7 +37,7 @@ import org.javahelpers.simple.builders.processor.generators.util.JavadocConstants; import org.javahelpers.simple.builders.processor.model.core.FieldDto; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; -import org.javahelpers.simple.builders.processor.model.method.MethodDto; +import org.javahelpers.simple.builders.processor.model.method.BuilderMethodDto; import org.javahelpers.simple.builders.processor.model.method.MethodParameterDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.model.type.TypeNameGeneric; @@ -94,10 +93,10 @@ public boolean appliesTo(FieldDto field, TypeName dtoType, ProcessingContext con } @Override - public List generateMethods( + public List generateMethods( FieldDto field, TypeName builderType, ProcessingContext context) { - MethodDto supplierMethod = + BuilderMethodDto supplierMethod = createFieldSupplier( field.getOriginalFieldName(), field.getFieldNameInBuilder(), @@ -121,7 +120,7 @@ public List generateMethods( * @param context processing context * @return the method DTO for the supplier */ - private MethodDto createFieldSupplier( + private BuilderMethodDto createFieldSupplier( String fieldName, String fieldNameInBuilder, String fieldJavaDoc, @@ -135,9 +134,8 @@ private MethodDto createFieldSupplier( parameter.setParameterName(parameterName); parameter.setParameterTypeName(supplierType); - MethodDto methodDto = new MethodDto(generateBuilderMethodName(fieldName, context), builderType); + BuilderMethodDto methodDto = createBuilderMethod(fieldName, builderType, context); methodDto.addParameter(parameter); - methodDto.setModifier(getMethodAccessModifier(context)); methodDto.setCode( """ @@ -147,7 +145,7 @@ private MethodDto createFieldSupplier( methodDto.addArgument("fieldName", fieldNameInBuilder); methodDto.addArgument("dtoMethodParam", parameterName); methodDto.addArgument("builderFieldWrapper", TRACKED_VALUE_TYPE); - methodDto.setPriority(MethodDto.PRIORITY_HIGH); + methodDto.setPriority(BuilderMethodDto.PRIORITY_HIGH); methodDto.setJavadoc( new JavadocDto( diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/VarArgsHelperGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/VarArgsHelperGenerator.java index b02dca46..a6572d6e 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/VarArgsHelperGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/VarArgsHelperGenerator.java @@ -31,7 +31,7 @@ import org.javahelpers.simple.builders.processor.generators.MethodGenerator; import org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil; import org.javahelpers.simple.builders.processor.model.core.FieldDto; -import org.javahelpers.simple.builders.processor.model.method.MethodDto; +import org.javahelpers.simple.builders.processor.model.method.BuilderMethodDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.model.type.TypeNameArray; import org.javahelpers.simple.builders.processor.model.type.TypeNameGeneric; @@ -98,7 +98,7 @@ public boolean appliesTo(FieldDto field, TypeName dtoType, ProcessingContext con } @Override - public List generateMethods( + public List generateMethods( FieldDto field, TypeName builderType, ProcessingContext context) { TypeName fieldType = field.getFieldType(); @@ -119,7 +119,7 @@ public List generateMethods( return Collections.emptyList(); } - MethodDto varArgsMethod = + BuilderMethodDto varArgsMethod = createFieldSetterByVarArgs(field, parameterType, builderType, context); return Collections.singletonList(varArgsMethod); } @@ -135,7 +135,7 @@ public List generateMethods( * @param context processing context * @return the method DTO for the setter */ - private MethodDto createFieldSetterByVarArgs( + private BuilderMethodDto createFieldSetterByVarArgs( FieldDto field, TypeName parameterType, TypeName builderType, ProcessingContext context) { String baseExpression; TypeName fieldType = field.getFieldType(); @@ -153,7 +153,7 @@ private MethodDto createFieldSetterByVarArgs( } String transform = MethodGeneratorUtil.wrapConcreteCollectionType(fieldType, baseExpression); - MethodDto method = + BuilderMethodDto method = MethodGeneratorUtil.createBuilderMethodForFieldWithTransform( field, transform, parameterType, builderType, context); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/registry/GeneratorRegistry.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/registry/GeneratorRegistry.java index b31b0494..ef0ebce8 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/registry/GeneratorRegistry.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/registry/GeneratorRegistry.java @@ -35,7 +35,7 @@ import org.javahelpers.simple.builders.processor.generators.MethodGenerator; import org.javahelpers.simple.builders.processor.model.core.BuilderDefinitionDto; import org.javahelpers.simple.builders.processor.model.core.FieldDto; -import org.javahelpers.simple.builders.processor.model.method.MethodDto; +import org.javahelpers.simple.builders.processor.model.method.BuilderMethodDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.processing.ProcessingContext; @@ -90,9 +90,9 @@ public GeneratorRegistry(ProcessingContext context, ProcessingEnvironment proces * @param builderType the type of the builder being generated, should not be null * @return list of all generated methods from all applicable generators */ - public List generateAllMethods( + public List generateAllMethods( FieldDto field, TypeName dtoType, TypeName builderType) { - List allMethods = new ArrayList<>(); + List allMethods = new ArrayList<>(); context.debugStartOperation("Processing method generators"); for (MethodGenerator generator : methodGenerators) { @@ -102,7 +102,8 @@ public List generateAllMethods( "Applying: %s (priority: %d)", generator.getClass().getSimpleName(), generator.getPriority()); - List generatedMethods = generator.generateMethods(field, builderType, context); + List generatedMethods = + generator.generateMethods(field, builderType, context); if (CollectionUtils.isNotEmpty(generatedMethods)) { allMethods.addAll(generatedMethods); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java index 4bd9e080..3fb9f061 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java @@ -32,7 +32,7 @@ import org.javahelpers.simple.builders.processor.analysis.JavaLangMapper; import org.javahelpers.simple.builders.processor.model.core.FieldDto; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; -import org.javahelpers.simple.builders.processor.model.method.MethodDto; +import org.javahelpers.simple.builders.processor.model.method.BuilderMethodDto; import org.javahelpers.simple.builders.processor.model.method.MethodParameterDto; import org.javahelpers.simple.builders.processor.model.type.GenericParameterDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; @@ -99,6 +99,23 @@ public static AccessModifier getMethodAccessModifier(ProcessingContext context) return context.getConfiguration().getMethodAccess(); } + /** + * Creates a {@link BuilderMethodDto} with the method name derived from the field name and + * configuration, and the access modifier set from the processing context. + * + * @param fieldName the field name to derive the method name from + * @param returnType the return type of the method + * @param context the processing context + * @return a new {@link BuilderMethodDto} with name and modifier set + */ + public static BuilderMethodDto createBuilderMethod( + String fieldName, TypeName returnType, ProcessingContext context) { + BuilderMethodDto method = + new BuilderMethodDto(generateBuilderMethodName(fieldName, context), returnType); + method.setModifier(getMethodAccessModifier(context)); + return method; + } + /** * Creates a generic TypeName from a base type and generic parameters. * @@ -136,7 +153,7 @@ public static TypeName createGenericTypeName( * @param context processing context * @return the method DTO for the setter */ - public static MethodDto createBuilderMethodForFieldWithTransform( + public static BuilderMethodDto createBuilderMethodForFieldWithTransform( FieldDto field, String transform, TypeName parameterType, @@ -151,11 +168,9 @@ public static MethodDto createBuilderMethodForFieldWithTransform( field.getParameterAnnotations().forEach(parameter::addAnnotation); } - MethodDto methodDto = - new MethodDto( - generateBuilderMethodName(field.getOriginalFieldName(), context), builderType); + BuilderMethodDto methodDto = + createBuilderMethod(field.getOriginalFieldName(), builderType, context); methodDto.addParameter(parameter); - methodDto.setModifier(getMethodAccessModifier(context)); String params; if (StringUtils.isBlank(transform)) { @@ -173,7 +188,8 @@ public static MethodDto createBuilderMethodForFieldWithTransform( methodDto.addArgument("dtoMethodParams", params); methodDto.addArgument("builderFieldWrapper", TRACKED_VALUE_TYPE); - methodDto.setPriority(transform == null ? MethodDto.PRIORITY_HIGHEST : MethodDto.PRIORITY_HIGH); + methodDto.setPriority( + transform == null ? BuilderMethodDto.PRIORITY_HIGHEST : BuilderMethodDto.PRIORITY_HIGH); String fieldJavadocDesc = field.getJavaDocDescriptionOrFieldName(); methodDto.setJavadoc( @@ -196,7 +212,7 @@ public static MethodDto createBuilderMethodForFieldWithTransform( * @param context the processing context * @return the method DTO for the consumer */ - public static MethodDto createFieldConsumerWithBuilder( + public static BuilderMethodDto createFieldConsumerWithBuilder( FieldDto field, TypeName fieldBuilderType, String existingValueConstructorArgs, @@ -208,11 +224,9 @@ public static MethodDto createFieldConsumerWithBuilder( MethodParameterDto parameter = new MethodParameterDto(); parameter.setParameterName(field.getFieldNameInBuilder() + BUILDER_SUFFIX + SUFFIX_CONSUMER); parameter.setParameterTypeName(consumerType); - MethodDto methodDto = - new MethodDto( - generateBuilderMethodName(field.getOriginalFieldName(), context), parentBuilderType); + BuilderMethodDto methodDto = + createBuilderMethod(field.getOriginalFieldName(), parentBuilderType, context); methodDto.addParameter(parameter); - methodDto.setModifier(getMethodAccessModifier(context)); String buildExpression = calculateBuildExpression(field.getFieldType()); @@ -232,7 +246,7 @@ public static MethodDto createFieldConsumerWithBuilder( methodDto.addArgument("buildExpression", buildExpression); additionalTemplateArguments.forEach(methodDto::addArgument); methodDto.addArgument("builderFieldWrapper", TRACKED_VALUE_TYPE); - methodDto.setPriority(MethodDto.PRIORITY_MEDIUM); + methodDto.setPriority(BuilderMethodDto.PRIORITY_MEDIUM); String fieldJavadocDesc = field.getJavaDocDescriptionOrFieldName(); methodDto.setJavadoc( new JavadocDto( @@ -310,7 +324,7 @@ public static String wrapConcreteCollectionType(TypeName fieldType, String baseE * @param context the processing context * @return the method DTO for the consumer */ - public static MethodDto createFieldConsumerWithElementBuilders( + public static BuilderMethodDto createFieldConsumerWithElementBuilders( FieldDto field, TypeName collectionBuilderType, TypeName elementBuilderType, @@ -338,18 +352,16 @@ public static MethodDto createFieldConsumerWithElementBuilders( * @param context the processing context * @return the method DTO for the simple field consumer */ - public static MethodDto createSimpleFieldConsumer( + public static BuilderMethodDto createSimpleFieldConsumer( FieldDto field, TypeName fieldType, TypeName builderType, ProcessingContext context) { TypeNameGeneric consumerType = createConsumerType(fieldType); MethodParameterDto parameter = new MethodParameterDto(); parameter.setParameterName(field.getOriginalFieldName() + SUFFIX_CONSUMER); parameter.setParameterTypeName(consumerType); - MethodDto methodDto = - new MethodDto( - generateBuilderMethodName(field.getOriginalFieldName(), context), builderType); + BuilderMethodDto methodDto = + createBuilderMethod(field.getOriginalFieldName(), builderType, context); methodDto.addParameter(parameter); - methodDto.setModifier(getMethodAccessModifier(context)); methodDto.setCode( """ @@ -364,7 +376,7 @@ public static MethodDto createSimpleFieldConsumer( methodDto.addArgument("dtoMethodParam", parameter.getParameterName()); methodDto.addArgument("helperType", fieldType); methodDto.addArgument("builderFieldWrapper", TRACKED_VALUE_TYPE); - methodDto.setPriority(MethodDto.PRIORITY_MEDIUM); + methodDto.setPriority(BuilderMethodDto.PRIORITY_MEDIUM); String fieldJavadocDesc = field.getJavaDocDescriptionOrFieldName(); methodDto.setJavadoc( @@ -393,7 +405,7 @@ public static MethodDto createSimpleFieldConsumer( * @param methodDto the method DTO to add the fragment to * @param fieldType the field type to get an example value for */ - public static void addExampleChainFragment(MethodDto methodDto, TypeName fieldType) { + public static void addExampleChainFragment(BuilderMethodDto methodDto, TypeName fieldType) { addExampleChainFragmentTemplate(methodDto, "#{methodName}(#{exampleValue})", fieldType); } @@ -411,7 +423,8 @@ public static void addExampleChainFragment(MethodDto methodDto, TypeName fieldTy * @param methodDto the method DTO to add the fragment to * @param elementType the element type to get an example value for */ - public static void addExampleChainFragmentVarArgs(MethodDto methodDto, TypeName elementType) { + public static void addExampleChainFragmentVarArgs( + BuilderMethodDto methodDto, TypeName elementType) { addExampleChainFragmentTemplate( methodDto, "#{methodName}(#{exampleValue}, #{exampleValue})", elementType); } @@ -430,7 +443,8 @@ public static void addExampleChainFragmentVarArgs(MethodDto methodDto, TypeName * @param methodDto the method DTO to add the fragment to * @param fieldType the field type to get an example value for */ - public static void addExampleChainFragmentWithSupplier(MethodDto methodDto, TypeName fieldType) { + public static void addExampleChainFragmentWithSupplier( + BuilderMethodDto methodDto, TypeName fieldType) { if (fieldType.hasEmptyConstructor()) { addExampleChainFragmentTemplate( methodDto, "#{methodName}(" + fieldType.getClassName() + "::new)"); @@ -453,7 +467,7 @@ public static void addExampleChainFragmentWithSupplier(MethodDto methodDto, Type * @param exampleValue the hardcoded example value to use */ public static void addExampleChainFragmentWithHardcodedValue( - MethodDto methodDto, String exampleValue) { + BuilderMethodDto methodDto, String exampleValue) { addExampleChainFragmentTemplate(methodDto, "#{methodName}(%s)".formatted(exampleValue)); } @@ -477,7 +491,7 @@ public static void addExampleChainFragmentWithHardcodedValue( * @param fieldType the field type to get an example value for (can be {@code null}) */ public static void addExampleChainFragmentTemplate( - MethodDto methodDto, String template, TypeName fieldType) { + BuilderMethodDto methodDto, String template, TypeName fieldType) { String fragment = template.replace("#{methodName}", methodDto.getMethodName()); if (fieldType != null) { @@ -507,7 +521,7 @@ public static void addExampleChainFragmentTemplate( * @param template the template string with placeholders (e.g., {@code #{methodName}(sb -> * sb.append("text"))}) */ - public static void addExampleChainFragmentTemplate(MethodDto methodDto, String template) { + public static void addExampleChainFragmentTemplate(BuilderMethodDto methodDto, String template) { String fragment = template.replace("#{methodName}", methodDto.getMethodName()); methodDto.setExampleChainFragment(fragment); } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/annotation/AnnotationDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/annotation/AnnotationDto.java index 4df62235..9787249f 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/annotation/AnnotationDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/annotation/AnnotationDto.java @@ -31,9 +31,8 @@ import org.javahelpers.simple.builders.processor.model.type.TypeName; /** - * DTO representing an annotation to be copied from the target class field to the builder class - * field. Contains the annotation type and its members in a plain format suitable for code - * generation. + * DTO representing an annotation to be generated in the output class. Contains the annotation type + * and its members in a plain format suitable for code generation. */ public class AnnotationDto { /** The annotation type (fully qualified name). */ diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/annotation/InterfaceName.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/annotation/InterfaceName.java index c39d40e0..e164d51d 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/annotation/InterfaceName.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/annotation/InterfaceName.java @@ -36,15 +36,15 @@ * InterfaceName represents a Java interface type with package and class name information. * *

    This type is specifically designed for interfaces and contains only interface-relevant - * information. Unlike {@link TypeName}, it doesn't include class-specific concepts like builders, - * constructors, or inner types. + * information. Unlike {@link TypeName}, it doesn't include class-specific concepts like + * constructors or inner types. * *

    Typical usage includes: * *

      - *
    • IBuilderBase interface for builder contracts - *
    • Custom interfaces for builder extensions - *
    • Mixin interfaces for Jackson serialization + *
    • Base interfaces for generated class contracts + *
    • Custom interfaces for generated class extensions + *
    • Mixin interfaces for serialization frameworks *
    */ public class InterfaceName { diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderDefinitionDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderDefinitionDto.java index 7bc198e2..88b4f925 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderDefinitionDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderDefinitionDto.java @@ -24,17 +24,37 @@ package org.javahelpers.simple.builders.processor.model.core; +import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; +import java.util.Set; +import org.javahelpers.simple.builders.core.enums.AccessModifier; +import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto; +import org.javahelpers.simple.builders.processor.model.annotation.InterfaceName; +import org.javahelpers.simple.builders.processor.model.imports.ImportStatement; +import org.javahelpers.simple.builders.processor.model.imports.RegularImport; +import org.javahelpers.simple.builders.processor.model.imports.StaticImport; +import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; +import org.javahelpers.simple.builders.processor.model.method.BuilderMethodDto; +import org.javahelpers.simple.builders.processor.model.method.ConstructorDto; +import org.javahelpers.simple.builders.processor.model.type.BuilderNestedTypeDto; +import org.javahelpers.simple.builders.processor.model.type.GenericParameterDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; +import org.javahelpers.simple.builders.processor.model.type.TypeNameArray; +import org.javahelpers.simple.builders.processor.model.type.TypeNameGeneric; /** - * Builder-specific extension of GenerationTargetClassDto. + * Generation-side DTO holding all information for generating a builder. * - *

    This class holds all information for generating a builder, including builder-specific fields - * and methods that are not part of the generic class generation. + *

    This class does NOT extend {@link GenerationTargetClassDto}. Instead, it holds generation-side + * data (including {@link BuilderMethodDto} and {@link BuilderNestedTypeDto}) and produces a {@link + * GenerationTargetClassDto} (rendering DTO) during finalization in {@code + * BuilderDefinitionCreator.finalizeDefinition()}. */ -public class BuilderDefinitionDto extends GenerationTargetClassDto { +public class BuilderDefinitionDto { + + // --- Builder-specific fields --- + /** Target type of result of building by builder. Containing package and name of DTO. */ private TypeName buildingTargetTypeName; @@ -54,22 +74,170 @@ public class BuilderDefinitionDto extends GenerationTargetClassDto { /** Configuration for builder generation. */ private BuilderConfiguration configuration; + // --- Rendering-side fields (set during enhancement, mapped during finalization) --- + + /** Generated class FQN. */ + private TypeName typeName; + + /** Class visibility. */ + private AccessModifier classAccessModifier; + + /** Superclass, null if none. */ + private TypeName superType; + + /** Field declarations. */ + private final List classFields = new LinkedList<>(); + + /** Constructor definitions. */ + private final List constructors = new LinkedList<>(); + + /** + * Builder-level methods generated by enhancers (not tied to a specific field). These are + * BuilderMethodDto instances that will be mapped to MethodDto during finalization. + */ + private final List methods = new LinkedList<>(); + + /** Generic parameters declared on the target DTO (e.g., {@code }). */ + private final List generics = new LinkedList<>(); + + /** Imports (both regular and static imports). */ + private final Set imports = new LinkedHashSet<>(); + /** - * Getting type of builder. Delegates to base class typeName. + * Nested types generated by enhancers (generation-side). These are BuilderNestedTypeDto instances + * that will be mapped to NestedTypeDto during finalization. + */ + private final List nestedTypes = new LinkedList<>(); + + /** Class-level annotations. */ + private final Set classAnnotations = new LinkedHashSet<>(); + + /** Interfaces to be implemented by the generated builder class. */ + private final Set interfaces = new LinkedHashSet<>(); + + /** Class-level JavaDoc for the generated builder class. */ + private JavadocDto classJavadoc; + + /** + * Getting type of builder. * * @return package and name of builder. */ public TypeName getBuilderTypeName() { - return getTypeName(); + return typeName; } /** - * Setting type of builder. Delegates to base class typeName. + * Setting type of builder. * * @param builderClassName type of builder */ public void setBuilderTypeName(TypeName builderClassName) { - setTypeName(builderClassName); + this.typeName = builderClassName; + } + + public TypeName getTypeName() { + return typeName; + } + + public void setTypeName(TypeName typeName) { + this.typeName = typeName; + } + + public AccessModifier getClassAccessModifier() { + return classAccessModifier; + } + + public void setClassAccessModifier(AccessModifier classAccessModifier) { + this.classAccessModifier = classAccessModifier; + } + + public TypeName getSuperType() { + return superType; + } + + public void setSuperType(TypeName superType) { + this.superType = superType; + } + + public List getClassFields() { + return classFields; + } + + public void addClassField(ClassFieldDto classField) { + this.classFields.add(classField); + } + + public List getConstructors() { + return constructors; + } + + public void addConstructor(ConstructorDto constructor) { + this.constructors.add(constructor); + } + + public Set getImports() { + return imports; + } + + public void addImport(ImportStatement importStatement) { + this.imports.add(importStatement); + } + + public void addImport(Class clazz) { + addImport(new RegularImport(TypeName.of(clazz))); + } + + public void addImport(TypeName typeName) { + if (typeName instanceof TypeNameGeneric generic) { + addImport(generic.getRawType()); + generic.getInnerTypeArguments().forEach(this::addImport); + } else if (typeName instanceof TypeNameArray array) { + addImport(array.getTypeOfArray()); + } else { + addImport(new RegularImport(typeName)); + } + } + + public void addStaticImport(Class clazz, String memberName) { + addImport(new StaticImport(TypeName.of(clazz), memberName)); + } + + public void addStaticImport(TypeName type, String memberName) { + TypeName typeRaw = new TypeName(type.getPackageName(), type.getClassName()); + addImport(new StaticImport(typeRaw, memberName)); + } + + public void addGeneric(GenericParameterDto generic) { + generics.add(generic); + } + + public List getGenerics() { + return generics; + } + + public Set getClassAnnotations() { + return classAnnotations; + } + + public void addClassAnnotation(AnnotationDto annotation) { + this.classAnnotations.add(annotation); + } + + public Set getInterfaces() { + return interfaces; + } + + public void addInterface(InterfaceName interfaceType) { + this.interfaces.add(interfaceType); + } + + public JavadocDto getClassJavadoc() { + return classJavadoc; + } + + public void setClassJavadoc(JavadocDto classJavadoc) { + this.classJavadoc = classJavadoc; } /** @@ -174,4 +342,40 @@ public BuilderConfiguration getConfiguration() { public void setConfiguration(BuilderConfiguration configuration) { this.configuration = configuration; } + + /** + * Adds a builder-level method generated by an enhancer. + * + * @param method the builder method to add + */ + public void addMethod(BuilderMethodDto method) { + this.methods.add(method); + } + + /** + * Returns all builder-level methods added by enhancers. + * + * @return list of builder-level methods + */ + public List getMethods() { + return methods; + } + + /** + * Adds a nested type generated by an enhancer. + * + * @param nestedType the nested type to add + */ + public void addNestedType(BuilderNestedTypeDto nestedType) { + this.nestedTypes.add(nestedType); + } + + /** + * Returns all nested types added by enhancers. + * + * @return list of nested types + */ + public List getNestedTypes() { + return nestedTypes; + } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderToGenerationTypeMapper.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderToGenerationTypeMapper.java new file mode 100644 index 00000000..9e4e1e36 --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderToGenerationTypeMapper.java @@ -0,0 +1,176 @@ +/* + * MIT License + * + * Copyright (c) 2026 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.model.core; + +import java.util.List; +import org.javahelpers.simple.builders.processor.model.method.BuilderMethodDto; +import org.javahelpers.simple.builders.processor.model.method.MethodCodePlaceholder; +import org.javahelpers.simple.builders.processor.model.method.MethodCodeStringPlaceholder; +import org.javahelpers.simple.builders.processor.model.method.MethodCodeTypePlaceholder; +import org.javahelpers.simple.builders.processor.model.method.MethodDto; +import org.javahelpers.simple.builders.processor.model.type.BuilderNestedTypeDto; +import org.javahelpers.simple.builders.processor.model.type.NestedTypeDto; + +/** + * Maps generation-side DTOs ({@link BuilderDefinitionDto}, {@link BuilderMethodDto}, {@link + * BuilderNestedTypeDto}) to rendering-side DTOs ({@link GenerationTargetClassDto}, {@link + * MethodDto}, {@link NestedTypeDto}). + * + *

    This mapper copies all rendering-relevant fields from the generation DTOs to the rendering + * DTOs. Generation-only fields ({@code sourceFieldName}, {@code constructorField}, {@code + * exampleChainFragment}) are not mapped. + */ +public class BuilderToGenerationTypeMapper { + + private BuilderToGenerationTypeMapper() { + // Utility class - prevent instantiation + } + + /** + * Maps a {@link BuilderDefinitionDto} (generation DTO) to a {@link GenerationTargetClassDto} + * (rendering DTO) for code generation. + * + *

    This maps all rendering-relevant fields, converting {@link BuilderMethodDto} to {@link + * MethodDto} and {@link BuilderNestedTypeDto} to {@link NestedTypeDto}. + * + * @param builderDto the generation DTO + * @return the rendering DTO for code generation + */ + public static GenerationTargetClassDto toRenderingDto(BuilderDefinitionDto builderDto) { + GenerationTargetClassDto renderingDto = new GenerationTargetClassDto(); + renderingDto.setTypeName(builderDto.getTypeName()); + renderingDto.setClassAccessModifier(builderDto.getClassAccessModifier()); + renderingDto.setSuperType(builderDto.getSuperType()); + renderingDto.setClassJavadoc(builderDto.getClassJavadoc()); + + // Copy class fields + builderDto.getClassFields().forEach(renderingDto::addClassField); + + // Copy constructors + builderDto.getConstructors().forEach(renderingDto::addConstructor); + + // Copy generics + builderDto.getGenerics().forEach(renderingDto::addGeneric); + + // Copy imports + builderDto.getImports().forEach(renderingDto::addImport); + + // Copy class annotations + builderDto.getClassAnnotations().forEach(renderingDto::addClassAnnotation); + + // Copy interfaces + builderDto.getInterfaces().forEach(renderingDto::addInterface); + + // Map and copy methods from fields + for (FieldDto field : builderDto.getConstructorFieldsForBuilder()) { + for (BuilderMethodDto method : field.getMethods()) { + renderingDto.addMethod(toMethodDto(method)); + } + } + for (FieldDto field : builderDto.getSetterFieldsForBuilder()) { + for (BuilderMethodDto method : field.getMethods()) { + renderingDto.addMethod(toMethodDto(method)); + } + } + + // Map and copy builder-level methods from enhancers + for (BuilderMethodDto classMethod : builderDto.getMethods()) { + renderingDto.addMethod(toMethodDto(classMethod)); + } + + // Map and copy nested types from enhancers + for (BuilderNestedTypeDto builderNestedType : builderDto.getNestedTypes()) { + renderingDto.addNestedType(toNestedTypeDto(builderNestedType)); + } + + return renderingDto; + } + + /** + * Maps a {@link BuilderMethodDto} to a {@link MethodDto}. + * + *

    All rendering-relevant fields are copied. The {@code MethodCodeDto} is shared by reference + * (not deep-copied), since the rendering phase only reads from it. + * + * @param classMethod the generation DTO to map + * @return a new {@link MethodDto} with all rendering fields copied + */ + public static MethodDto toMethodDto(BuilderMethodDto classMethod) { + MethodDto method = new MethodDto(classMethod.getMethodName(), classMethod.getReturnType()); + method.setModifier(classMethod.getModifier().orElse(null)); + method.setStatic(classMethod.isStatic()); + method.setPriority(classMethod.getPriority()); + method.setOrdering(classMethod.getOrdering()); + method.setJavadoc(classMethod.getJavadoc()); + classMethod.getAnnotations().forEach(method::addAnnotation); + classMethod.getParameters().forEach(method::addParameter); + classMethod.getGenericParameters().forEach(method::addGenericParameter); + + // Copy method code: set code format and copy all arguments + if (classMethod.hasCode()) { + method.setCode(classMethod.getMethodCodeDto().getCodeFormat()); + for (MethodCodePlaceholder argument : classMethod.getMethodCodeDto().getCodeArguments()) { + if (argument instanceof MethodCodeStringPlaceholder stringPlaceholder) { + method.addArgument(stringPlaceholder.getLabel(), stringPlaceholder.getValue()); + } else if (argument instanceof MethodCodeTypePlaceholder typePlaceholder) { + method.addArgument(typePlaceholder.getLabel(), typePlaceholder.getValue()); + } + } + } + + return method; + } + + /** + * Maps a list of {@link BuilderMethodDto} to a list of {@link MethodDto}. + * + * @param methods the generation DTOs to map + * @return a list of new {@link MethodDto} instances + */ + public static List toMethodDtoList(List methods) { + return methods.stream().map(BuilderToGenerationTypeMapper::toMethodDto).toList(); + } + + /** + * Maps a {@link BuilderNestedTypeDto} (generation DTO) to a {@link NestedTypeDto} (rendering + * DTO). + * + *

    All rendering-relevant fields are copied, including type name, kind, visibility, javadoc, + * annotations, and methods (mapped via {@link #toMethodDto}). + * + * @param builderNestedType the generation DTO to map + * @return a new {@link NestedTypeDto} with all rendering fields copied + */ + public static NestedTypeDto toNestedTypeDto(BuilderNestedTypeDto builderNestedType) { + NestedTypeDto nestedType = new NestedTypeDto(); + nestedType.setTypeName(builderNestedType.getTypeName()); + nestedType.setKind(builderNestedType.getKind()); + nestedType.setVisibility(builderNestedType.getVisibility()); + nestedType.setJavadoc(builderNestedType.getJavadoc()); + builderNestedType.getAnnotations().forEach(nestedType::addAnnotation); + builderNestedType.getMethods().forEach(method -> nestedType.addMethod(toMethodDto(method))); + return nestedType; + } +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/ClassFieldDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/ClassFieldDto.java index c465d62d..8a75a55f 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/ClassFieldDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/ClassFieldDto.java @@ -42,8 +42,7 @@ /** * Represents a field declaration in the generated class. * - *

    This DTO contains only rendering information for a field. It is created from {@code FieldDto} - * during finalization in {@code BuilderDefinitionCreator}. + *

    This DTO contains only rendering information for a field, consumed by the code generator. */ public class ClassFieldDto { /** Field name in generated class. */ diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/FieldDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/FieldDto.java index 3f565fdb..d5107f15 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/FieldDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/FieldDto.java @@ -30,7 +30,7 @@ import org.apache.commons.lang3.StringUtils; import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; -import org.javahelpers.simple.builders.processor.model.method.MethodDto; +import org.javahelpers.simple.builders.processor.model.method.BuilderMethodDto; import org.javahelpers.simple.builders.processor.model.type.GenericParameterDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; @@ -49,7 +49,7 @@ public class FieldDto { private TypeName fieldType; /** List of all methods in builder, which provide helpers to change the field. */ - private final List fieldSetterMethodsList = new ArrayList<>(); + private final List fieldSetterMethodsList = new ArrayList<>(); /** * Original javadoc description extracted from setter or constructor parameter. This is used when @@ -151,10 +151,9 @@ public void setFieldType(TypeName fieldType) { /** * Getting list of methods to modify that field. * - * @return list of methods with type {@code - * org.javahelpers.simple.builders.internal.dtos.MethodDto} + * @return list of methods with type {@code BuilderMethodDto} */ - public List getMethods() { + public List getMethods() { return fieldSetterMethodsList; } @@ -163,7 +162,7 @@ public List getMethods() { * * @param methodDto method definition */ - public void addMethod(MethodDto methodDto) { + public void addMethod(BuilderMethodDto methodDto) { this.fieldSetterMethodsList.add(methodDto); } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/GenerationTargetClassDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/GenerationTargetClassDto.java index 685cde3c..a61fc56c 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/GenerationTargetClassDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/GenerationTargetClassDto.java @@ -74,29 +74,24 @@ public class GenerationTargetClassDto { /** Imports (both regular and static imports). */ private final Set imports = new LinkedHashSet<>(); - /** - * Nested types (interfaces or classes) to be generated inside the builder, such as the "With" - * interface. - */ + /** Nested types (interfaces or classes) to be generated inside the generated class. */ private final List nestedTypes = new LinkedList<>(); /** - * Class-level annotations to be added to the generated builder class. These are added by - * BuilderEnhancers and include annotations like @Generated, @BuilderImplementation, etc. + * Class-level annotations to be added to the generated class. * *

    Uses a Set to ensure annotation uniqueness and prevent duplicates. */ private final Set classAnnotations = new LinkedHashSet<>(); /** - * Interfaces to be implemented by the generated builder class. These are added by - * BuilderEnhancers and include interfaces like IBuilderBase. + * Interfaces to be implemented by the generated class. * *

    Uses a Set to ensure interface uniqueness and prevent duplicates. */ private final Set interfaces = new LinkedHashSet<>(); - /** Class-level JavaDoc for the generated builder class. */ + /** Class-level JavaDoc for the generated class. */ private JavadocDto classJavadoc; public TypeName getTypeName() { @@ -222,7 +217,8 @@ public List getGenerics() { } /** - * Returns the list of nested types (interfaces or classes) to be generated inside the builder. + * Returns the list of nested types (interfaces or classes) to be generated inside the generated + * class. * * @return the list of nested types */ @@ -231,7 +227,7 @@ public List getNestedTypes() { } /** - * Adds a nested type definition to the builder. + * Adds a nested type definition to the generated class. * * @param nestedType the nested type to add */ @@ -249,7 +245,7 @@ public Set getClassAnnotations() { } /** - * Adds a class-level annotation to be generated in the builder. + * Adds a class-level annotation to be generated in the generated class. * * @param annotation the class annotation to add */ @@ -258,7 +254,7 @@ public void addClassAnnotation(AnnotationDto annotation) { } /** - * Returns the set of interfaces to be implemented by the builder. + * Returns the set of interfaces to be implemented by the generated class. * * @return the set of interfaces (unique, no duplicates) */ @@ -267,7 +263,7 @@ public Set getInterfaces() { } /** - * Adds an interface to be implemented by the builder. + * Adds an interface to be implemented by the generated class. * * @param interfaceType the interface to add */ @@ -276,7 +272,7 @@ public void addInterface(InterfaceName interfaceType) { } /** - * Returns the class-level JavaDoc for the generated builder. + * Returns the class-level JavaDoc for the generated class. * * @return the class JavaDoc, or null if not set */ @@ -285,7 +281,7 @@ public JavadocDto getClassJavadoc() { } /** - * Sets the class-level JavaDoc for the generated builder. + * Sets the class-level JavaDoc for the generated class. * * @param classJavadoc the class JavaDoc to set */ diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/javadoc/JavadocDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/javadoc/JavadocDto.java index f841158e..f4213415 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/javadoc/JavadocDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/javadoc/JavadocDto.java @@ -241,6 +241,23 @@ public JavadocDto addThrows(String exceptionName, String descriptionFormat, Obje return this; } + /** + * Appends additional text to the existing description. + * + *

    If the current description is blank, the additional text becomes the description. Otherwise, + * the additional text is appended with a space separator. + * + * @param additionalText the text to append to the description + * @return this JavadocDto for fluent chaining + */ + public JavadocDto appendDescription(String additionalText) { + if (StringUtils.isNotBlank(additionalText)) { + this.description = + StringUtils.isBlank(description) ? additionalText : description + " " + additionalText; + } + return this; + } + /** * Returns whether this Javadoc has any content (description, tags, or code blocks). * diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/BuilderMethodDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/BuilderMethodDto.java new file mode 100644 index 00000000..3eab8456 --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/BuilderMethodDto.java @@ -0,0 +1,582 @@ +/* + * MIT License + * + * Copyright (c) 2026 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.model.method; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import org.apache.commons.lang3.StringUtils; +import org.javahelpers.simple.builders.core.enums.AccessModifier; +import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto; +import org.javahelpers.simple.builders.processor.model.javadoc.JavadocCodeBlockDto; +import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; +import org.javahelpers.simple.builders.processor.model.type.GenericParameterDto; +import org.javahelpers.simple.builders.processor.model.type.TypeName; + +/** + * BuilderMethodDto containing all information for generating a method in the builder class, + * including field-origin metadata for pre-conflict-resolution logging and javadoc enrichment. + * + *

    This is the generation-side DTO. It is produced by generators and enhancers, and mapped to + * {@link MethodDto} (the rendering DTO) by {@link + * org.javahelpers.simple.builders.processor.model.core.BuilderToGenerationTypeMapper} before being + * added to {@code GenerationTargetClassDto}. + */ +public class BuilderMethodDto { + // Priority constants for method conflict resolution (higher values win) + public static final int PRIORITY_HIGHEST = 100; // Direct setters, with() methods + public static final int PRIORITY_HIGH = 80; // Supplier, transform methods + public static final int PRIORITY_MEDIUM = 70; // Consumer, builder consumers + public static final int PRIORITY_LOW = 60; // Specialized consumers + + /** Access modifier for method. */ + private Optional modifier = Optional.empty(); + + /** Whether the method is static. */ + private boolean isStatic = false; + + /** Priority for method conflict resolution. Higher wins. */ + private int priority = 0; + + /** Ordering for method generation. Lower values appear first in generated class. */ + private int ordering = 1000; // Default high value for field-generated methods + + /** Name of method. */ + private String methodName; + + /** Return type of method. */ + private TypeName returnType; + + /** Javadoc comment for the method. */ + private JavadocDto javadoc; + + /** List of annotations on this method. */ + private final List annotations = new ArrayList<>(); + + /** List of parameters of Method. */ + private final LinkedList parameters = new LinkedList<>(); + + /** List of generic type parameters for the method (e.g., ). */ + private final List genericParameters = new ArrayList<>(); + + /** Definition of inner implementation for method. */ + private final MethodCodeDto methodCodeDto = new MethodCodeDto(); + + /** + * Fluent-chain fragment describing how this method is invoked in Javadoc examples (e.g., {@code + * .title("example value")}). When present, downstream enhancers use it to synthesise the + * method-level example block (as {@code builder;}) and to aggregate the class-level + * kitchen-sink chain. A {@code null} value means the method should not appear in either example. + */ + private String exampleChainFragment; + + /** Name of the source field this method was generated for. {@code null} for enhancer methods. */ + private String sourceFieldName; + + /** Whether this method was generated for a constructor field (vs a setter field). */ + private boolean constructorField; + + /** Default constructor. */ + public BuilderMethodDto() { + // Default constructor + } + + /** + * Constructor with method name and return type. + * + * @param methodName the name of the method + * @param returnType the return type of the method + */ + public BuilderMethodDto(String methodName, TypeName returnType) { + this.methodName = methodName; + this.returnType = returnType; + } + + /** + * Sets the priority for this method. Higher values win when signatures clash. Priority levels: + * + *

      + *
    • {@link #PRIORITY_HIGHEST} (100): Direct setters, with() methods + *
    • {@link #PRIORITY_HIGH} (80): Supplier methods, transform methods (e.g., format, toArray) + *
    • {@link #PRIORITY_MEDIUM} (70): Consumer methods, builder consumers + *
    • {@link #PRIORITY_LOW} (60): Specialized consumers (e.g., StringBuilder) + *
    • 0: Default (no priority set) + *
    + * + * @param priority the priority value (higher values take precedence in conflicts) + */ + public void setPriority(int priority) { + this.priority = priority; + } + + /** + * Returns the priority of this method for conflict resolution. + * + * @return the priority value + */ + public int getPriority() { + return priority; + } + + /** + * Sets the ordering for this method. + * + *

    Lower values appear first in the generated class. Methods with the same ordering and name + * are sorted using the following enhanced rules: + * + *

      + *
    1. Methods with fewer parameters come first + *
    2. Non-generic methods come before generic methods + *
    3. Full method signature (name(paramType1,paramType2,...)) used for final ordering + *
    + * + * @param ordering the ordering value (lower values appear first) + */ + public void setOrdering(int ordering) { + this.ordering = ordering; + } + + /** + * Returns the ordering of this method. + * + * @return the ordering value + */ + public int getOrdering() { + return ordering; + } + + /** + * Setting the inner implementation of a method. Supports placeholders which has to be set by + * addArgument. + * + * @param codeFormat Codeformat with placeholders + */ + public void setCode(String codeFormat) { + methodCodeDto.setCodeFormat(codeFormat); + } + + /** + * Adding the value for a text - placeholder. + * + * @param name name of placeholder + * @param value dynamic value of placeholder + */ + public void addArgument(String name, String value) { + methodCodeDto.addArgument(name, value); + } + + /** + * Adding the value for a type - placeholder. + * + * @param name name of placeholder + * @param value dynamic value of placeholder + */ + public void addArgument(String name, TypeName value) { + methodCodeDto.addArgument(name, value); + } + + /** + * Getter for inner implementation of method. + * + * @return {@code MethodCodeDto} containing definition of implementation + */ + public MethodCodeDto getMethodCodeDto() { + return methodCodeDto; + } + + /** + * Returns the fluent-chain fragment for Javadoc examples (e.g. {@code .title("example value")}) + * or {@code null} if this method should not participate in example generation. + * + * @return the fragment or {@code null} + */ + public String getExampleChainFragment() { + return exampleChainFragment; + } + + /** + * Stores the fluent-chain fragment describing how this method is invoked in examples. + * + *

    The fragment contains just the method invocation, e.g. {@code title("example value")}. + * Downstream enhancers synthesise the method-level example block (as {@code builder.;}) + * and the class-level kitchen-sink chain from it. + * + *

    Automatically adds a method-level example to the javadoc if javadoc exists and has no + * existing examples (to avoid overriding manually set examples). + * + * @param exampleChainFragment the fragment or {@code null} to clear + */ + public void setExampleChainFragment(String exampleChainFragment) { + this.exampleChainFragment = exampleChainFragment; + // Automatically add method-level example to javadoc if javadoc exists and has no examples + if (exampleChainFragment != null && javadoc != null && javadoc.getCodeBlocks().isEmpty()) { + JavadocCodeBlockDto methodExample = new JavadocCodeBlockDto(); + methodExample.setCodeFormat("builder.%s;".formatted(exampleChainFragment)); + javadoc.addExample(methodExample); + } + } + + /** + * Checks if the method has a code block. + * + * @return true if the method has a code block, false otherwise + */ + public boolean hasCode() { + return methodCodeDto.hasCode(); + } + + /** + * Getting name of method. + * + * @return name with type {@code java.lang.String} + */ + public String getMethodName() { + return methodName; + } + + /** + * Setting name of method. + * + * @param methodName name with type {@code java.lang.String} + */ + public void setMethodName(String methodName) { + this.methodName = methodName; + } + + /** + * Adding a further parameter of method. + * + * @param paramDto parameter to be added of type {@code + * org.javahelpers.simple.builders.internal.dtos.MethodParameterDto} + */ + public void addParameter(MethodParameterDto paramDto) { + this.parameters.add(paramDto); + } + + /** + * Getting a list of parameters of method. + * + * @return List of parameters of type {@code + * org.javahelpers.simple.builders.internal.dtos.MethodParameterDto} + */ + public List getParameters() { + return parameters; + } + + /** + * Adds a generic type parameter to this method. + * + * @param genericParameter the generic parameter to add + */ + public void addGenericParameter(GenericParameterDto genericParameter) { + this.genericParameters.add(genericParameter); + } + + /** + * Getting a list of generic type parameters of method. + * + * @return List of generic parameters of type {@code GenericParameterDto} + */ + public List getGenericParameters() { + return genericParameters; + } + + /** + * Getting the access modifier for method. Optional for usage in stream-notation. + * + * @return modifier {@code java.util.Optional} access modifier of type {@code AccessModifier} + */ + public Optional getModifier() { + return modifier; + } + + /** + * Sets the access modifier for method. + * + * @param modifier access modifier of type {@code AccessModifier} + */ + public void setModifier(AccessModifier modifier) { + this.modifier = Optional.ofNullable(modifier); + } + + /** + * Returns whether this method is static. + * + * @return true if the method is static, false otherwise + */ + public boolean isStatic() { + return isStatic; + } + + /** + * Sets whether this method is static. + * + * @param isStatic true if the method should be static, false otherwise + */ + public void setStatic(boolean isStatic) { + this.isStatic = isStatic; + } + + /** + * Gets the return type of the method. + * + * @return the return type as TypeName + */ + public TypeName getReturnType() { + return returnType; + } + + /** + * Sets the return type of the method. + * + * @param returnType the return type as TypeName + */ + public void setReturnType(TypeName returnType) { + this.returnType = returnType; + } + + /** + * Returns a unique signature key for the method based on name and parameter types. Used for + * conflict resolution. The signature matches Java's method signature rules (name + parameter + * types, ignoring generics due to type erasure). + * + * @return the signature key (e.g., "fieldName(java.lang.String,java.util.List)") + */ + public String getSignatureKey() { + StringBuilder sb = new StringBuilder(); + sb.append(methodName).append('('); + for (int i = 0; i < parameters.size(); i++) { + if (i > 0) sb.append(','); + TypeName tn = parameters.get(i).getParameterType(); + // Handle null package names + if (StringUtils.isNoneBlank(tn.getPackageName())) { + sb.append(tn.getPackageName()).append('.'); + } + sb.append(tn.getClassName()); + } + sb.append(')'); + return sb.toString(); + } + + public JavadocDto getJavadoc() { + return javadoc; + } + + /** + * Sets the Javadoc comment for the method. + * + * @param javadoc the Javadoc comment + */ + public void setJavadoc(JavadocDto javadoc) { + this.javadoc = javadoc; + } + + /** + * Returns the list of annotations on this method. + * + * @return list of annotations + */ + public List getAnnotations() { + return annotations; + } + + /** + * Adds an annotation to this method. + * + * @param annotation the annotation to add + */ + public void addAnnotation(AnnotationDto annotation) { + this.annotations.add(annotation); + } + + /** + * Returns the name of the source field this method was generated for. + * + * @return the source field name, or {@code null} for enhancer-generated methods + */ + public String getSourceFieldName() { + return sourceFieldName; + } + + /** + * Sets the name of the source field this method was generated for. + * + * @param sourceFieldName the source field name, or {@code null} for enhancer-generated methods + */ + public void setSourceFieldName(String sourceFieldName) { + this.sourceFieldName = sourceFieldName; + } + + /** + * Returns whether this method was generated for a constructor field. + * + * @return true if this method was generated for a constructor field, false otherwise + */ + public boolean isConstructorField() { + return constructorField; + } + + /** + * Sets whether this method was generated for a constructor field. + * + * @param constructorField true if this method was generated for a constructor field + */ + public void setConstructorField(boolean constructorField) { + this.constructorField = constructorField; + } + + /** + * Comparator for sorting BuilderMethodDto instances with sophisticated ordering rules. + * + *

    Sorting order for methods with same priority and name: + * + *

      + *
    1. Methods with fewer parameters come first + *
    2. Non-generic methods come before generic methods + *
    3. Full method signature (name(paramType1,paramType2,...)) used for final ordering + *
    + */ + public static class BuilderMethodComparator implements java.util.Comparator { + + @Override + public int compare(BuilderMethodDto m1, BuilderMethodDto m2) { + // Primary sort: ordering value + int orderingCompare = Integer.compare(m1.getOrdering(), m2.getOrdering()); + if (orderingCompare != 0) { + return orderingCompare; + } + + // Secondary sort: method name + int nameCompare = m1.getMethodName().compareTo(m2.getMethodName()); + if (nameCompare != 0) { + return nameCompare; + } + + // Tertiary sort: parameter count (fewer parameters first) + int paramCountCompare = Integer.compare(m1.getParameters().size(), m2.getParameters().size()); + if (paramCountCompare != 0) { + return paramCountCompare; + } + + // Quaternary sort: generic vs non-generic (non-generic first) + boolean m1Generic = hasGenericParameters(m1); + boolean m2Generic = hasGenericParameters(m2); + if (m1Generic != m2Generic) { + return m1Generic ? 1 : -1; // non-generic comes first + } + + // Final sort: full method signature + String signature1 = createMethodSignature(m1); + String signature2 = createMethodSignature(m2); + return signature1.compareTo(signature2); + } + + /** + * Creates a qualified name string for a TypeName. + * + * @param typeName the type name + * @return qualified name using the type's own formatting logic + */ + private String getQualifiedName(TypeName typeName) { + return typeName.getFullQualifiedName(); + } + + /** + * Creates a method signature string for sorting purposes. + * + *

    The signature includes method name and parameter types in the format: + * methodName(paramType1,paramType2,...) + * + * @param method the method to create signature for + * @return signature string for comparison + */ + private String createMethodSignature(BuilderMethodDto method) { + StringBuilder signature = new StringBuilder(method.getMethodName()); + signature.append("("); + + java.util.List paramTypes = + method.getParameters().stream() + .map(param -> getQualifiedName(param.getParameterType())) + .toList(); + + signature.append(String.join(",", paramTypes)); + signature.append(")"); + + return signature.toString(); + } + + /** + * Checks if a method has generic parameters. + * + * @param method the method to check + * @return true if any parameter is generic (contains type parameters) + */ + private boolean hasGenericParameters(BuilderMethodDto method) { + return method.getParameters().stream() + .anyMatch(param -> getQualifiedName(param.getParameterType()).contains("<")); + } + } + + /** + * Returns a string representation of this method in Java method signature format. + * + *

    Examples: + * + *

      + *
    • {@code public PersonBuilder name(String)} + *
    • {@code public PersonBuilder age(int)} + *
    • {@code public PersonBuilder tags(Consumer>)} + *
    + * + * @return method signature as a string + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + + // Add modifier if present + modifier.ifPresent(m -> sb.append(m.toString().toLowerCase()).append(" ")); + + // Add static if applicable + if (isStatic) { + sb.append("static "); + } + + // Add return type (void if not specified) + String returnTypeName = returnType != null ? returnType.getClassName() : "void"; + sb.append(returnTypeName).append(" "); + + // Add method name and parameters + String parameterList = + parameters.stream() + .map(param -> param.getParameterType().getClassName()) + .collect(java.util.stream.Collectors.joining(", ")); + + sb.append(methodName).append("(").append(parameterList).append(")"); + + return sb.toString(); + } +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodDto.java index 509804ae..104b3106 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodDto.java @@ -35,13 +35,19 @@ import org.javahelpers.simple.builders.processor.model.type.GenericParameterDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; -/** MethodDto containing all information to generate a method in builder. */ +/** + * Rendering-side DTO containing all information to generate a method in a generated class. + * + *

    This DTO is consumed by the code generator. It is generation-specific in the sense that it + * holds only rendering-relevant data — no field-origin metadata or other generation-phase-only + * information. + */ public class MethodDto { // Priority constants for method conflict resolution (higher values win) - public static final int PRIORITY_HIGHEST = 100; // Direct setters, with() methods - public static final int PRIORITY_HIGH = 80; // Supplier, transform methods - public static final int PRIORITY_MEDIUM = 70; // Consumer, builder consumers - public static final int PRIORITY_LOW = 60; // Specialized consumers + public static final int PRIORITY_HIGHEST = 100; + public static final int PRIORITY_HIGH = 80; + public static final int PRIORITY_MEDIUM = 70; + public static final int PRIORITY_LOW = 60; /** Access modifier for method. */ private Optional modifier = Optional.empty(); @@ -53,7 +59,7 @@ public class MethodDto { private int priority = 0; /** Ordering for method generation. Lower values appear first in generated class. */ - private int ordering = 1000; // Default high value for field-generated methods + private int ordering = 1000; /** Name of method. */ private String methodName; @@ -76,14 +82,6 @@ public class MethodDto { /** Definition of inner implementation for method. */ private final MethodCodeDto methodCodeDto = new MethodCodeDto(); - /** - * Fluent-chain fragment describing how this method is invoked in Javadoc examples (e.g., {@code - * .title("example value")}). When present, downstream enhancers use it to synthesise the - * method-level example block (as {@code builder;}) and to aggregate the class-level - * kitchen-sink chain. A {@code null} value means the method should not appear in either example. - */ - private String exampleChainFragment; - /** Default constructor. */ public MethodDto() { // Default constructor @@ -101,13 +99,15 @@ public MethodDto(String methodName, TypeName returnType) { } /** - * Sets the priority for this method. Higher values win when signatures clash. Priority levels: + * Sets the priority for this method. Higher values win when signatures clash. + * + *

    Priority levels: * *

      - *
    • {@link #PRIORITY_HIGHEST} (100): Direct setters, with() methods - *
    • {@link #PRIORITY_HIGH} (80): Supplier methods, transform methods (e.g., format, toArray) - *
    • {@link #PRIORITY_MEDIUM} (70): Consumer methods, builder consumers - *
    • {@link #PRIORITY_LOW} (60): Specialized consumers (e.g., StringBuilder) + *
    • {@link #PRIORITY_HIGHEST} (100) + *
    • {@link #PRIORITY_HIGH} (80) + *
    • {@link #PRIORITY_MEDIUM} (70) + *
    • {@link #PRIORITY_LOW} (60) *
    • 0: Default (no priority set) *
    * @@ -193,42 +193,9 @@ public MethodCodeDto getMethodCodeDto() { } /** - * Returns the fluent-chain fragment for Javadoc examples (e.g. {@code .title("example value")}) - * or {@code null} if this method should not participate in example generation. - * - * @return the fragment or {@code null} - */ - public String getExampleChainFragment() { - return exampleChainFragment; - } - - /** - * Stores the fluent-chain fragment describing how this method is invoked in examples. - * - *

    The fragment contains just the method invocation, e.g. {@code title("example value")}. - * Downstream enhancers synthesise the method-level example block (as {@code builder.;}) - * and the class-level kitchen-sink chain from it. - * - *

    Automatically adds a method-level example to the javadoc if javadoc exists and has no - * existing examples (to avoid overriding manually set examples). - * - * @param exampleChainFragment the fragment or {@code null} to clear - */ - public void setExampleChainFragment(String exampleChainFragment) { - this.exampleChainFragment = exampleChainFragment; - // Automatically add method-level example to javadoc if javadoc exists and has no examples - if (exampleChainFragment != null && javadoc != null && javadoc.getCodeBlocks().isEmpty()) { - org.javahelpers.simple.builders.processor.model.javadoc.JavadocCodeBlockDto methodExample = - new org.javahelpers.simple.builders.processor.model.javadoc.JavadocCodeBlockDto(); - methodExample.setCodeFormat("builder.%s;".formatted(exampleChainFragment)); - javadoc.addExample(methodExample); - } - } - - /** - * Checks if the constructor has a code block. + * Checks if the method has a code block. * - * @return true if the constructor has a code block, false otherwise + * @return true if the method has a code block, false otherwise */ public boolean hasCode() { return methodCodeDto.hasCode(); @@ -243,15 +210,6 @@ public String getMethodName() { return methodName; } - /** - * Helper function to generate a name for setter method. Does not work on non-field methods. - * - * @return returning name of field-setter method - */ - public String createFieldSetterMethodName() { - return "set" + StringUtils.capitalize(this.getMethodName()); - } - /** * Setting name of method. * @@ -505,9 +463,9 @@ private boolean hasGenericParameters(MethodDto method) { *

    Examples: * *

      - *
    • {@code public PersonBuilder name(String)} - *
    • {@code public PersonBuilder age(int)} - *
    • {@code public PersonBuilder tags(Consumer>)} + *
    • {@code public GeneratedClass name(String)} + *
    • {@code public GeneratedClass age(int)} + *
    • {@code public GeneratedClass tags(Consumer>)} *
    * * @return method signature as a string diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/BuilderNestedTypeDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/BuilderNestedTypeDto.java new file mode 100644 index 00000000..e8493f0a --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/BuilderNestedTypeDto.java @@ -0,0 +1,113 @@ +/* + * MIT License + * + * Copyright (c) 2026 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.model.type; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import org.javahelpers.simple.builders.core.enums.AccessModifier; +import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto; +import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; +import org.javahelpers.simple.builders.processor.model.method.BuilderMethodDto; + +/** + * Generation-side DTO for a nested type (interface or class) to be generated inside the builder. + * + *

    This is the generation-phase counterpart of {@link NestedTypeDto}. It holds {@link + * BuilderMethodDto} instances (generation-side method DTOs) and is produced by enhancers. During + * finalization in {@code BuilderDefinitionCreator}, it is mapped to {@link NestedTypeDto} (the + * rendering DTO) via {@link + * org.javahelpers.simple.builders.processor.model.core.BuilderToGenerationTypeMapper#toNestedTypeDto}. + * + *

    For example, the "With" interface that allows DTOs to implement fluent modification methods. + */ +public class BuilderNestedTypeDto { + + /** The simple name of the nested type (e.g., "With"). */ + private String typeName; + + /** The kind of nested type (INTERFACE or CLASS). */ + private NestedTypeDto.NestedTypeKind kind; + + /** Visibility of this nested type. */ + private AccessModifier visibility = AccessModifier.PUBLIC; + + /** Methods to be generated in this nested type (generation-side). */ + private final List methods = new LinkedList<>(); + + /** Javadoc comment for this nested type. */ + private JavadocDto javadoc; + + /** Type-level annotations for this nested type. */ + private final List annotations = new ArrayList<>(); + + public String getTypeName() { + return typeName; + } + + public void setTypeName(String typeName) { + this.typeName = typeName; + } + + public NestedTypeDto.NestedTypeKind getKind() { + return kind; + } + + public void setKind(NestedTypeDto.NestedTypeKind kind) { + this.kind = kind; + } + + public AccessModifier getVisibility() { + return visibility; + } + + public void setVisibility(AccessModifier visibility) { + this.visibility = visibility; + } + + public List getMethods() { + return methods; + } + + public void addMethod(BuilderMethodDto method) { + this.methods.add(method); + } + + public JavadocDto getJavadoc() { + return javadoc; + } + + public void setJavadoc(JavadocDto javadoc) { + this.javadoc = javadoc; + } + + public List getAnnotations() { + return annotations; + } + + public void addAnnotation(AnnotationDto annotation) { + this.annotations.add(annotation); + } +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/NestedTypeDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/NestedTypeDto.java index 1f6f6b50..38809cf1 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/NestedTypeDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/NestedTypeDto.java @@ -33,7 +33,11 @@ import org.javahelpers.simple.builders.processor.model.method.MethodDto; /** - * Represents a nested type (interface or class) to be generated inside the builder. + * Rendering-side DTO for a nested type (interface or class) to be generated inside a generated + * class. + * + *

    It holds {@link MethodDto} instances (rendering-side method DTOs) and is consumed by the code + * generator. * *

    For example, the "With" interface that allows DTOs to implement fluent modification methods. */ diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java index 7f1b6dc9..1af6f864 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java @@ -52,7 +52,7 @@ import org.javahelpers.simple.builders.processor.model.core.ClassFieldDto; import org.javahelpers.simple.builders.processor.model.core.FieldDto; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; -import org.javahelpers.simple.builders.processor.model.method.MethodDto; +import org.javahelpers.simple.builders.processor.model.method.BuilderMethodDto; import org.javahelpers.simple.builders.processor.model.method.MethodParameterDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.model.type.TypeNameGeneric; @@ -115,8 +115,9 @@ public static BuilderDefinitionDto extractFromElement( * *

      *
    • Converts FieldDto instances to ClassFieldDto instances - *
    • Collects all methods from fields and core methods - *
    • Sets source descriptions on methods for conflict resolution logging + *
    • Sets origin info (sourceFieldName, constructorField) on each BuilderMethodDto + *
    • Maps all BuilderMethodDto to MethodDto via BuilderToGenerationTypeMapper + *
    • Collects all mapped methods from fields and class-level enhancer methods *
    • Sets class access modifier *
    • Sets static imports for TrackedValue *
    @@ -134,15 +135,17 @@ private static void finalizeDefinition( builderDto.addClassField(classField); } - // 2. Collect methods from fields + // 2. Set origin info on field-level BuilderMethodDto instances for (FieldDto field : builderDto.getConstructorFieldsForBuilder()) { - for (MethodDto method : field.getMethods()) { - builderDto.addMethod(method); + for (BuilderMethodDto method : field.getMethods()) { + method.setSourceFieldName(field.getOriginalFieldName()); + method.setConstructorField(true); } } for (FieldDto field : builderDto.getSetterFieldsForBuilder()) { - for (MethodDto method : field.getMethods()) { - builderDto.addMethod(method); + for (BuilderMethodDto method : field.getMethods()) { + method.setSourceFieldName(field.getOriginalFieldName()); + method.setConstructorField(false); } } @@ -155,7 +158,7 @@ private static void finalizeDefinition( builderDto.addStaticImport(TrackedValue.class, "unsetValue"); context.debugEndOperation( - "Finalized: %d class fields, %d methods, %d constructors", + "Finalized: %d class fields, %d builder-level methods, %d constructors", builderDto.getClassFields().size(), builderDto.getMethods().size(), builderDto.getConstructors().size()); @@ -579,7 +582,7 @@ private static Optional createFieldDto( // Builder and constructor information is now set when TypeName is created in JavaLangMapper // Use GeneratorRegistry to generate all methods for this field - List generatedMethods = + List generatedMethods = context.getGeneratorRegistry().generateAllMethods(field, dtoType, builderType); generatedMethods.forEach(field::addMethod); diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/CustomizingDocumentationTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/CustomizingDocumentationTest.java index 91781cbb..063461d0 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/CustomizingDocumentationTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/CustomizingDocumentationTest.java @@ -96,7 +96,7 @@ void testGeneratorInterfaceStructure() { import org.javahelpers.simple.builders.processor.generators.MethodGenerator; import org.javahelpers.simple.builders.processor.model.core.FieldDto; - import org.javahelpers.simple.builders.processor.model.method.MethodDto; + import org.javahelpers.simple.builders.processor.model.method.BuilderMethodDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.processing.ProcessingContext; import java.util.List; @@ -109,7 +109,7 @@ public boolean appliesTo(FieldDto field, TypeName dtoType, ProcessingContext con } @Override - public List generateMethods(FieldDto field, TypeName builderType, ProcessingContext context) { + public List generateMethods(FieldDto field, TypeName builderType, ProcessingContext context) { return List.of(); } @@ -181,7 +181,7 @@ void testCustomGeneratorMethodCreation() { import org.javahelpers.simple.builders.processor.generators.MethodGenerator; import org.javahelpers.simple.builders.processor.model.core.FieldDto; - import org.javahelpers.simple.builders.processor.model.method.MethodDto; + import org.javahelpers.simple.builders.processor.model.method.BuilderMethodDto; import org.javahelpers.simple.builders.processor.model.method.MethodParameterDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.processing.ProcessingContext; @@ -194,11 +194,11 @@ public boolean appliesTo(FieldDto field, TypeName dtoType, ProcessingContext con } @Override - public List generateMethods(FieldDto field, TypeName builderType, ProcessingContext context) { + public List generateMethods(FieldDto field, TypeName builderType, ProcessingContext context) { String fieldName = field.getFieldNameInBuilder(); String methodName = "custom" + capitalize(fieldName); - MethodDto method = new MethodDto(methodName, builderType); + BuilderMethodDto method = new BuilderMethodDto(methodName, builderType); MethodParameterDto parameter = new MethodParameterDto(); parameter.setParameterName("value"); @@ -242,7 +242,7 @@ void testGeneratorPriorityPattern() { import org.javahelpers.simple.builders.processor.generators.MethodGenerator; import org.javahelpers.simple.builders.processor.model.core.FieldDto; - import org.javahelpers.simple.builders.processor.model.method.MethodDto; + import org.javahelpers.simple.builders.processor.model.method.BuilderMethodDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.processing.ProcessingContext; import java.util.List; @@ -254,7 +254,7 @@ public boolean appliesTo(FieldDto field, TypeName dtoType, ProcessingContext con } @Override - public List generateMethods(FieldDto field, TypeName builderType, ProcessingContext context) { + public List generateMethods(FieldDto field, TypeName builderType, ProcessingContext context) { return List.of(); } @@ -285,7 +285,7 @@ void testErrorHandlingPattern() { import org.javahelpers.simple.builders.processor.generators.MethodGenerator; import org.javahelpers.simple.builders.processor.model.core.FieldDto; - import org.javahelpers.simple.builders.processor.model.method.MethodDto; + import org.javahelpers.simple.builders.processor.model.method.BuilderMethodDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.processing.ProcessingContext; import java.util.List; @@ -297,7 +297,7 @@ public boolean appliesTo(FieldDto field, TypeName dtoType, ProcessingContext con } @Override - public List generateMethods(FieldDto field, TypeName builderType, ProcessingContext context) { + public List generateMethods(FieldDto field, TypeName builderType, ProcessingContext context) { try { // Generation logic return List.of(); @@ -334,7 +334,7 @@ void testConditionalApplicationPattern() { import org.javahelpers.simple.builders.processor.generators.MethodGenerator; import org.javahelpers.simple.builders.processor.model.core.FieldDto; - import org.javahelpers.simple.builders.processor.model.method.MethodDto; + import org.javahelpers.simple.builders.processor.model.method.BuilderMethodDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.processing.ProcessingContext; import java.util.List; @@ -352,7 +352,7 @@ public boolean appliesTo(FieldDto field, TypeName dtoType, ProcessingContext con } @Override - public List generateMethods(FieldDto field, TypeName builderType, ProcessingContext context) { + public List generateMethods(FieldDto field, TypeName builderType, ProcessingContext context) { return List.of(); } @@ -388,7 +388,7 @@ void testCompleteCustomValidationGenerator() { import org.javahelpers.simple.builders.processor.generators.MethodGenerator; import org.javahelpers.simple.builders.processor.model.core.FieldDto; - import org.javahelpers.simple.builders.processor.model.method.MethodDto; + import org.javahelpers.simple.builders.processor.model.method.BuilderMethodDto; import org.javahelpers.simple.builders.processor.model.method.MethodParameterDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.processing.ProcessingContext; @@ -407,11 +407,11 @@ public boolean appliesTo(FieldDto field, TypeName dtoType, ProcessingContext con } @Override - public List generateMethods(FieldDto field, TypeName builderType, ProcessingContext context) { + public List generateMethods(FieldDto field, TypeName builderType, ProcessingContext context) { String fieldName = field.getFieldNameInBuilder(); String methodName = "validated" + capitalize(fieldName); - MethodDto method = new MethodDto(methodName, builderType); + BuilderMethodDto method = new BuilderMethodDto(methodName, builderType); String parameterName = fieldName; MethodParameterDto parameter = new MethodParameterDto(); @@ -464,7 +464,7 @@ void testCompleteCustomValidationEnhancer() { import org.javahelpers.simple.builders.processor.generators.BuilderEnhancer; import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto; import org.javahelpers.simple.builders.processor.model.core.BuilderDefinitionDto; - import org.javahelpers.simple.builders.processor.model.method.MethodDto; + import org.javahelpers.simple.builders.processor.model.method.BuilderMethodDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.processing.ProcessingContext; @@ -485,7 +485,7 @@ public boolean appliesTo(BuilderDefinitionDto builderDto, TypeName dtoType, Proc @Override public void enhanceBuilder(BuilderDefinitionDto builderDto, ProcessingContext context) { // Add validation method to builder - MethodDto validateMethod = createValidateMethod(); + BuilderMethodDto validateMethod = createValidateMethod(); builderDto.addMethod(validateMethod); // Add @Valid annotation if available @@ -504,9 +504,9 @@ public int getPriority() { return 500; } - private MethodDto createValidateMethod() { + private BuilderMethodDto createValidateMethod() { TypeName returnType = new TypeName("java.lang", "Void"); - MethodDto method = new MethodDto("validate", returnType); + BuilderMethodDto method = new BuilderMethodDto("validate", returnType); method.setCode("// Validation logic here"); return method; } @@ -573,7 +573,7 @@ void testDateParserGeneratorExample() { import org.javahelpers.simple.builders.processor.generators.MethodGenerator; import org.javahelpers.simple.builders.processor.model.core.FieldDto; - import org.javahelpers.simple.builders.processor.model.method.MethodDto; + import org.javahelpers.simple.builders.processor.model.method.BuilderMethodDto; import org.javahelpers.simple.builders.processor.model.method.MethodParameterDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.processing.ProcessingContext; @@ -588,11 +588,11 @@ public boolean appliesTo(FieldDto field, TypeName dtoType, ProcessingContext con } @Override - public List generateMethods(FieldDto field, TypeName builderType, ProcessingContext context) { + public List generateMethods(FieldDto field, TypeName builderType, ProcessingContext context) { String fieldName = field.getFieldNameInBuilder(); String methodName = fieldName + "FromString"; - MethodDto method = new MethodDto(methodName, builderType); + BuilderMethodDto method = new BuilderMethodDto(methodName, builderType); String parameterName = fieldName + "String"; MethodParameterDto parameter = new MethodParameterDto(); @@ -637,7 +637,7 @@ void testBuilderFactoryEnhancerExample() { import org.javahelpers.simple.builders.processor.generators.BuilderEnhancer; import org.javahelpers.simple.builders.processor.model.core.BuilderDefinitionDto; - import org.javahelpers.simple.builders.processor.model.method.MethodDto; + import org.javahelpers.simple.builders.processor.model.method.BuilderMethodDto; import org.javahelpers.simple.builders.processor.model.method.MethodParameterDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.processing.ProcessingContext; @@ -654,7 +654,7 @@ public void enhanceBuilder(BuilderDefinitionDto builderDto, ProcessingContext co TypeName builderType = builderDto.getBuilderTypeName(); // Add static factory method (conceptual - actual API may differ) - MethodDto factoryMethod = new MethodDto("from", builderType); + BuilderMethodDto factoryMethod = new BuilderMethodDto("from", builderType); factoryMethod.setStatic(true); MethodParameterDto parameter = new MethodParameterDto(); From e4801eb98b07ed87edf584798d4b32d941337fd1 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 2 Aug 2026 20:24:10 +0200 Subject: [PATCH 10/23] Adding source documentation in javadoc --- .../builders/example/BookDtoBuilder.java | 44 ++++ .../example/JacksonIntegrationDtoBuilder.java | 21 ++ .../example/MannschaftDtoBuilder.java | 20 ++ .../builders/example/PersonDtoBuilder.java | 38 ++++ .../example/ProductRecordBuilder.java | 36 ++++ .../builders/example/SponsorDtoBuilder.java | 10 + .../core/BuilderToGenerationTypeMapper.java | 60 +++++- .../processor/model/core/FieldDto.java | 50 +++++ .../processor/model/javadoc/JavadocDto.java | 17 ++ .../model/method/BuilderMethodDto.java | 53 +++++ .../processor/model/type/TypeName.java | 13 ++ .../processor/model/type/TypeNameGeneric.java | 16 ++ .../processing/BuilderDefinitionCreator.java | 48 ++++- .../BuilderConfigurationReaderTest.java | 4 + .../processor/BuilderJavadocExampleTest.java | 8 +- .../processor/BuilderProcessorTest.java | 4 +- .../ComprehensiveFeatureIntegrationTest.java | 190 ++++++++++++++++++ .../MethodConflictResolutionTest.java | 8 +- 18 files changed, 629 insertions(+), 11 deletions(-) diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java index b37d062b..b936be21 100644 --- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java @@ -173,6 +173,8 @@ public static BookDtoBuilder create() { /** * Sets the value for author. + *

    + * Generated from setter {@link BookDto#setAuthor(String) setAuthor(String author)} * *

    Example:

    * @@ -190,6 +192,8 @@ public BookDtoBuilder author(String author) { /** * Sets the value for available. + *

    + * Generated from setter {@link BookDto#setAvailable(boolean) setAvailable(boolean available)} * *

    Example:

    * @@ -207,6 +211,8 @@ public BookDtoBuilder available(boolean available) { /** * Sets the value for category. + *

    + * Generated from setter {@link BookDto#setCategory(char) setCategory(char category)} * *

    Example:

    * @@ -224,6 +230,8 @@ public BookDtoBuilder category(char category) { /** * Sets the value for discount. + *

    + * Generated from setter {@link BookDto#setDiscount(float) setDiscount(float discount)} * *

    Example:

    * @@ -241,6 +249,8 @@ public BookDtoBuilder discount(float discount) { /** * Sets the value for edition. + *

    + * Generated from setter {@link BookDto#setEdition(short) setEdition(short edition)} * * @param edition the edition number to set * @return current instance of builder @@ -252,6 +262,8 @@ public BookDtoBuilder edition(short edition) { /** * Sets the value for exactPrice. + *

    + * Generated from setter {@link BookDto#setExactPrice(BigDecimal) setExactPrice(BigDecimal exactPrice)} * *

    Example:

    * @@ -269,6 +281,8 @@ public BookDtoBuilder exactPrice(BigDecimal exactPrice) { /** * Sets the value for genres. + *

    + * Generated from setter {@link BookDto#setGenres(Set) setGenres(Set genres)} * *

    Example:

    * @@ -286,6 +300,8 @@ public BookDtoBuilder genres(Set genres) { /** * Sets the value for isbn. + *

    + * Generated from setter {@link BookDto#setIsbn(String) setIsbn(String isbn)} * *

    Example:

    * @@ -303,6 +319,8 @@ public BookDtoBuilder isbn(String isbn) { /** * Sets the value for lastUpdated. + *

    + * Generated from setter {@link BookDto#setLastUpdated(LocalDateTime) setLastUpdated(LocalDateTime lastUpdated)} * *

    Example:

    * @@ -320,6 +338,8 @@ public BookDtoBuilder lastUpdated(LocalDateTime lastUpdated) { /** * Sets the value for metadata. + *

    + * Generated from setter {@link BookDto#setMetadata(Map) setMetadata(Map metadata)} * *

    Example:

    * @@ -337,6 +357,8 @@ public BookDtoBuilder metadata(Map metadata) { /** * Sets the value for pages. + *

    + * Generated from setter {@link BookDto#setPages(int) setPages(int pages)} * *

    Example:

    * @@ -354,6 +376,8 @@ public BookDtoBuilder pages(int pages) { /** * Sets the value for price. + *

    + * Generated from setter {@link BookDto#setPrice(double) setPrice(double price)} * *

    Example:

    * @@ -371,6 +395,8 @@ public BookDtoBuilder price(double price) { /** * Sets the value for publishDate. + *

    + * Generated from setter {@link BookDto#setPublishDate(LocalDate) setPublishDate(LocalDate publishDate)} * *

    Example:

    * @@ -388,6 +414,8 @@ public BookDtoBuilder publishDate(LocalDate publishDate) { /** * Sets the value for publisher. + *

    + * Generated from setter {@link BookDto#setPublisher(PersonDto) setPublisher(PersonDto publisher)} * *

    Example:

    * @@ -405,6 +433,8 @@ public BookDtoBuilder publisher(PersonDto publisher) { /** * Sets the value for rating. + *

    + * Generated from setter {@link BookDto#setRating(byte) setRating(byte rating)} * * @param rating the book rating to set * @return current instance of builder @@ -416,6 +446,8 @@ public BookDtoBuilder rating(byte rating) { /** * Sets the value for salesCount. + *

    + * Generated from setter {@link BookDto#setSalesCount(long) setSalesCount(long salesCount)} * *

    Example:

    * @@ -433,6 +465,8 @@ public BookDtoBuilder salesCount(long salesCount) { /** * Sets the value for subtitle. + *

    + * Generated from setter {@link BookDto#setSubtitle(Optional) setSubtitle(Optional subtitle)} * * @param subtitle an Optional containing the subtitle to set * @return current instance of builder @@ -444,6 +478,8 @@ public BookDtoBuilder subtitle(Optional subtitle) { /** * Sets the value for tags. + *

    + * Generated from setter {@link BookDto#setTags(List) setTags(List tags)} * *

    Example:

    * @@ -461,6 +497,8 @@ public BookDtoBuilder tags(List tags) { /** * Sets the value for title. + *

    + * Generated from setter {@link BookDto#setTitle(String) setTitle(String title)} * *

    Example:

    * @@ -478,6 +516,8 @@ public BookDtoBuilder title(String title) { /** * Validates that the author field is not null or empty. + *

    + * Generated from setter {@link BookDto#setAuthor(String) setAuthor(String author)} * * @return this builder instance for chaining * @throws IllegalArgumentException if author is null or empty @@ -491,6 +531,8 @@ BookDtoBuilder validateAuthor() { /** * Validates that the isbn field is not null or empty. + *

    + * Generated from setter {@link BookDto#setIsbn(String) setIsbn(String isbn)} * * @return this builder instance for chaining * @throws IllegalArgumentException if isbn is null or empty @@ -504,6 +546,8 @@ BookDtoBuilder validateIsbn() { /** * Validates that the title field is not null or empty. + *

    + * Generated from setter {@link BookDto#setTitle(String) setTitle(String title)} * * @return this builder instance for chaining * @throws IllegalArgumentException if title is null or empty diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/JacksonIntegrationDtoBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/JacksonIntegrationDtoBuilder.java index 18fda69d..199010b5 100644 --- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/JacksonIntegrationDtoBuilder.java +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/JacksonIntegrationDtoBuilder.java @@ -82,6 +82,9 @@ public static JacksonIntegrationDtoBuilder create() { /** * Sets the value for age. + *

    + * Generated from parameter in constructor {@link JacksonIntegrationDto#JacksonIntegrationDto(String, int) + * JacksonIntegrationDto(String name, int age)} * *

    Example:

    * @@ -99,6 +102,9 @@ public JacksonIntegrationDtoBuilder age(int age) { /** * Sets the value for age by invoking the provided supplier. + *

    + * Generated from parameter in constructor {@link JacksonIntegrationDto#JacksonIntegrationDto(String, int) + * JacksonIntegrationDto(String name, int age)} * *

    Example:

    * @@ -116,6 +122,9 @@ public JacksonIntegrationDtoBuilder age(Supplier ageSupplier) { /** * Sets the value for name. + *

    + * Generated from parameter in constructor {@link JacksonIntegrationDto#JacksonIntegrationDto(String, int) + * JacksonIntegrationDto(String name, int age)} * *

    Example:

    * @@ -133,6 +142,9 @@ public JacksonIntegrationDtoBuilder name(String name) { /** * Sets the value for name by executing the provided consumer. + *

    + * Generated from parameter in constructor {@link JacksonIntegrationDto#JacksonIntegrationDto(String, int) + * JacksonIntegrationDto(String name, int age)} * *

    Example:

    * @@ -152,6 +164,9 @@ public JacksonIntegrationDtoBuilder name(Consumer nameStringBuild /** * Sets the value for name by invoking the provided supplier. + *

    + * Generated from parameter in constructor {@link JacksonIntegrationDto#JacksonIntegrationDto(String, int) + * JacksonIntegrationDto(String name, int age)} * *

    Example:

    * @@ -170,6 +185,9 @@ public JacksonIntegrationDtoBuilder name(Supplier nameSupplier) { /** * Sets the String value for name by using String.format(format, args). See * {@link String#format(String, Object...)} for details. + *

    + * Generated from parameter in constructor {@link JacksonIntegrationDto#JacksonIntegrationDto(String, int) + * JacksonIntegrationDto(String name, int age)} * *

    Example:

    * @@ -188,6 +206,9 @@ public JacksonIntegrationDtoBuilder name(String format, Object... args) { /** * Validates that the name field is not null or empty. + *

    + * Generated from parameter in constructor {@link JacksonIntegrationDto#JacksonIntegrationDto(String, int) + * JacksonIntegrationDto(String name, int age)} * * @return this builder instance for chaining * @throws IllegalArgumentException if name is null or empty diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/MannschaftDtoBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/MannschaftDtoBuilder.java index 230161e7..4333788d 100644 --- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/MannschaftDtoBuilder.java +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/MannschaftDtoBuilder.java @@ -81,6 +81,8 @@ public static MannschaftDtoBuilder create() { /** * Adds a single element to sponsoren. + *

    + * Generated from setter {@link MannschaftDto#setSponsoren(Set) setSponsoren(Set sponsoren)} * * @param element the element to add * @return current instance of builder @@ -99,6 +101,8 @@ public MannschaftDtoBuilder add2Sponsoren(SponsorDto element) { /** * Sets the value for name. + *

    + * Generated from setter {@link MannschaftDto#setName(String) setName(String name)} * *

    Example:

    * @@ -116,6 +120,8 @@ public MannschaftDtoBuilder name(String name) { /** * Sets the value for name by executing the provided consumer. + *

    + * Generated from setter {@link MannschaftDto#setName(String) setName(String name)} * *

    Example:

    * @@ -135,6 +141,8 @@ public MannschaftDtoBuilder name(Consumer nameStringBuilderConsum /** * Sets the value for name by invoking the provided supplier. + *

    + * Generated from setter {@link MannschaftDto#setName(String) setName(String name)} * *

    Example:

    * @@ -153,6 +161,8 @@ public MannschaftDtoBuilder name(Supplier nameSupplier) { /** * Sets the String value for name by using String.format(format, args). See * {@link String#format(String, Object...)} for details. + *

    + * Generated from setter {@link MannschaftDto#setName(String) setName(String name)} * *

    Example:

    * @@ -171,6 +181,8 @@ public MannschaftDtoBuilder name(String format, Object... args) { /** * Sets the value for sponsoren. + *

    + * Generated from setter {@link MannschaftDto#setSponsoren(Set) setSponsoren(Set sponsoren)} * * @param sponsoren sponsoren * @return current instance of builder @@ -182,6 +194,8 @@ public MannschaftDtoBuilder sponsoren(SponsorDto... sponsoren) { /** * Sets the value for sponsoren. + *

    + * Generated from setter {@link MannschaftDto#setSponsoren(Set) setSponsoren(Set sponsoren)} * * @param sponsoren sponsoren * @return current instance of builder @@ -193,6 +207,8 @@ public MannschaftDtoBuilder sponsoren(Set sponsoren) { /** * Sets the value for sponsoren using a builder consumer that produces the value. + *

    + * Generated from setter {@link MannschaftDto#setSponsoren(Set) setSponsoren(Set sponsoren)} * *

    Example:

    * @@ -216,6 +232,8 @@ public MannschaftDtoBuilder sponsoren( /** * Sets the value for sponsoren by invoking the provided supplier. + *

    + * Generated from setter {@link MannschaftDto#setSponsoren(Set) setSponsoren(Set sponsoren)} * * @param sponsorenSupplier supplier for sponsoren * @return current instance of builder @@ -227,6 +245,8 @@ public MannschaftDtoBuilder sponsoren(Supplier> sponsorenSupplie /** * Validates that the name field is not null or empty. + *

    + * Generated from setter {@link MannschaftDto#setName(String) setName(String name)} * * @return this builder instance for chaining * @throws IllegalArgumentException if name is null or empty diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/PersonDtoBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/PersonDtoBuilder.java index 7f0ff5e5..fd94d1d0 100644 --- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/PersonDtoBuilder.java +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/PersonDtoBuilder.java @@ -108,6 +108,8 @@ public static PersonDtoBuilder create() { /** * Adds a single element to nickNames. + *

    + * Generated from setter {@link PersonDto#setNickNames(List) setNickNames(List nickNames)} * *

    Example:

    * @@ -132,6 +134,8 @@ public PersonDtoBuilder add2NickNames(String element) { /** * Sets the value for birthdate. + *

    + * Generated from setter {@link PersonDto#setBirthdate(LocalDate) setBirthdate(LocalDate birthdate)} * *

    Example:

    * @@ -149,6 +153,8 @@ public PersonDtoBuilder birthdate(LocalDate birthdate) { /** * Sets the value for birthdate by invoking the provided supplier. + *

    + * Generated from setter {@link PersonDto#setBirthdate(LocalDate) setBirthdate(LocalDate birthdate)} * *

    Example:

    * @@ -166,6 +172,8 @@ public PersonDtoBuilder birthdate(Supplier birthdateSupplier) { /** * Sets the value for mannschaft. + *

    + * Generated from setter {@link PersonDto#setMannschaft(MannschaftDto) setMannschaft(MannschaftDto mannschaft)} * *

    Example:

    * @@ -183,6 +191,8 @@ public PersonDtoBuilder mannschaft(MannschaftDto mannschaft) { /** * Sets the value for mannschaft using a builder consumer that produces the value. + *

    + * Generated from setter {@link PersonDto#setMannschaft(MannschaftDto) setMannschaft(MannschaftDto mannschaft)} * *

    Example:

    * @@ -204,6 +214,8 @@ public PersonDtoBuilder mannschaft(Consumer mannschaftBuil /** * Sets the value for mannschaft by invoking the provided supplier. + *

    + * Generated from setter {@link PersonDto#setMannschaft(MannschaftDto) setMannschaft(MannschaftDto mannschaft)} * *

    Example:

    * @@ -221,6 +233,8 @@ public PersonDtoBuilder mannschaft(Supplier mannschaftSupplier) { /** * Sets the value for name. + *

    + * Generated from parameter in constructor {@link PersonDto#PersonDto(String) PersonDto(String name)} * *

    Example:

    * @@ -238,6 +252,8 @@ public PersonDtoBuilder name(String name) { /** * Sets the value for name by executing the provided consumer. + *

    + * Generated from parameter in constructor {@link PersonDto#PersonDto(String) PersonDto(String name)} * *

    Example:

    * @@ -257,6 +273,8 @@ public PersonDtoBuilder name(Consumer nameStringBuilderConsumer) /** * Sets the value for name by invoking the provided supplier. + *

    + * Generated from parameter in constructor {@link PersonDto#PersonDto(String) PersonDto(String name)} * *

    Example:

    * @@ -275,6 +293,8 @@ public PersonDtoBuilder name(Supplier nameSupplier) { /** * Sets the String value for name by using String.format(format, args). See * {@link String#format(String, Object...)} for details. + *

    + * Generated from parameter in constructor {@link PersonDto#PersonDto(String) PersonDto(String name)} * *

    Example:

    * @@ -293,6 +313,8 @@ public PersonDtoBuilder name(String format, Object... args) { /** * Sets the value for nickNames. + *

    + * Generated from setter {@link PersonDto#setNickNames(List) setNickNames(List nickNames)} * *

    Example:

    * @@ -310,6 +332,8 @@ public PersonDtoBuilder nickNames(String... nickNames) { /** * Sets the value for nickNames. + *

    + * Generated from setter {@link PersonDto#setNickNames(List) setNickNames(List nickNames)} * *

    Example:

    * @@ -327,6 +351,8 @@ public PersonDtoBuilder nickNames(List nickNames) { /** * Sets the value for nickNames using a builder consumer that produces the value. + *

    + * Generated from setter {@link PersonDto#setNickNames(List) setNickNames(List nickNames)} * *

    Example:

    * @@ -348,6 +374,8 @@ public PersonDtoBuilder nickNames(Consumer> nickNamesBu /** * Sets the value for nickNames by invoking the provided supplier. + *

    + * Generated from setter {@link PersonDto#setNickNames(List) setNickNames(List nickNames)} * *

    Example:

    * @@ -365,6 +393,8 @@ public PersonDtoBuilder nickNames(Supplier> nickNamesSupplier) { /** * Sets the value for nickNames2. + *

    + * Generated from setter {@link PersonDto#setNickNames2(String) setNickNames2(String nickNames2)} * *

    Example:

    * @@ -382,6 +412,8 @@ public PersonDtoBuilder nickNames2(String... nickNames2) { /** * Sets the value for nickNames2. + *

    + * Generated from setter {@link PersonDto#setNickNames2(String) setNickNames2(String nickNames2)} * *

    Example:

    * @@ -399,6 +431,8 @@ public PersonDtoBuilder nickNames2(List nickNames2) { /** * Sets the value for nickNames2 using the fluent builder consumer. + *

    + * Generated from setter {@link PersonDto#setNickNames2(String) setNickNames2(String nickNames2)} * * @param nickNames2BuilderConsumer consumer for nickNames2 * @return current instance of builder @@ -414,6 +448,8 @@ public PersonDtoBuilder nickNames2(Consumer> nickNames2 /** * Sets the value for nickNames2 by invoking the provided supplier. + *

    + * Generated from setter {@link PersonDto#setNickNames2(String) setNickNames2(String nickNames2)} * *

    Example:

    * @@ -431,6 +467,8 @@ public PersonDtoBuilder nickNames2(Supplier nickNames2Supplier) { /** * Validates that the name field is not null or empty. + *

    + * Generated from parameter in constructor {@link PersonDto#PersonDto(String) PersonDto(String name)} * * @return this builder instance for chaining * @throws IllegalArgumentException if name is null or empty diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/ProductRecordBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/ProductRecordBuilder.java index ee195d57..2d753258 100644 --- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/ProductRecordBuilder.java +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/ProductRecordBuilder.java @@ -88,6 +88,9 @@ public static ProductRecordBuilder create() { /** * Sets the value for category. + *

    + * Generated from parameter in constructor {@link ProductRecord#ProductRecord(String, double, String) + * ProductRecord(String name, double price, String category)} * *

    Example:

    * @@ -105,6 +108,9 @@ public ProductRecordBuilder category(String category) { /** * Sets the value for category by executing the provided consumer. + *

    + * Generated from parameter in constructor {@link ProductRecord#ProductRecord(String, double, String) + * ProductRecord(String name, double price, String category)} * *

    Example:

    * @@ -124,6 +130,9 @@ public ProductRecordBuilder category(Consumer categoryStringBuild /** * Sets the value for category by invoking the provided supplier. + *

    + * Generated from parameter in constructor {@link ProductRecord#ProductRecord(String, double, String) + * ProductRecord(String name, double price, String category)} * *

    Example:

    * @@ -142,6 +151,9 @@ public ProductRecordBuilder category(Supplier categorySupplier) { /** * Sets the String value for category by using String.format(format, args). See * {@link String#format(String, Object...)} for details. + *

    + * Generated from parameter in constructor {@link ProductRecord#ProductRecord(String, double, String) + * ProductRecord(String name, double price, String category)} * *

    Example:

    * @@ -160,6 +172,9 @@ public ProductRecordBuilder category(String format, Object... args) { /** * Sets the value for name. + *

    + * Generated from parameter in constructor {@link ProductRecord#ProductRecord(String, double, String) + * ProductRecord(String name, double price, String category)} * *

    Example:

    * @@ -177,6 +192,9 @@ public ProductRecordBuilder name(String name) { /** * Sets the value for name by executing the provided consumer. + *

    + * Generated from parameter in constructor {@link ProductRecord#ProductRecord(String, double, String) + * ProductRecord(String name, double price, String category)} * *

    Example:

    * @@ -196,6 +214,9 @@ public ProductRecordBuilder name(Consumer nameStringBuilderConsum /** * Sets the value for name by invoking the provided supplier. + *

    + * Generated from parameter in constructor {@link ProductRecord#ProductRecord(String, double, String) + * ProductRecord(String name, double price, String category)} * *

    Example:

    * @@ -214,6 +235,9 @@ public ProductRecordBuilder name(Supplier nameSupplier) { /** * Sets the String value for name by using String.format(format, args). See * {@link String#format(String, Object...)} for details. + *

    + * Generated from parameter in constructor {@link ProductRecord#ProductRecord(String, double, String) + * ProductRecord(String name, double price, String category)} * *

    Example:

    * @@ -232,6 +256,9 @@ public ProductRecordBuilder name(String format, Object... args) { /** * Sets the value for price. + *

    + * Generated from parameter in constructor {@link ProductRecord#ProductRecord(String, double, String) + * ProductRecord(String name, double price, String category)} * *

    Example:

    * @@ -249,6 +276,9 @@ public ProductRecordBuilder price(double price) { /** * Sets the value for price by invoking the provided supplier. + *

    + * Generated from parameter in constructor {@link ProductRecord#ProductRecord(String, double, String) + * ProductRecord(String name, double price, String category)} * *

    Example:

    * @@ -266,6 +296,9 @@ public ProductRecordBuilder price(Supplier priceSupplier) { /** * Validates that the category field is not null or empty. + *

    + * Generated from parameter in constructor {@link ProductRecord#ProductRecord(String, double, String) + * ProductRecord(String name, double price, String category)} * * @return this builder instance for chaining * @throws IllegalArgumentException if category is null or empty @@ -279,6 +312,9 @@ ProductRecordBuilder validateCategory() { /** * Validates that the name field is not null or empty. + *

    + * Generated from parameter in constructor {@link ProductRecord#ProductRecord(String, double, String) + * ProductRecord(String name, double price, String category)} * * @return this builder instance for chaining * @throws IllegalArgumentException if name is null or empty diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SponsorDtoBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SponsorDtoBuilder.java index 094a4570..f9d6982b 100644 --- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SponsorDtoBuilder.java +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SponsorDtoBuilder.java @@ -72,6 +72,8 @@ public static SponsorDtoBuilder create() { /** * Sets the value for name. + *

    + * Generated from setter {@link SponsorDto#setName(String) setName(String name)} * *

    Example:

    * @@ -89,6 +91,8 @@ public SponsorDtoBuilder name(String name) { /** * Sets the value for name by executing the provided consumer. + *

    + * Generated from setter {@link SponsorDto#setName(String) setName(String name)} * *

    Example:

    * @@ -108,6 +112,8 @@ public SponsorDtoBuilder name(Consumer nameStringBuilderConsumer) /** * Sets the value for name by invoking the provided supplier. + *

    + * Generated from setter {@link SponsorDto#setName(String) setName(String name)} * *

    Example:

    * @@ -126,6 +132,8 @@ public SponsorDtoBuilder name(Supplier nameSupplier) { /** * Sets the String value for name by using String.format(format, args). See * {@link String#format(String, Object...)} for details. + *

    + * Generated from setter {@link SponsorDto#setName(String) setName(String name)} * *

    Example:

    * @@ -144,6 +152,8 @@ public SponsorDtoBuilder name(String format, Object... args) { /** * Validates that the name field is not null or empty. + *

    + * Generated from setter {@link SponsorDto#setName(String) setName(String name)} * * @return this builder instance for chaining * @throws IllegalArgumentException if name is null or empty diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderToGenerationTypeMapper.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderToGenerationTypeMapper.java index 9e4e1e36..90680948 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderToGenerationTypeMapper.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderToGenerationTypeMapper.java @@ -25,6 +25,8 @@ package org.javahelpers.simple.builders.processor.model.core; import java.util.List; +import org.apache.commons.lang3.StringUtils; +import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; import org.javahelpers.simple.builders.processor.model.method.BuilderMethodDto; import org.javahelpers.simple.builders.processor.model.method.MethodCodePlaceholder; import org.javahelpers.simple.builders.processor.model.method.MethodCodeStringPlaceholder; @@ -83,21 +85,29 @@ public static GenerationTargetClassDto toRenderingDto(BuilderDefinitionDto build // Copy interfaces builderDto.getInterfaces().forEach(renderingDto::addInterface); + // Source class name for javadoc enrichment (e.g., "PersonDto") + // Simple class name is sufficient because the builder is always generated in the same + // package as the source DTO, so no import is needed for {@link} to resolve. + String sourceClassName = + builderDto.getBuildingTargetTypeName() != null + ? builderDto.getBuildingTargetTypeName().getClassName() + : null; + // Map and copy methods from fields for (FieldDto field : builderDto.getConstructorFieldsForBuilder()) { for (BuilderMethodDto method : field.getMethods()) { - renderingDto.addMethod(toMethodDto(method)); + renderingDto.addMethod(toMethodDto(method, sourceClassName)); } } for (FieldDto field : builderDto.getSetterFieldsForBuilder()) { for (BuilderMethodDto method : field.getMethods()) { - renderingDto.addMethod(toMethodDto(method)); + renderingDto.addMethod(toMethodDto(method, sourceClassName)); } } // Map and copy builder-level methods from enhancers for (BuilderMethodDto classMethod : builderDto.getMethods()) { - renderingDto.addMethod(toMethodDto(classMethod)); + renderingDto.addMethod(toMethodDto(classMethod, sourceClassName)); } // Map and copy nested types from enhancers @@ -118,12 +128,54 @@ public static GenerationTargetClassDto toRenderingDto(BuilderDefinitionDto build * @return a new {@link MethodDto} with all rendering fields copied */ public static MethodDto toMethodDto(BuilderMethodDto classMethod) { + return toMethodDto(classMethod, null); + } + + /** + * Maps a {@link BuilderMethodDto} to a {@link MethodDto}, enriching the javadoc with field-origin + * information including the source class name. + * + *

    All rendering-relevant fields are copied. The {@code MethodCodeDto} is shared by reference + * (not deep-copied), since the rendering phase only reads from it. + * + * @param classMethod the generation DTO to map + * @param sourceClassName the simple class name of the source DTO (e.g., "PersonDto"), or null if + * unknown + * @return a new {@link MethodDto} with all rendering fields copied + */ + private static MethodDto toMethodDto(BuilderMethodDto classMethod, String sourceClassName) { MethodDto method = new MethodDto(classMethod.getMethodName(), classMethod.getReturnType()); method.setModifier(classMethod.getModifier().orElse(null)); method.setStatic(classMethod.isStatic()); method.setPriority(classMethod.getPriority()); method.setOrdering(classMethod.getOrdering()); - method.setJavadoc(classMethod.getJavadoc()); + + // Enrich javadoc with field-origin section if source field is known + JavadocDto javadoc = classMethod.getJavadoc(); + if (javadoc == null && StringUtils.isNotBlank(classMethod.getSourceFieldName())) { + javadoc = new JavadocDto(); + } + if (javadoc != null && StringUtils.isNotBlank(classMethod.getSourceFieldName())) { + String originType = classMethod.isConstructorField() ? "parameter in constructor" : "setter"; + String displaySignature = classMethod.getSourceMethodSignature(); + String linkSignature = classMethod.getSourceMethodLinkSignature(); + if (StringUtils.isBlank(displaySignature)) { + displaySignature = classMethod.getSourceFieldName(); + } + if (StringUtils.isBlank(linkSignature)) { + linkSignature = displaySignature; + } + String originLine; + if (StringUtils.isNotBlank(sourceClassName)) { + originLine = + "

    Generated from %s {@link %s#%s %s}" + .formatted(originType, sourceClassName, linkSignature, displaySignature); + } else { + originLine = "

    Generated from %s %s".formatted(originType, displaySignature); + } + javadoc.appendDescriptionLine(originLine); + } + method.setJavadoc(javadoc); classMethod.getAnnotations().forEach(method::addAnnotation); classMethod.getParameters().forEach(method::addParameter); classMethod.getGenericParameters().forEach(method::addGenericParameter); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/FieldDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/FieldDto.java index d5107f15..eebd1626 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/FieldDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/FieldDto.java @@ -82,6 +82,20 @@ public class FieldDto { */ private final List parameterAnnotations = new ArrayList<>(); + /** + * Source method signature for javadoc enrichment, e.g. {@code setTeamname(String teamName)} for + * setter fields or {@code PersonDto(String name, int age, ...)} for constructor parameters. This + * is the display label shown in the {@code {@link}} tag. {@code null} if not yet computed. + */ + private String sourceMethodSignature; + + /** + * Types-only signature for the {@code {@link}} target, e.g. {@code setTeamname(String)} or {@code + * PersonDto(String, int)}. Uses raw types (no generics) per Javadoc spec. {@code null} if not yet + * computed. + */ + private String sourceMethodLinkSignature; + /** * Gets the original field name from the DTO. This name is used for generating method names, * parameter names, and setter method names (e.g., "userName" becomes "setUserName"). @@ -292,6 +306,42 @@ public void setParameterAnnotations(List annotations) { } } + /** + * Returns the source method signature for javadoc enrichment (display label). + * + * @return the source method signature, or {@code null} if not yet computed + */ + public String getSourceMethodSignature() { + return sourceMethodSignature; + } + + /** + * Sets the source method signature for javadoc enrichment (display label). + * + * @param sourceMethodSignature the display signature, e.g. {@code setTeamname(String teamName)} + */ + public void setSourceMethodSignature(String sourceMethodSignature) { + this.sourceMethodSignature = sourceMethodSignature; + } + + /** + * Returns the types-only link signature for the {@code {@link}} target. + * + * @return the link signature, or {@code null} if not yet computed + */ + public String getSourceMethodLinkSignature() { + return sourceMethodLinkSignature; + } + + /** + * Sets the types-only link signature for the {@code {@link}} target. + * + * @param sourceMethodLinkSignature the link signature, e.g. {@code setTeamname(String)} + */ + public void setSourceMethodLinkSignature(String sourceMethodLinkSignature) { + this.sourceMethodLinkSignature = sourceMethodLinkSignature; + } + /** * Checks if this field has a parameter annotation with the given fully qualified name. * diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/javadoc/JavadocDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/javadoc/JavadocDto.java index f4213415..5f70ef8b 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/javadoc/JavadocDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/javadoc/JavadocDto.java @@ -258,6 +258,23 @@ public JavadocDto appendDescription(String additionalText) { return this; } + /** + * Appends additional text to the existing description on a new line. + * + *

    If the current description is blank, the additional text becomes the description. Otherwise, + * the additional text is appended with a newline separator. + * + * @param additionalText the text to append on a new line + * @return this JavadocDto for fluent chaining + */ + public JavadocDto appendDescriptionLine(String additionalText) { + if (StringUtils.isNotBlank(additionalText)) { + this.description = + StringUtils.isBlank(description) ? additionalText : description + "\n" + additionalText; + } + return this; + } + /** * Returns whether this Javadoc has any content (description, tags, or code blocks). * diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/BuilderMethodDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/BuilderMethodDto.java index 3eab8456..969307a4 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/BuilderMethodDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/BuilderMethodDto.java @@ -96,6 +96,20 @@ public class BuilderMethodDto { /** Name of the source field this method was generated for. {@code null} for enhancer methods. */ private String sourceFieldName; + /** + * Source method signature for javadoc enrichment, e.g. {@code setTeamname(String teamName)} for + * setter fields or {@code PersonDto(String name, int age, ...)} for constructor parameters. + * {@code null} for enhancer methods. + */ + private String sourceMethodSignature; + + /** + * Types-only signature for the {@code {@link}} target, e.g. {@code setTeamname(String)} or {@code + * PersonDto(String, int)}. Uses raw types (no generics) per Javadoc spec. {@code null} for + * enhancer methods. + */ + private String sourceMethodLinkSignature; + /** Whether this method was generated for a constructor field (vs a setter field). */ private boolean constructorField; @@ -430,6 +444,45 @@ public void setSourceFieldName(String sourceFieldName) { this.sourceFieldName = sourceFieldName; } + /** + * Returns the source method signature for javadoc enrichment. + * + * @return the source method signature, or {@code null} for enhancer-generated methods + */ + public String getSourceMethodSignature() { + return sourceMethodSignature; + } + + /** + * Sets the source method signature for javadoc enrichment. + * + * @param sourceMethodSignature the source method signature, e.g. {@code setTeamname(String + * teamName)} for setters or {@code PersonDto(String name, int age, ...)} for constructor + * parameters + */ + public void setSourceMethodSignature(String sourceMethodSignature) { + this.sourceMethodSignature = sourceMethodSignature; + } + + /** + * Returns the types-only link signature for the {@code {@link}} target. + * + * @return the link signature, or {@code null} for enhancer-generated methods + */ + public String getSourceMethodLinkSignature() { + return sourceMethodLinkSignature; + } + + /** + * Sets the types-only link signature for the {@code {@link}} target. + * + * @param sourceMethodLinkSignature the link signature, e.g. {@code setTeamname(String)} or {@code + * PersonDto(String, int)} + */ + public void setSourceMethodLinkSignature(String sourceMethodLinkSignature) { + this.sourceMethodLinkSignature = sourceMethodLinkSignature; + } + /** * Returns whether this method was generated for a constructor field. * diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/TypeName.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/TypeName.java index 3102acf2..4800d00a 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/TypeName.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/TypeName.java @@ -81,6 +81,19 @@ public String getClassName() { return className; } + /** + * Returns the simple name with generic type arguments (if any), without package prefixes. + * + *

    For a plain {@code TypeName} this is equivalent to {@link #getClassName()}. Subclasses like + * {@link TypeNameGeneric} override this to include generic parameters, e.g. {@code List} + * instead of just {@code List}. + * + * @return simple name with generics, e.g. {@code String} or {@code List} + */ + public String getSimpleNameWithGenerics() { + return className; + } + /** * Returns the full qualified name (package + class name). * diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/TypeNameGeneric.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/TypeNameGeneric.java index 115d6160..505aabde 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/TypeNameGeneric.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/TypeNameGeneric.java @@ -166,6 +166,22 @@ public String getFullQualifiedName() { return baseName + "<" + typeArgs + ">"; } + @Override + public String getSimpleNameWithGenerics() { + String baseName = getClassName(); + if (innerTypeArguments.isEmpty()) { + return baseName; + } + + String typeArgs = + innerTypeArguments.stream() + .map(TypeName::getSimpleNameWithGenerics) + .reduce((a, b) -> a + ", " + b) + .orElse(""); + + return baseName + "<" + typeArgs + ">"; + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java index 1af6f864..83354797 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java @@ -135,16 +135,62 @@ private static void finalizeDefinition( builderDto.addClassField(classField); } - // 2. Set origin info on field-level BuilderMethodDto instances + // 2. Set origin info on FieldDto (primary source of truth), then propagate to methods + // For constructor parameters, build the full constructor signature once: + // Display: ClassName(type1 param1, type2 param2, ...) + // Link: ClassName(Type1, Type2, ...) (types only, raw erasure for {@link} target) + String constructorSignature = null; + String constructorLinkSignature = null; + if (!builderDto.getConstructorFieldsForBuilder().isEmpty() + && builderDto.getBuildingTargetTypeName() != null) { + StringBuilder displaySb = new StringBuilder(); + StringBuilder linkSb = new StringBuilder(); + displaySb.append(builderDto.getBuildingTargetTypeName().getClassName()).append("("); + linkSb.append(builderDto.getBuildingTargetTypeName().getClassName()).append("("); + boolean first = true; + for (FieldDto field : builderDto.getConstructorFieldsForBuilder()) { + if (!first) { + displaySb.append(", "); + linkSb.append(", "); + } + displaySb + .append(field.getFieldType().getSimpleNameWithGenerics()) + .append(" ") + .append(field.getOriginalFieldName()); + linkSb.append(field.getFieldType().getClassName()); + first = false; + } + displaySb.append(")"); + linkSb.append(")"); + constructorSignature = displaySb.toString(); + constructorLinkSignature = linkSb.toString(); + } for (FieldDto field : builderDto.getConstructorFieldsForBuilder()) { + field.setSourceMethodSignature(constructorSignature); + field.setSourceMethodLinkSignature(constructorLinkSignature); for (BuilderMethodDto method : field.getMethods()) { method.setSourceFieldName(field.getOriginalFieldName()); + method.setSourceMethodSignature(field.getSourceMethodSignature()); + method.setSourceMethodLinkSignature(field.getSourceMethodLinkSignature()); method.setConstructorField(true); } } for (FieldDto field : builderDto.getSetterFieldsForBuilder()) { + String displaySignature = + field.getSetterName() + + "(" + + field.getFieldType().getSimpleNameWithGenerics() + + " " + + field.getOriginalFieldName() + + ")"; + String linkSignature = + field.getSetterName() + "(" + field.getFieldType().getClassName() + ")"; + field.setSourceMethodSignature(displaySignature); + field.setSourceMethodLinkSignature(linkSignature); for (BuilderMethodDto method : field.getMethods()) { method.setSourceFieldName(field.getOriginalFieldName()); + method.setSourceMethodSignature(field.getSourceMethodSignature()); + method.setSourceMethodLinkSignature(field.getSourceMethodLinkSignature()); method.setConstructorField(false); } } diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderConfigurationReaderTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderConfigurationReaderTest.java index ad135297..80ddcc7c 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderConfigurationReaderTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderConfigurationReaderTest.java @@ -528,6 +528,8 @@ public static PersonDtoMinimalBuilder create() { /** * Sets the value for name. + *

    + * Generated from setter {@link PersonDto#setName(String) setName(String name)} * *

    Example:

    * @@ -545,6 +547,8 @@ public PersonDtoMinimalBuilder withName(String name) { /** * Sets the value for tags. + *

    + * Generated from setter {@link PersonDto#setTags(List) setTags(List tags)} * *

    Example:

    * diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderJavadocExampleTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderJavadocExampleTest.java index cb34c28d..76947b67 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderJavadocExampleTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderJavadocExampleTest.java @@ -130,11 +130,13 @@ void shouldGenerateMethodJavadocExampleForBasicStringSetter() { String generatedCode = loadGeneratedSource(compilation, builderClassName); assertGenerationSucceeded(compilation, builderClassName, generatedCode); - // Expected method javadoc for the basic setter (description + example + tags) + // Expected method javadoc for the basic setter (description + field origin + example + tags) ProcessorAsserts.assertContaining( generatedCode, """ * Sets the value for teamname. + *

    + * Generated from setter {@link Person#setTeamname(String) setTeamname(String teamname)} * *

    Example:

    * @@ -173,6 +175,8 @@ void shouldGenerateMethodJavadocExampleForPrimitiveSetter() { generatedCode, """ * Sets the value for amount. + *

    + * Generated from setter {@link Counter#setAmount(int) setAmount(int amount)} * *

    Example:

    * @@ -242,6 +246,8 @@ void shouldGenerateMethodJavadocExampleForAddToCollection() { generatedCode, """ * Adds a single element to tags. + *

    + * Generated from setter {@link TagsDto#setTags(List) setTags(List tags)} * *

    Example:

    * 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 a3b4af88..27b6fde2 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 @@ -122,7 +122,9 @@ void shouldLogDebugMessagesWhenVerboseModeEnabled() { "[DEBUG] │ │ ├─ Applying: ClassJavaDocEnhancer (priority: 10)", "[DEBUG] │ │ └─ Applied 8 builder enhancers", "[DEBUG] │ ├─ Finalizing builder definition", - "[DEBUG] │ │ └─ Finalized: 1 class fields, 9 methods, 2 constructors", + "[DEBUG] │ │ ├─ Resolving method conflicts", + "[DEBUG] │ │ │ └─ Resolved: 9 signatures, 0 conflicts, 0 methods removed", + "[DEBUG] │ │ └─ Finalized: 1 class fields, 5 builder-level methods, 2 constructors", "[DEBUG] │ ├─ Builder will be generated as: VerboseTestBuilder", "[DEBUG] │ └─ Builder definition extracted: VerboseTestBuilder", "[DEBUG] ├─ Code generation for class: VerboseTestBuilder", diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/ComprehensiveFeatureIntegrationTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/ComprehensiveFeatureIntegrationTest.java index 2e2b267d..f38d7b5e 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/ComprehensiveFeatureIntegrationTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/ComprehensiveFeatureIntegrationTest.java @@ -295,6 +295,11 @@ public static PersonDtoBuilder create() { /** * Adds a single element to nicknames. + *

    + * Generated from parameter in constructor + * {@link PersonDto#PersonDto(String,int,Optional,List,Set,Map,AddressDto,List,LinkedList) PersonDto(String + * name, int age, Optional email, List nicknames, Set tags, Map metadata, + * AddressDto address, List previousAddresses, LinkedList phoneNumbers)} * *

    Example:

    * @@ -319,6 +324,11 @@ public PersonDtoBuilder add2Nicknames(String element) { /** * Adds a single element to phoneNumbers. + *

    + * Generated from parameter in constructor + * {@link PersonDto#PersonDto(String,int,Optional,List,Set,Map,AddressDto,List,LinkedList) PersonDto(String + * name, int age, Optional email, List nicknames, Set tags, Map metadata, + * AddressDto address, List previousAddresses, LinkedList phoneNumbers)} * *

    Example:

    * @@ -343,6 +353,11 @@ public PersonDtoBuilder add2PhoneNumbers(String element) { /** * Adds a single element to previousAddresses. + *

    + * Generated from parameter in constructor + * {@link PersonDto#PersonDto(String,int,Optional,List,Set,Map,AddressDto,List,LinkedList) PersonDto(String + * name, int age, Optional email, List nicknames, Set tags, Map metadata, + * AddressDto address, List previousAddresses, LinkedList phoneNumbers)} * * @param element the element to add * @return current instance of builder @@ -361,6 +376,11 @@ public PersonDtoBuilder add2PreviousAddresses(AddressDto element) { /** * Adds a single element to tags. + *

    + * Generated from parameter in constructor + * {@link PersonDto#PersonDto(String,int,Optional,List,Set,Map,AddressDto,List,LinkedList) PersonDto(String + * name, int age, Optional email, List nicknames, Set tags, Map metadata, + * AddressDto address, List previousAddresses, LinkedList phoneNumbers)} * *

    Example:

    * @@ -385,6 +405,11 @@ public PersonDtoBuilder add2Tags(String element) { /** * Sets the value for address. + *

    + * Generated from parameter in constructor + * {@link PersonDto#PersonDto(String,int,Optional,List,Set,Map,AddressDto,List,LinkedList) PersonDto(String + * name, int age, Optional email, List nicknames, Set tags, Map metadata, + * AddressDto address, List previousAddresses, LinkedList phoneNumbers)} * *

    Example:

    * @@ -402,6 +427,11 @@ public PersonDtoBuilder address(AddressDto address) { /** * Sets the value for address using a builder consumer that produces the value. + *

    + * Generated from parameter in constructor + * {@link PersonDto#PersonDto(String,int,Optional,List,Set,Map,AddressDto,List,LinkedList) PersonDto(String + * name, int age, Optional email, List nicknames, Set tags, Map metadata, + * AddressDto address, List previousAddresses, LinkedList phoneNumbers)} * *

    Example:

    * @@ -423,6 +453,11 @@ public PersonDtoBuilder address(Consumer addressBuilderConsum /** * Sets the value for address by invoking the provided supplier. + *

    + * Generated from parameter in constructor + * {@link PersonDto#PersonDto(String,int,Optional,List,Set,Map,AddressDto,List,LinkedList) PersonDto(String + * name, int age, Optional email, List nicknames, Set tags, Map metadata, + * AddressDto address, List previousAddresses, LinkedList phoneNumbers)} * *

    Example:

    * @@ -440,6 +475,11 @@ public PersonDtoBuilder address(Supplier addressSupplier) { /** * Sets the value for age. + *

    + * Generated from parameter in constructor + * {@link PersonDto#PersonDto(String,int,Optional,List,Set,Map,AddressDto,List,LinkedList) PersonDto(String + * name, int age, Optional email, List nicknames, Set tags, Map metadata, + * AddressDto address, List previousAddresses, LinkedList phoneNumbers)} * *

    Example:

    * @@ -457,6 +497,11 @@ public PersonDtoBuilder age(int age) { /** * Sets the value for age by invoking the provided supplier. + *

    + * Generated from parameter in constructor + * {@link PersonDto#PersonDto(String,int,Optional,List,Set,Map,AddressDto,List,LinkedList) PersonDto(String + * name, int age, Optional email, List nicknames, Set tags, Map metadata, + * AddressDto address, List previousAddresses, LinkedList phoneNumbers)} * *

    Example:

    * @@ -474,6 +519,11 @@ public PersonDtoBuilder age(Supplier ageSupplier) { /** * Sets the value for email. + *

    + * Generated from parameter in constructor + * {@link PersonDto#PersonDto(String,int,Optional,List,Set,Map,AddressDto,List,LinkedList) PersonDto(String + * name, int age, Optional email, List nicknames, Set tags, Map metadata, + * AddressDto address, List previousAddresses, LinkedList phoneNumbers)} * *

    Example:

    * @@ -491,6 +541,11 @@ public PersonDtoBuilder email(String email) { /** * Sets the value for email. + *

    + * Generated from parameter in constructor + * {@link PersonDto#PersonDto(String,int,Optional,List,Set,Map,AddressDto,List,LinkedList) PersonDto(String + * name, int age, Optional email, List nicknames, Set tags, Map metadata, + * AddressDto address, List previousAddresses, LinkedList phoneNumbers)} * * @param email email * @return current instance of builder @@ -502,6 +557,11 @@ public PersonDtoBuilder email(Optional email) { /** * Sets the value for email by executing the provided consumer. + *

    + * Generated from parameter in constructor + * {@link PersonDto#PersonDto(String,int,Optional,List,Set,Map,AddressDto,List,LinkedList) PersonDto(String + * name, int age, Optional email, List nicknames, Set tags, Map metadata, + * AddressDto address, List previousAddresses, LinkedList phoneNumbers)} * *

    Example:

    * @@ -521,6 +581,11 @@ public PersonDtoBuilder email(Consumer emailStringBuilderConsumer /** * Sets the value for email by invoking the provided supplier. + *

    + * Generated from parameter in constructor + * {@link PersonDto#PersonDto(String,int,Optional,List,Set,Map,AddressDto,List,LinkedList) PersonDto(String + * name, int age, Optional email, List nicknames, Set tags, Map metadata, + * AddressDto address, List previousAddresses, LinkedList phoneNumbers)} * * @param emailSupplier supplier for email * @return current instance of builder @@ -533,6 +598,11 @@ public PersonDtoBuilder email(Supplier> emailSupplier) { /** * Sets the String value for email by using String.format(format, args). See * {@link String#format(String, Object...)} for details. + *

    + * Generated from parameter in constructor + * {@link PersonDto#PersonDto(String,int,Optional,List,Set,Map,AddressDto,List,LinkedList) PersonDto(String + * name, int age, Optional email, List nicknames, Set tags, Map metadata, + * AddressDto address, List previousAddresses, LinkedList phoneNumbers)} * *

    Example:

    * @@ -551,6 +621,11 @@ public PersonDtoBuilder email(String format, Object... args) { /** * Sets the value for metadata. + *

    + * Generated from parameter in constructor + * {@link PersonDto#PersonDto(String,int,Optional,List,Set,Map,AddressDto,List,LinkedList) PersonDto(String + * name, int age, Optional email, List nicknames, Set tags, Map metadata, + * AddressDto address, List previousAddresses, LinkedList phoneNumbers)} * *

    Example:

    * @@ -568,6 +643,11 @@ public PersonDtoBuilder metadata(Entry... metadata) { /** * Sets the value for metadata. + *

    + * Generated from parameter in constructor + * {@link PersonDto#PersonDto(String,int,Optional,List,Set,Map,AddressDto,List,LinkedList) PersonDto(String + * name, int age, Optional email, List nicknames, Set tags, Map metadata, + * AddressDto address, List previousAddresses, LinkedList phoneNumbers)} * *

    Example:

    * @@ -585,6 +665,11 @@ public PersonDtoBuilder metadata(Map metadata) { /** * Sets the value for metadata using a builder consumer that produces the value. + *

    + * Generated from parameter in constructor + * {@link PersonDto#PersonDto(String,int,Optional,List,Set,Map,AddressDto,List,LinkedList) PersonDto(String + * name, int age, Optional email, List nicknames, Set tags, Map metadata, + * AddressDto address, List previousAddresses, LinkedList phoneNumbers)} * * @param metadataBuilderConsumer consumer providing an instance of a builder for metadata * @return current instance of builder @@ -600,6 +685,11 @@ public PersonDtoBuilder metadata(Consumer> metada /** * Sets the value for metadata by invoking the provided supplier. + *

    + * Generated from parameter in constructor + * {@link PersonDto#PersonDto(String,int,Optional,List,Set,Map,AddressDto,List,LinkedList) PersonDto(String + * name, int age, Optional email, List nicknames, Set tags, Map metadata, + * AddressDto address, List previousAddresses, LinkedList phoneNumbers)} * *

    Example:

    * @@ -617,6 +707,11 @@ public PersonDtoBuilder metadata(Supplier> metadataSupplier) /** * Sets the value for name. + *

    + * Generated from parameter in constructor + * {@link PersonDto#PersonDto(String,int,Optional,List,Set,Map,AddressDto,List,LinkedList) PersonDto(String + * name, int age, Optional email, List nicknames, Set tags, Map metadata, + * AddressDto address, List previousAddresses, LinkedList phoneNumbers)} * *

    Example:

    * @@ -634,6 +729,11 @@ public PersonDtoBuilder name(String name) { /** * Sets the value for name by executing the provided consumer. + *

    + * Generated from parameter in constructor + * {@link PersonDto#PersonDto(String,int,Optional,List,Set,Map,AddressDto,List,LinkedList) PersonDto(String + * name, int age, Optional email, List nicknames, Set tags, Map metadata, + * AddressDto address, List previousAddresses, LinkedList phoneNumbers)} * *

    Example:

    * @@ -653,6 +753,11 @@ public PersonDtoBuilder name(Consumer nameStringBuilderConsumer) /** * Sets the value for name by invoking the provided supplier. + *

    + * Generated from parameter in constructor + * {@link PersonDto#PersonDto(String,int,Optional,List,Set,Map,AddressDto,List,LinkedList) PersonDto(String + * name, int age, Optional email, List nicknames, Set tags, Map metadata, + * AddressDto address, List previousAddresses, LinkedList phoneNumbers)} * *

    Example:

    * @@ -671,6 +776,11 @@ public PersonDtoBuilder name(Supplier nameSupplier) { /** * Sets the String value for name by using String.format(format, args). See * {@link String#format(String, Object...)} for details. + *

    + * Generated from parameter in constructor + * {@link PersonDto#PersonDto(String,int,Optional,List,Set,Map,AddressDto,List,LinkedList) PersonDto(String + * name, int age, Optional email, List nicknames, Set tags, Map metadata, + * AddressDto address, List previousAddresses, LinkedList phoneNumbers)} * *

    Example:

    * @@ -689,6 +799,11 @@ public PersonDtoBuilder name(String format, Object... args) { /** * Sets the value for nicknames. + *

    + * Generated from parameter in constructor + * {@link PersonDto#PersonDto(String,int,Optional,List,Set,Map,AddressDto,List,LinkedList) PersonDto(String + * name, int age, Optional email, List nicknames, Set tags, Map metadata, + * AddressDto address, List previousAddresses, LinkedList phoneNumbers)} * *

    Example:

    * @@ -706,6 +821,11 @@ public PersonDtoBuilder nicknames(String... nicknames) { /** * Sets the value for nicknames. + *

    + * Generated from parameter in constructor + * {@link PersonDto#PersonDto(String,int,Optional,List,Set,Map,AddressDto,List,LinkedList) PersonDto(String + * name, int age, Optional email, List nicknames, Set tags, Map metadata, + * AddressDto address, List previousAddresses, LinkedList phoneNumbers)} * *

    Example:

    * @@ -723,6 +843,11 @@ public PersonDtoBuilder nicknames(List nicknames) { /** * Sets the value for nicknames using a builder consumer that produces the value. + *

    + * Generated from parameter in constructor + * {@link PersonDto#PersonDto(String,int,Optional,List,Set,Map,AddressDto,List,LinkedList) PersonDto(String + * name, int age, Optional email, List nicknames, Set tags, Map metadata, + * AddressDto address, List previousAddresses, LinkedList phoneNumbers)} * *

    Example:

    * @@ -744,6 +869,11 @@ public PersonDtoBuilder nicknames(Consumer> nicknamesBu /** * Sets the value for nicknames by invoking the provided supplier. + *

    + * Generated from parameter in constructor + * {@link PersonDto#PersonDto(String,int,Optional,List,Set,Map,AddressDto,List,LinkedList) PersonDto(String + * name, int age, Optional email, List nicknames, Set tags, Map metadata, + * AddressDto address, List previousAddresses, LinkedList phoneNumbers)} * *

    Example:

    * @@ -761,6 +891,11 @@ public PersonDtoBuilder nicknames(Supplier> nicknamesSupplier) { /** * Sets the value for phoneNumbers. + *

    + * Generated from parameter in constructor + * {@link PersonDto#PersonDto(String,int,Optional,List,Set,Map,AddressDto,List,LinkedList) PersonDto(String + * name, int age, Optional email, List nicknames, Set tags, Map metadata, + * AddressDto address, List previousAddresses, LinkedList phoneNumbers)} * *

    Example:

    * @@ -778,6 +913,11 @@ public PersonDtoBuilder phoneNumbers(String... phoneNumbers) { /** * Sets the value for phoneNumbers. + *

    + * Generated from parameter in constructor + * {@link PersonDto#PersonDto(String,int,Optional,List,Set,Map,AddressDto,List,LinkedList) PersonDto(String + * name, int age, Optional email, List nicknames, Set tags, Map metadata, + * AddressDto address, List previousAddresses, LinkedList phoneNumbers)} * *

    Example:

    * @@ -795,6 +935,11 @@ public PersonDtoBuilder phoneNumbers(LinkedList phoneNumbers) { /** * Sets the value for phoneNumbers using a builder consumer that produces the value. + *

    + * Generated from parameter in constructor + * {@link PersonDto#PersonDto(String,int,Optional,List,Set,Map,AddressDto,List,LinkedList) PersonDto(String + * name, int age, Optional email, List nicknames, Set tags, Map metadata, + * AddressDto address, List previousAddresses, LinkedList phoneNumbers)} * *

    Example:

    * @@ -816,6 +961,11 @@ public PersonDtoBuilder phoneNumbers(Consumer> phoneNum /** * Sets the value for phoneNumbers by invoking the provided supplier. + *

    + * Generated from parameter in constructor + * {@link PersonDto#PersonDto(String,int,Optional,List,Set,Map,AddressDto,List,LinkedList) PersonDto(String + * name, int age, Optional email, List nicknames, Set tags, Map metadata, + * AddressDto address, List previousAddresses, LinkedList phoneNumbers)} * *

    Example:

    * @@ -833,6 +983,11 @@ public PersonDtoBuilder phoneNumbers(Supplier> phoneNumbersSu /** * Sets the value for previousAddresses. + *

    + * Generated from parameter in constructor + * {@link PersonDto#PersonDto(String,int,Optional,List,Set,Map,AddressDto,List,LinkedList) PersonDto(String + * name, int age, Optional email, List nicknames, Set tags, Map metadata, + * AddressDto address, List previousAddresses, LinkedList phoneNumbers)} * * @param previousAddresses previousAddresses * @return current instance of builder @@ -844,6 +999,11 @@ public PersonDtoBuilder previousAddresses(AddressDto... previousAddresses) { /** * Sets the value for previousAddresses. + *

    + * Generated from parameter in constructor + * {@link PersonDto#PersonDto(String,int,Optional,List,Set,Map,AddressDto,List,LinkedList) PersonDto(String + * name, int age, Optional email, List nicknames, Set tags, Map metadata, + * AddressDto address, List previousAddresses, LinkedList phoneNumbers)} * * @param previousAddresses previousAddresses * @return current instance of builder @@ -855,6 +1015,11 @@ public PersonDtoBuilder previousAddresses(List previousAddresses) { /** * Sets the value for previousAddresses using a builder consumer that produces the value. + *

    + * Generated from parameter in constructor + * {@link PersonDto#PersonDto(String,int,Optional,List,Set,Map,AddressDto,List,LinkedList) PersonDto(String + * name, int age, Optional email, List nicknames, Set tags, Map metadata, + * AddressDto address, List previousAddresses, LinkedList phoneNumbers)} * *

    Example:

    * @@ -878,6 +1043,11 @@ public PersonDtoBuilder previousAddresses( /** * Sets the value for previousAddresses by invoking the provided supplier. + *

    + * Generated from parameter in constructor + * {@link PersonDto#PersonDto(String,int,Optional,List,Set,Map,AddressDto,List,LinkedList) PersonDto(String + * name, int age, Optional email, List nicknames, Set tags, Map metadata, + * AddressDto address, List previousAddresses, LinkedList phoneNumbers)} * * @param previousAddressesSupplier supplier for previousAddresses * @return current instance of builder @@ -889,6 +1059,11 @@ public PersonDtoBuilder previousAddresses(Supplier> previousAdd /** * Sets the value for tags. + *

    + * Generated from parameter in constructor + * {@link PersonDto#PersonDto(String,int,Optional,List,Set,Map,AddressDto,List,LinkedList) PersonDto(String + * name, int age, Optional email, List nicknames, Set tags, Map metadata, + * AddressDto address, List previousAddresses, LinkedList phoneNumbers)} * *

    Example:

    * @@ -906,6 +1081,11 @@ public PersonDtoBuilder tags(String... tags) { /** * Sets the value for tags. + *

    + * Generated from parameter in constructor + * {@link PersonDto#PersonDto(String,int,Optional,List,Set,Map,AddressDto,List,LinkedList) PersonDto(String + * name, int age, Optional email, List nicknames, Set tags, Map metadata, + * AddressDto address, List previousAddresses, LinkedList phoneNumbers)} * *

    Example:

    * @@ -923,6 +1103,11 @@ public PersonDtoBuilder tags(Set tags) { /** * Sets the value for tags using a builder consumer that produces the value. + *

    + * Generated from parameter in constructor + * {@link PersonDto#PersonDto(String,int,Optional,List,Set,Map,AddressDto,List,LinkedList) PersonDto(String + * name, int age, Optional email, List nicknames, Set tags, Map metadata, + * AddressDto address, List previousAddresses, LinkedList phoneNumbers)} * *

    Example:

    * @@ -944,6 +1129,11 @@ public PersonDtoBuilder tags(Consumer> tagsBuilderConsume /** * Sets the value for tags by invoking the provided supplier. + *

    + * Generated from parameter in constructor + * {@link PersonDto#PersonDto(String,int,Optional,List,Set,Map,AddressDto,List,LinkedList) PersonDto(String + * name, int age, Optional email, List nicknames, Set tags, Map metadata, + * AddressDto address, List previousAddresses, LinkedList phoneNumbers)} * *

    Example:

    * diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/MethodConflictResolutionTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/MethodConflictResolutionTest.java index 9ccb1665..415e201a 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/MethodConflictResolutionTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/MethodConflictResolutionTest.java @@ -389,10 +389,10 @@ public void setValue(java.util.function.Supplier valueSupplier) { .filter(d -> d.getKind() == Diagnostic.Kind.WARNING) .filter( d -> - d.getMessage(null).contains("Method conflict") - && d.getMessage(null).contains("dropped in favor of")) - .filter(d -> d.getMessage(null).contains("priority 80")) - .filter(d -> d.getMessage(null).contains("priority 100")) + d.getMessage(null).contains("Method conflict resolved") + && d.getMessage(null).contains("Dropped")) + .filter(d -> d.getMessage(null).contains("priority=80")) + .filter(d -> d.getMessage(null).contains("priority=100")) .count(); assert lowerPriorityDroppedCount > 0 From 5ad9d799ac6bcb4f15ec07b2b7e596c2e75f223a Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 2 Aug 2026 22:18:02 +0200 Subject: [PATCH 11/23] Refactoring conflict resolution, it is done now primary in BuilderDefinitionCreator, only on fallback cases it is in RoasterCodeGenerator --- .../roaster/RoasterCodeGenerator.java | 30 +- .../core/BuilderToGenerationTypeMapper.java | 58 +--- .../processor/model/core/FieldDto.java | 50 --- .../model/method/BuilderMethodDto.java | 71 ++--- .../processor/model/method/MethodDto.java | 39 +-- .../processing/BuilderDefinitionCreator.java | 286 ++++++++++++++---- 6 files changed, 282 insertions(+), 252 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java index 68b759b8..bec7cb86 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java @@ -231,6 +231,19 @@ private void appendMethods(JavaClassSource source, GenerationTargetClassDto clas logger.debugEndOperation("Methods added: %d", resolvedMethods.size()); } + /** + * Generic safety net for method conflict resolution, preventing the code generator from producing + * invalid output (duplicate method signatures). + * + *

    This is a generation-level check that operates on {@link MethodDto} (rendering-side DTO) + * which no longer carries field-origin metadata. Builder-specific conflict resolution with + * field-origin logging is performed earlier in {@link + * org.javahelpers.simple.builders.processor.processing.BuilderDefinitionCreator#resolveMethodConflicts}. + * + *

    Since conflicts should already be resolved by the builder-specific step, this safety net + * simply keeps the first occurrence for any remaining duplicate signatures and logs a generic + * warning. + */ private List resolveMethodConflicts(List methods) { MethodDto.MethodComparator comparator = new MethodDto.MethodComparator(); @@ -246,20 +259,9 @@ private List resolveMethodConflicts(List methods) { if (existing == null) { signatureToMethod.put(signature, method); } else { - if (method.getPriority() > existing.getPriority()) { - signatureToMethod.put(signature, method); - logger.warning( - " Method conflict: '%s' (priority %d) dropped in favor of priority %d", - signature, existing.getPriority(), method.getPriority()); - } else if (method.getPriority() < existing.getPriority()) { - logger.warning( - " Method conflict: '%s' (priority %d) dropped in favor of priority %d", - signature, method.getPriority(), existing.getPriority()); - } else { - logger.warning( - " Method conflict: '%s' (priority %d) - equal priority, keeping first", - signature, method.getPriority()); - } + logger.warning( + " Unexpected duplicate method signature: '%s' — keeping first occurrence (safety net)", + signature); } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderToGenerationTypeMapper.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderToGenerationTypeMapper.java index 90680948..2a267520 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderToGenerationTypeMapper.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderToGenerationTypeMapper.java @@ -85,29 +85,21 @@ public static GenerationTargetClassDto toRenderingDto(BuilderDefinitionDto build // Copy interfaces builderDto.getInterfaces().forEach(renderingDto::addInterface); - // Source class name for javadoc enrichment (e.g., "PersonDto") - // Simple class name is sufficient because the builder is always generated in the same - // package as the source DTO, so no import is needed for {@link} to resolve. - String sourceClassName = - builderDto.getBuildingTargetTypeName() != null - ? builderDto.getBuildingTargetTypeName().getClassName() - : null; - // Map and copy methods from fields for (FieldDto field : builderDto.getConstructorFieldsForBuilder()) { for (BuilderMethodDto method : field.getMethods()) { - renderingDto.addMethod(toMethodDto(method, sourceClassName)); + renderingDto.addMethod(toMethodDto(method)); } } for (FieldDto field : builderDto.getSetterFieldsForBuilder()) { for (BuilderMethodDto method : field.getMethods()) { - renderingDto.addMethod(toMethodDto(method, sourceClassName)); + renderingDto.addMethod(toMethodDto(method)); } } // Map and copy builder-level methods from enhancers for (BuilderMethodDto classMethod : builderDto.getMethods()) { - renderingDto.addMethod(toMethodDto(classMethod, sourceClassName)); + renderingDto.addMethod(toMethodDto(classMethod)); } // Map and copy nested types from enhancers @@ -119,7 +111,8 @@ public static GenerationTargetClassDto toRenderingDto(BuilderDefinitionDto build } /** - * Maps a {@link BuilderMethodDto} to a {@link MethodDto}. + * Maps a {@link BuilderMethodDto} to a {@link MethodDto}, enriching the javadoc with the + * pre-built source description if available. * *

    All rendering-relevant fields are copied. The {@code MethodCodeDto} is shared by reference * (not deep-copied), since the rendering phase only reads from it. @@ -127,53 +120,22 @@ public static GenerationTargetClassDto toRenderingDto(BuilderDefinitionDto build * @param classMethod the generation DTO to map * @return a new {@link MethodDto} with all rendering fields copied */ - public static MethodDto toMethodDto(BuilderMethodDto classMethod) { - return toMethodDto(classMethod, null); - } - - /** - * Maps a {@link BuilderMethodDto} to a {@link MethodDto}, enriching the javadoc with field-origin - * information including the source class name. - * - *

    All rendering-relevant fields are copied. The {@code MethodCodeDto} is shared by reference - * (not deep-copied), since the rendering phase only reads from it. - * - * @param classMethod the generation DTO to map - * @param sourceClassName the simple class name of the source DTO (e.g., "PersonDto"), or null if - * unknown - * @return a new {@link MethodDto} with all rendering fields copied - */ - private static MethodDto toMethodDto(BuilderMethodDto classMethod, String sourceClassName) { + private static MethodDto toMethodDto(BuilderMethodDto classMethod) { MethodDto method = new MethodDto(classMethod.getMethodName(), classMethod.getReturnType()); method.setModifier(classMethod.getModifier().orElse(null)); method.setStatic(classMethod.isStatic()); - method.setPriority(classMethod.getPriority()); method.setOrdering(classMethod.getOrdering()); - // Enrich javadoc with field-origin section if source field is known + // Enrich javadoc with pre-built source description if source field is known JavadocDto javadoc = classMethod.getJavadoc(); if (javadoc == null && StringUtils.isNotBlank(classMethod.getSourceFieldName())) { javadoc = new JavadocDto(); } if (javadoc != null && StringUtils.isNotBlank(classMethod.getSourceFieldName())) { - String originType = classMethod.isConstructorField() ? "parameter in constructor" : "setter"; - String displaySignature = classMethod.getSourceMethodSignature(); - String linkSignature = classMethod.getSourceMethodLinkSignature(); - if (StringUtils.isBlank(displaySignature)) { - displaySignature = classMethod.getSourceFieldName(); - } - if (StringUtils.isBlank(linkSignature)) { - linkSignature = displaySignature; - } - String originLine; - if (StringUtils.isNotBlank(sourceClassName)) { - originLine = - "

    Generated from %s {@link %s#%s %s}" - .formatted(originType, sourceClassName, linkSignature, displaySignature); - } else { - originLine = "

    Generated from %s %s".formatted(originType, displaySignature); + String sourceDescription = classMethod.getSourceDescription(); + if (sourceDescription != null) { + javadoc.appendDescriptionLine(sourceDescription); } - javadoc.appendDescriptionLine(originLine); } method.setJavadoc(javadoc); classMethod.getAnnotations().forEach(method::addAnnotation); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/FieldDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/FieldDto.java index eebd1626..d5107f15 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/FieldDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/FieldDto.java @@ -82,20 +82,6 @@ public class FieldDto { */ private final List parameterAnnotations = new ArrayList<>(); - /** - * Source method signature for javadoc enrichment, e.g. {@code setTeamname(String teamName)} for - * setter fields or {@code PersonDto(String name, int age, ...)} for constructor parameters. This - * is the display label shown in the {@code {@link}} tag. {@code null} if not yet computed. - */ - private String sourceMethodSignature; - - /** - * Types-only signature for the {@code {@link}} target, e.g. {@code setTeamname(String)} or {@code - * PersonDto(String, int)}. Uses raw types (no generics) per Javadoc spec. {@code null} if not yet - * computed. - */ - private String sourceMethodLinkSignature; - /** * Gets the original field name from the DTO. This name is used for generating method names, * parameter names, and setter method names (e.g., "userName" becomes "setUserName"). @@ -306,42 +292,6 @@ public void setParameterAnnotations(List annotations) { } } - /** - * Returns the source method signature for javadoc enrichment (display label). - * - * @return the source method signature, or {@code null} if not yet computed - */ - public String getSourceMethodSignature() { - return sourceMethodSignature; - } - - /** - * Sets the source method signature for javadoc enrichment (display label). - * - * @param sourceMethodSignature the display signature, e.g. {@code setTeamname(String teamName)} - */ - public void setSourceMethodSignature(String sourceMethodSignature) { - this.sourceMethodSignature = sourceMethodSignature; - } - - /** - * Returns the types-only link signature for the {@code {@link}} target. - * - * @return the link signature, or {@code null} if not yet computed - */ - public String getSourceMethodLinkSignature() { - return sourceMethodLinkSignature; - } - - /** - * Sets the types-only link signature for the {@code {@link}} target. - * - * @param sourceMethodLinkSignature the link signature, e.g. {@code setTeamname(String)} - */ - public void setSourceMethodLinkSignature(String sourceMethodLinkSignature) { - this.sourceMethodLinkSignature = sourceMethodLinkSignature; - } - /** * Checks if this field has a parameter annotation with the given fully qualified name. * diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/BuilderMethodDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/BuilderMethodDto.java index 969307a4..edc7c143 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/BuilderMethodDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/BuilderMethodDto.java @@ -97,18 +97,10 @@ public class BuilderMethodDto { private String sourceFieldName; /** - * Source method signature for javadoc enrichment, e.g. {@code setTeamname(String teamName)} for - * setter fields or {@code PersonDto(String name, int age, ...)} for constructor parameters. - * {@code null} for enhancer methods. + * Pre-built javadoc origin line for this method, e.g. {@code

    Generated from setter {@link + * PersonDto#setAuthor(String) setAuthor(String author)}}. {@code null} for enhancer methods. */ - private String sourceMethodSignature; - - /** - * Types-only signature for the {@code {@link}} target, e.g. {@code setTeamname(String)} or {@code - * PersonDto(String, int)}. Uses raw types (no generics) per Javadoc spec. {@code null} for - * enhancer methods. - */ - private String sourceMethodLinkSignature; + private String sourceDescription; /** Whether this method was generated for a constructor field (vs a setter field). */ private boolean constructorField; @@ -445,42 +437,22 @@ public void setSourceFieldName(String sourceFieldName) { } /** - * Returns the source method signature for javadoc enrichment. - * - * @return the source method signature, or {@code null} for enhancer-generated methods - */ - public String getSourceMethodSignature() { - return sourceMethodSignature; - } - - /** - * Sets the source method signature for javadoc enrichment. + * Gets the pre-built javadoc origin line for this method. * - * @param sourceMethodSignature the source method signature, e.g. {@code setTeamname(String - * teamName)} for setters or {@code PersonDto(String name, int age, ...)} for constructor - * parameters + * @return the source description line, or {@code null} for enhancer-generated methods */ - public void setSourceMethodSignature(String sourceMethodSignature) { - this.sourceMethodSignature = sourceMethodSignature; + public String getSourceDescription() { + return sourceDescription; } /** - * Returns the types-only link signature for the {@code {@link}} target. + * Sets the pre-built javadoc origin line for this method. * - * @return the link signature, or {@code null} for enhancer-generated methods + * @param sourceDescription the full origin line, e.g. {@code

    Generated from setter {@link + * PersonDto#setAuthor(String) setAuthor(String author)}} */ - public String getSourceMethodLinkSignature() { - return sourceMethodLinkSignature; - } - - /** - * Sets the types-only link signature for the {@code {@link}} target. - * - * @param sourceMethodLinkSignature the link signature, e.g. {@code setTeamname(String)} or {@code - * PersonDto(String, int)} - */ - public void setSourceMethodLinkSignature(String sourceMethodLinkSignature) { - this.sourceMethodLinkSignature = sourceMethodLinkSignature; + public void setSourceDescription(String sourceDescription) { + this.sourceDescription = sourceDescription; } /** @@ -504,9 +476,12 @@ public void setConstructorField(boolean constructorField) { /** * Comparator for sorting BuilderMethodDto instances with sophisticated ordering rules. * - *

    Sorting order for methods with same priority and name: + *

    Sorting order: * *

      + *
    1. Priority (descending — higher priority first) + *
    2. Ordering value (ascending — lower ordering first) + *
    3. Method name *
    4. Methods with fewer parameters come first *
    5. Non-generic methods come before generic methods *
    6. Full method signature (name(paramType1,paramType2,...)) used for final ordering @@ -516,25 +491,31 @@ public static class BuilderMethodComparator implements java.util.Comparator modifier = Optional.empty(); /** Whether the method is static. */ private boolean isStatic = false; - /** Priority for method conflict resolution. Higher wins. */ - private int priority = 0; - /** Ordering for method generation. Lower values appear first in generated class. */ private int ordering = 1000; @@ -98,34 +89,6 @@ public MethodDto(String methodName, TypeName returnType) { this.returnType = returnType; } - /** - * Sets the priority for this method. Higher values win when signatures clash. - * - *

      Priority levels: - * - *

        - *
      • {@link #PRIORITY_HIGHEST} (100) - *
      • {@link #PRIORITY_HIGH} (80) - *
      • {@link #PRIORITY_MEDIUM} (70) - *
      • {@link #PRIORITY_LOW} (60) - *
      • 0: Default (no priority set) - *
      - * - * @param priority the priority value (higher values take precedence in conflicts) - */ - public void setPriority(int priority) { - this.priority = priority; - } - - /** - * Returns the priority of this method for conflict resolution. - * - * @return the priority value - */ - public int getPriority() { - return priority; - } - /** * Sets the ordering for this method. * @@ -368,7 +331,7 @@ public void addAnnotation(AnnotationDto annotation) { /** * Comparator for sorting MethodDto instances with sophisticated ordering rules. * - *

      Sorting order for methods with same priority and name: + *

      Sorting order for methods with same ordering and name: * *

        *
      1. Methods with fewer parameters come first diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java index 83354797..ae56959f 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java @@ -116,6 +116,7 @@ public static BuilderDefinitionDto extractFromElement( *
          *
        • Converts FieldDto instances to ClassFieldDto instances *
        • Sets origin info (sourceFieldName, constructorField) on each BuilderMethodDto + *
        • Performs pre-conflict resolution at BuilderMethodDto level with origin logging *
        • Maps all BuilderMethodDto to MethodDto via BuilderToGenerationTypeMapper *
        • Collects all mapped methods from fields and class-level enhancer methods *
        • Sets class access modifier @@ -135,79 +136,250 @@ private static void finalizeDefinition( builderDto.addClassField(classField); } - // 2. Set origin info on FieldDto (primary source of truth), then propagate to methods - // For constructor parameters, build the full constructor signature once: - // Display: ClassName(type1 param1, type2 param2, ...) - // Link: ClassName(Type1, Type2, ...) (types only, raw erasure for {@link} target) - String constructorSignature = null; - String constructorLinkSignature = null; - if (!builderDto.getConstructorFieldsForBuilder().isEmpty() - && builderDto.getBuildingTargetTypeName() != null) { - StringBuilder displaySb = new StringBuilder(); - StringBuilder linkSb = new StringBuilder(); - displaySb.append(builderDto.getBuildingTargetTypeName().getClassName()).append("("); - linkSb.append(builderDto.getBuildingTargetTypeName().getClassName()).append("("); - boolean first = true; - for (FieldDto field : builderDto.getConstructorFieldsForBuilder()) { - if (!first) { - displaySb.append(", "); - linkSb.append(", "); - } - displaySb - .append(field.getFieldType().getSimpleNameWithGenerics()) - .append(" ") - .append(field.getOriginalFieldName()); - linkSb.append(field.getFieldType().getClassName()); - first = false; - } - displaySb.append(")"); - linkSb.append(")"); - constructorSignature = displaySb.toString(); - constructorLinkSignature = linkSb.toString(); - } + // 2. Set origin info on BuilderMethodDto for javadoc enrichment + setConstructorOriginInfo(builderDto); + addSetterOriginInfo(builderDto); + + // 3. Builder-specific conflict resolution: resolve by priority, log with field origin + resolveMethodConflicts(builderDto, context); + + // 4. Set class access modifier + builderDto.setClassAccessModifier(builderDto.getConfiguration().getBuilderAccess()); + + // 5. Set static imports for TrackedValue + builderDto.addStaticImport(TrackedValue.class, "changedValue"); + builderDto.addStaticImport(TrackedValue.class, "initialValue"); + builderDto.addStaticImport(TrackedValue.class, "unsetValue"); + + context.debugEndOperation( + "Finalized: %d class fields, %d builder-level methods, %d constructors", + builderDto.getClassFields().size(), + builderDto.getMethods().size(), + builderDto.getConstructors().size()); + } + + /** + * Sets origin info (source field name, source description, constructor flag) on all {@link + * BuilderMethodDto}s associated with constructor fields. + * + *

          For constructor parameters, the full constructor signature is built once and shared across + * all constructor-field methods. The origin line includes an {@code {@link}} tag with types-only + * link target and full display label. + * + * @param builderDto the builder definition containing constructor fields + */ + private static void setConstructorOriginInfo(BuilderDefinitionDto builderDto) { + String sourceDescription = buildConstructorSourceDescription(builderDto); for (FieldDto field : builderDto.getConstructorFieldsForBuilder()) { - field.setSourceMethodSignature(constructorSignature); - field.setSourceMethodLinkSignature(constructorLinkSignature); for (BuilderMethodDto method : field.getMethods()) { method.setSourceFieldName(field.getOriginalFieldName()); - method.setSourceMethodSignature(field.getSourceMethodSignature()); - method.setSourceMethodLinkSignature(field.getSourceMethodLinkSignature()); + method.setSourceDescription(sourceDescription); method.setConstructorField(true); } } + } + + /** + * Sets origin info (source field name, source description, constructor flag) on all {@link + * BuilderMethodDto}s associated with setter fields. + * + *

          For each setter field, the signature is built per-field and the origin line includes an + * {@code {@link}} tag with types-only link target and full display label. + * + * @param builderDto the builder definition containing setter fields + */ + private static void addSetterOriginInfo(BuilderDefinitionDto builderDto) { + String sourceClassName = + builderDto.getBuildingTargetTypeName() != null + ? builderDto.getBuildingTargetTypeName().getClassName() + : null; for (FieldDto field : builderDto.getSetterFieldsForBuilder()) { - String displaySignature = - field.getSetterName() - + "(" - + field.getFieldType().getSimpleNameWithGenerics() - + " " - + field.getOriginalFieldName() - + ")"; - String linkSignature = - field.getSetterName() + "(" + field.getFieldType().getClassName() + ")"; - field.setSourceMethodSignature(displaySignature); - field.setSourceMethodLinkSignature(linkSignature); + String sourceDescription = buildSetterSourceDescription(field, sourceClassName); for (BuilderMethodDto method : field.getMethods()) { method.setSourceFieldName(field.getOriginalFieldName()); - method.setSourceMethodSignature(field.getSourceMethodSignature()); - method.setSourceMethodLinkSignature(field.getSourceMethodLinkSignature()); + method.setSourceDescription(sourceDescription); method.setConstructorField(false); } } + } - // 3. Set class access modifier - builderDto.setClassAccessModifier(builderDto.getConfiguration().getBuilderAccess()); + /** + * Builds the javadoc origin line for constructor-based methods. + * + *

          The full constructor signature is built from all constructor fields, e.g. {@code + *

          Generated from parameter in constructor {@link PersonDto#PersonDto(String, int) + * PersonDto(String name, int age)}}. + * + * @param builderDto the builder definition containing constructor fields and target type name + * @return the complete origin line, or {@code null} if no constructor fields or target type + */ + private static String buildConstructorSourceDescription(BuilderDefinitionDto builderDto) { + if (builderDto.getConstructorFieldsForBuilder().isEmpty() + || builderDto.getBuildingTargetTypeName() == null) { + return null; + } + String className = builderDto.getBuildingTargetTypeName().getClassName(); + String displayParams = + builderDto.getConstructorFieldsForBuilder().stream() + .map( + f -> + "%s %s" + .formatted( + f.getFieldType().getSimpleNameWithGenerics(), f.getOriginalFieldName())) + .collect(java.util.stream.Collectors.joining(", ")); + String linkParams = + builderDto.getConstructorFieldsForBuilder().stream() + .map(f -> f.getFieldType().getClassName()) + .collect(java.util.stream.Collectors.joining(", ")); + return "

          Generated from parameter in constructor {@link %s#%s(%s) %s(%s)}" + .formatted(className, className, linkParams, className, displayParams); + } - // 4. Set static imports for TrackedValue - builderDto.addStaticImport(TrackedValue.class, "changedValue"); - builderDto.addStaticImport(TrackedValue.class, "initialValue"); - builderDto.addStaticImport(TrackedValue.class, "unsetValue"); + /** + * Builds the javadoc origin line for setter-based methods. + * + *

          The signature is built from the field's setter name and type, e.g. {@code

          Generated from + * setter {@link PersonDto#setAuthor(String) setAuthor(String author)}}. + * + * @param field the setter field to build the description for + * @param sourceClassName the simple class name of the source DTO, or {@code null} if unknown + * @return the complete origin line + */ + private static String buildSetterSourceDescription(FieldDto field, String sourceClassName) { + String displaySignature = + "%s(%s %s)" + .formatted( + field.getSetterName(), + field.getFieldType().getSimpleNameWithGenerics(), + field.getOriginalFieldName()); + String linkSignature = + "%s(%s)".formatted(field.getSetterName(), field.getFieldType().getClassName()); + if (sourceClassName != null) { + return "

          Generated from setter {@link %s#%s %s}" + .formatted(sourceClassName, linkSignature, displaySignature); + } else { + return "

          Generated from setter %s".formatted(displaySignature); + } + } + + /** + * Builder-specific conflict resolution at the BuilderMethodDto level, using priority-based + * resolution with field-origin logging. + * + *

          This is the primary place for conflict resolution because only here the field-origin + * metadata ({@code sourceFieldName}, {@code constructorField}) is available. Methods with the + * same signature key are resolved by keeping the highest-priority one; losers are removed from + * their field/class method lists so they are never mapped to the rendering DTO. + * + *

          A generic safety net in {@link + * org.javahelpers.simple.builders.processor.classgen.roaster.RoasterCodeGenerator#resolveMethodConflicts} + * handles any remaining duplicates (e.g., from enhancer methods added after this step) by keeping + * the first occurrence. + * + * @param builderDto the builder definition containing all methods + * @param context the processing context for logging + */ + private static void resolveMethodConflicts( + BuilderDefinitionDto builderDto, ProcessingContext context) { + context.debugStartOperation("Resolving method conflicts"); + + // Collect all BuilderMethodDto instances grouped by signature key + java.util.Map> methodsBySignature = + collectMethodsBySignature(builderDto); + + // Resolve conflicts: keep highest priority, remove losers + java.util.Set methodsToRemove = new java.util.HashSet<>(); + for (java.util.Map.Entry> entry : + methodsBySignature.entrySet()) { + List methodsWithSameSignature = entry.getValue(); + boolean isConflicting = methodsWithSameSignature.size() > 1; + if (isConflicting) { + // Sort by priority (descending), then ordering, then name, etc. for deterministic winner + methodsWithSameSignature.sort(new BuilderMethodDto.BuilderMethodComparator()); + BuilderMethodDto winner = methodsWithSameSignature.get(0); + context.warning( + "Method conflict resolved for signature '%s': %d methods found. " + + "Kept: priority=%d, sourceField='%s', constructorField=%b. " + + "Dropped: %s", + entry.getKey(), + methodsWithSameSignature.size(), + winner.getPriority(), + winner.getSourceFieldName(), + winner.isConstructorField(), + methodsWithSameSignature.stream() + .skip(1) + .map( + m -> + String.format( + "[priority=%d, sourceField='%s', constructorField=%b]", + m.getPriority(), m.getSourceFieldName(), m.isConstructorField())) + .toList()); + // Mark losers for removal + methodsWithSameSignature.stream().skip(1).forEach(methodsToRemove::add); + } + } + + // Remove losing methods from their field/class method lists + removeMethodsFromBuilder(builderDto, methodsToRemove); context.debugEndOperation( - "Finalized: %d class fields, %d builder-level methods, %d constructors", - builderDto.getClassFields().size(), - builderDto.getMethods().size(), - builderDto.getConstructors().size()); + "Resolved: %d signatures, %d conflicts, %d methods removed", + methodsBySignature.size(), + methodsBySignature.values().stream().filter(l -> l.size() > 1).count(), + methodsToRemove.size()); + } + + /** + * Collects all {@link BuilderMethodDto} instances from the builder definition, grouped by their + * signature key. This includes methods from constructor fields, setter fields, and builder-level + * enhancer methods. + * + * @param builderDto the builder definition containing all methods + * @return map from signature key to list of methods with that signature + */ + private static java.util.Map> collectMethodsBySignature( + BuilderDefinitionDto builderDto) { + java.util.Map> methodsBySignature = new HashMap<>(); + + for (FieldDto field : builderDto.getConstructorFieldsForBuilder()) { + for (BuilderMethodDto method : field.getMethods()) { + methodsBySignature + .computeIfAbsent(method.getSignatureKey(), k -> new java.util.ArrayList<>()) + .add(method); + } + } + for (FieldDto field : builderDto.getSetterFieldsForBuilder()) { + for (BuilderMethodDto method : field.getMethods()) { + methodsBySignature + .computeIfAbsent(method.getSignatureKey(), k -> new java.util.ArrayList<>()) + .add(method); + } + } + for (BuilderMethodDto classMethod : builderDto.getMethods()) { + methodsBySignature + .computeIfAbsent(classMethod.getSignatureKey(), k -> new java.util.ArrayList<>()) + .add(classMethod); + } + + return methodsBySignature; + } + + /** + * Removes the given methods from all field method lists and builder-level methods in the builder + * definition. + * + * @param builderDto the builder definition containing all methods + * @param methodsToRemove the set of methods to remove + */ + private static void removeMethodsFromBuilder( + BuilderDefinitionDto builderDto, java.util.Set methodsToRemove) { + for (FieldDto field : builderDto.getConstructorFieldsForBuilder()) { + field.getMethods().removeAll(methodsToRemove); + } + for (FieldDto field : builderDto.getSetterFieldsForBuilder()) { + field.getMethods().removeAll(methodsToRemove); + } + builderDto.getMethods().removeAll(methodsToRemove); } /** From 8cbd565f58967cac631a4dda3d2fec10ffb2b092 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Tue, 4 Aug 2026 22:40:38 +0200 Subject: [PATCH 12/23] Implementation of Default-Value support --- README.md | 45 ++++++++ .../builders/core/annotations/Default.java | 104 ++++++++++++++++++ .../builders/core/util/TrackedValue.java | 48 +++++++- .../builders/core/util/TrackedValueTest.java | 91 +++++++++++++++ .../analysis/FieldAnnotationExtractor.java | 99 +++++++++++++++++ .../builder/CoreMethodsEnhancer.java | 27 +++-- .../processor/model/core/FieldDto.java | 39 +++++++ .../processing/BuilderDefinitionCreator.java | 39 +++++++ 8 files changed, 482 insertions(+), 10 deletions(-) create mode 100644 core/src/main/java/org/javahelpers/simple/builders/core/annotations/Default.java diff --git a/README.md b/README.md index 00d55078..e503d088 100644 --- a/README.md +++ b/README.md @@ -166,6 +166,50 @@ User user = UserBuilder.create() This ensures validation frameworks work seamlessly with builder-generated objects. +#### Default Values + +Specify default values for fields that are applied when not explicitly set before `build()`: + +```java +import org.javahelpers.simple.builders.core.annotations.Default; + +@SimpleBuilder +public record Product( + String name, + double price, + @Default("GENERAL") String category, + @Default("true") boolean active +) {} + +// category defaults to "GENERAL", active defaults to true +Product product = ProductBuilder.create() + .name("Laptop") + .price(1500.0) + .build(); +// product.category() == "GENERAL" +// product.active() == true + +// Explicit values override defaults +Product custom = ProductBuilder.create() + .name("Widget") + .price(9.99) + .category("ACCESSORIES") + .active(false) + .build(); +// custom.category() == "ACCESSORIES" +``` + +The `@Default` annotation works on both **constructor parameters** (records) and **fields** (classes with setters). The `value()` is a string expression interpreted based on the field type: + +- **String** — wrapped in double quotes automatically (e.g. `@Default("GENERAL")` generates `"GENERAL"`) +- **char** — wrapped in single quotes (e.g. `@Default("A")` generates `'A'`) +- **numeric/boolean primitives** — used as-is (e.g. `@Default("0.0")` generates `0.0`) +- **complex types** — used as a raw Java expression (e.g. `@Default("List.of()")` generates `List.of()`) + +**Framework-agnostic detection:** The processor also detects annotations named `Default` or `DefaultValue` from any package (e.g. Jakarta REST `@DefaultValue`) if they have a `String value()` member. + +**Interaction with non-null checks:** A field with a `@Default` is never considered "required" — even if annotated with `@NotNull`, no validation error is raised when the field is unset. + #### Conditional Builder Logic Apply builder modifications conditionally using the `conditional()` method: @@ -334,6 +378,7 @@ 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 +- **Default Values**: [`ProductWithDefaults.java`](example/src/main/java/org/javahelpers/simple/builders/example/ProductWithDefaults.java) (record) and [`OrderWithDefaults.java`](example/src/main/java/org/javahelpers/simple/builders/example/OrderWithDefaults.java) (class) - Demonstrate `@Default` annotation for unset builder fields These examples serve as both documentation and integration tests for the annotation processor. diff --git a/core/src/main/java/org/javahelpers/simple/builders/core/annotations/Default.java b/core/src/main/java/org/javahelpers/simple/builders/core/annotations/Default.java new file mode 100644 index 00000000..d874189b --- /dev/null +++ b/core/src/main/java/org/javahelpers/simple/builders/core/annotations/Default.java @@ -0,0 +1,104 @@ +/* + * MIT License + * + * Copyright (c) 2026 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.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Specifies a default value for a builder field that is applied when the field is not explicitly + * set before calling {@code build()}. + * + *

          The {@link #value()} is a string expression that is interpreted based on the field type: + * + *

            + *
          • String — wrapped in double quotes, e.g. {@code @Default("GENERAL")} generates {@code + * "GENERAL"} + *
          • char — wrapped in single quotes, e.g. {@code @Default("A")} generates {@code 'A'} + *
          • numeric/boolean primitives — used as-is, e.g. {@code @Default("0.0")} generates + * {@code 0.0} + *
          • complex types — used as a raw Java expression, e.g. {@code @Default("List.of()")} + * generates {@code List.of()} + *
          + * + *

          Can be placed on constructor parameters or fields. When a field has a default value, it is no + * longer considered "required" even if annotated with {@code @NotNull} or {@code @NonNull}. + * + *

          Example with a record: + * + *

          {@code
          + * @SimpleBuilder
          + * public record Product(String name, double price,
          + *     @Default("GENERAL") String category) {}
          + *
          + * // category defaults to "GENERAL" if not set
          + * Product p = ProductBuilder.create()
          + *     .name("Widget")
          + *     .price(9.99)
          + *     .build();
          + * // p.category() == "GENERAL"
          + * }
          + * + *

          Example with a class: + * + *

          {@code
          + * @SimpleBuilder
          + * public class Order {
          + *   private String id;
          + *   @Default("PENDING") private String status;
          + *
          + *   public String getId() { return id; }
          + *   public void setId(String id) { this.id = id; }
          + *   public String getStatus() { return status; }
          + *   public void setStatus(String status) { this.status = status; }
          + * }
          + *
          + * // status defaults to "PENDING" if not set
          + * Order o = OrderBuilder.create()
          + *     .id("ORD-001")
          + *     .build();
          + * // o.getStatus() == "PENDING"
          + * }
          + * + *

          Framework-agnostic detection: The builder processor also detects annotations named + * {@code Default} or {@code DefaultValue} from any package (e.g. Jakarta REST {@code + * jakarta.ws.rs.DefaultValue}) if they have a {@code String value()} member. + */ +@Target({ElementType.PARAMETER, ElementType.FIELD}) +@Retention(RetentionPolicy.CLASS) +public @interface Default { + + /** + * The default value as a string expression, interpreted based on the field type. + * + *

          For String fields, the value is quoted automatically. For char fields, it is single-quoted. + * For numeric/boolean primitives and complex types, it is used as a raw Java expression. + * + * @return the default value expression + */ + String value(); +} diff --git a/core/src/main/java/org/javahelpers/simple/builders/core/util/TrackedValue.java b/core/src/main/java/org/javahelpers/simple/builders/core/util/TrackedValue.java index dca9ba98..0aa6de25 100644 --- a/core/src/main/java/org/javahelpers/simple/builders/core/util/TrackedValue.java +++ b/core/src/main/java/org/javahelpers/simple/builders/core/util/TrackedValue.java @@ -56,16 +56,60 @@ public boolean isSet() { return isChanged || isInitial; } + /** + * Returns the value if set, otherwise the given default. + * + * @param defaultValue the default to return if unset + * @return the value if set, otherwise the default + */ + public T valueOr(T defaultValue) { + return isSet() ? value : defaultValue; + } + /** * Executes the provided {@link Consumer} only if this value has been set (initial value or - * changed). + * changed). Returns a {@link DefaultValueApplier} that can fluently provide a default via {@code + * .orElse(default)} if the value was unset. + * + *

          Existing code that ignores the return value (e.g. {@code tracked.ifSet(consumer);}) + * continues to work unchanged. * * @param consumer action to perform with the current {@link #value()} + * @return a {@link DefaultValueApplier} for fluent default handling */ - public void ifSet(Consumer consumer) { + public DefaultValueApplier ifSet(Consumer consumer) { if (isSet()) { consumer.accept(value); } + return new DefaultValueApplier<>(isSet(), consumer); + } + + /** + * Intermediate result returned by {@link #ifSet(Consumer)} to support fluent default-value + * application via {@code .orElse(default)}. + * + *

          When {@link #ifSet(Consumer)} is called on a set {@link TrackedValue}, the consumer is + * invoked immediately and {@code alreadyApplied} is {@code true}, making the subsequent {@link + * #orElse(Object)} call a no-op. When called on an unset value, the consumer is not invoked and + * {@code alreadyApplied} is {@code false}, so {@link #orElse(Object)} applies the default value + * to the same consumer. + * + * @param the value type + * @param alreadyApplied whether the consumer has already been invoked + * @param consumer the consumer to apply the default value to if not already applied + */ + public record DefaultValueApplier(boolean alreadyApplied, Consumer consumer) { + + /** + * Applies the given default value to the consumer if the original value was unset. + * + * @param defaultValue the default value to apply + */ + public void orElse(T defaultValue) { + if (!alreadyApplied) { + consumer.accept(defaultValue); + } + } } /** diff --git a/core/src/test/java/org/javahelpers/simple/builders/core/util/TrackedValueTest.java b/core/src/test/java/org/javahelpers/simple/builders/core/util/TrackedValueTest.java index 124f8b4f..0b7eccfb 100644 --- a/core/src/test/java/org/javahelpers/simple/builders/core/util/TrackedValueTest.java +++ b/core/src/test/java/org/javahelpers/simple/builders/core/util/TrackedValueTest.java @@ -207,4 +207,95 @@ void shouldWorkWithComplexTypes() { assertTrue(intTracked.isSet()); assertTrue(doubleTracked.isSet()); } + + @Test + void valueOr_returnsValueWhenSet() { + TrackedValue tracked = TrackedValue.changedValue("actual"); + assertEquals("actual", tracked.valueOr("default")); + } + + @Test + void valueOr_returnsValueWhenInitial() { + TrackedValue tracked = TrackedValue.initialValue("initial"); + assertEquals("initial", tracked.valueOr("default")); + } + + @Test + void valueOr_returnsDefaultWhenUnset() { + TrackedValue tracked = TrackedValue.unsetValue(); + assertEquals("default", tracked.valueOr("default")); + } + + @Test + void valueOr_returnsNullDefaultWhenUnset() { + TrackedValue tracked = TrackedValue.unsetValue(); + assertNull(tracked.valueOr(null)); + } + + @Test + void valueOr_worksWithPrimitives() { + TrackedValue tracked = TrackedValue.unsetValue(); + assertEquals(42, tracked.valueOr(42)); + } + + @Test + void ifSet_orElse_appliesValueWhenChanged() { + TrackedValue tracked = TrackedValue.changedValue("actual"); + AtomicReference received = new AtomicReference<>("unchanged"); + + tracked.ifSet(received::set).orElse("default"); + + assertEquals("actual", received.get()); + } + + @Test + void ifSet_orElse_appliesValueWhenInitial() { + TrackedValue tracked = TrackedValue.initialValue("initial"); + AtomicReference received = new AtomicReference<>("unchanged"); + + tracked.ifSet(received::set).orElse("default"); + + assertEquals("initial", received.get()); + } + + @Test + void ifSet_orElse_appliesDefaultWhenUnset() { + TrackedValue tracked = TrackedValue.unsetValue(); + AtomicReference received = new AtomicReference<>("unchanged"); + + tracked.ifSet(received::set).orElse("default"); + + assertEquals("default", received.get()); + } + + @Test + void ifSet_orElse_appliesNullDefaultWhenUnset() { + TrackedValue tracked = TrackedValue.unsetValue(); + AtomicReference received = new AtomicReference<>("unchanged"); + + tracked.ifSet(received::set).orElse(null); + + assertNull(received.get()); + } + + @Test + void ifSet_returnValueCanBeIgnored() { + TrackedValue tracked = TrackedValue.changedValue("actual"); + AtomicReference received = new AtomicReference<>("unchanged"); + + // Simulate existing generated code that ignores the return value + tracked.ifSet(received::set); + + assertEquals("actual", received.get()); + } + + @Test + void ifSet_orElse_appliesPrimitiveDefaultWhenUnset() { + TrackedValue tracked = TrackedValue.unsetValue(); + AtomicReference received = new AtomicReference<>(0); + + tracked.ifSet(received::set).orElse(42); + + assertEquals(42, received.get()); + } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/FieldAnnotationExtractor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/FieldAnnotationExtractor.java index 82904bf8..9bf6278e 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/FieldAnnotationExtractor.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/FieldAnnotationExtractor.java @@ -37,6 +37,7 @@ import javax.lang.model.type.DeclaredType; import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; +import org.javahelpers.simple.builders.processor.model.type.TypeNamePrimitive; import org.javahelpers.simple.builders.processor.processing.ProcessingContext; /** Extractor for field annotations, converting them from Java model elements to DTOs. */ @@ -260,4 +261,102 @@ public static boolean hasNonNullConstraint(VariableElement param) { private static boolean isNonNullAnnotation(String simpleName) { return "NotNull".equals(simpleName) || "NonNull".equals(simpleName); } + + /** + * Extracts a default value expression from annotations named {@code Default} or {@code + * DefaultValue} on the given parameter, regardless of package. + * + *

          Detects annotations from any package (e.g. {@code + * org.javahelpers.simple.builders.core.annotations.Default}, {@code jakarta.ws.rs.DefaultValue}) + * as long as they have a {@code String value()} member. + * + * @param param the parameter element to check + * @return an {@link Optional} containing the raw default value string, or empty if no default + * annotation is present + */ + public static Optional extractDefaultValue(VariableElement param) { + return param.getAnnotationMirrors().stream() + .filter(FieldAnnotationExtractor::isDefaultAnnotation) + .map(FieldAnnotationExtractor::getValueMember) + .filter(Optional::isPresent) + .map(Optional::get) + .findFirst(); + } + + /** + * Checks if the given annotation mirror represents a default-value annotation (named {@code + * Default} or {@code DefaultValue} from any package). + * + * @param mirror the annotation mirror to check + * @return {@code true} if the annotation is a recognized default-value annotation + */ + private static boolean isDefaultAnnotation(AnnotationMirror mirror) { + if (!(mirror.getAnnotationType().asElement() instanceof TypeElement type)) { + return false; + } + String name = type.getSimpleName().toString(); + return "Default".equals(name) || "DefaultValue".equals(name); + } + + /** + * Extracts the {@code value()} member from an annotation mirror as a string. + * + * @param mirror the annotation mirror to extract from + * @return an {@link Optional} containing the value string, or empty if no {@code value()} member + * is present + */ + private static Optional getValueMember(AnnotationMirror mirror) { + return mirror.getElementValues().entrySet().stream() + .filter(e -> "value".equals(e.getKey().getSimpleName().toString())) + .map(e -> e.getValue().getValue().toString()) + .findFirst(); + } + + /** + * Formats a raw string default value as a Java expression based on the field type. + * + *

          Interpretation rules: + * + *

            + *
          • {@code String} — wrapped in double quotes, e.g. {@code "GENERAL"} + *
          • {@code char} — wrapped in single quotes, e.g. {@code 'A'} + *
          • numeric/boolean primitives — used as-is, e.g. {@code 0.0}, {@code true} + *
          • complex types (List, custom objects) — used as a raw Java expression, e.g. {@code + * List.of()} + *
          + * + * @param rawValue the raw string from the annotation's {@code value()} member + * @param fieldType the {@link TypeName} of the target field + * @return a formatted Java expression string suitable for code generation + */ + public static String formatDefaultExpression(String rawValue, TypeName fieldType) { + if (isStringType(fieldType)) { + return "\"%s\"".formatted(rawValue); + } + if (isCharType(fieldType)) { + return "'%s'".formatted(rawValue); + } + return rawValue; + } + + /** + * Checks if the given type is {@code java.lang.String}. + * + * @param type the type to check + * @return {@code true} if the type is String + */ + private static boolean isStringType(TypeName type) { + return "java.lang".equals(type.getPackageName()) && "String".equals(type.getClassName()); + } + + /** + * Checks if the given type is the {@code char} primitive. + * + * @param type the type to check + * @return {@code true} if the type is char + */ + private static boolean isCharType(TypeName type) { + return type instanceof TypeNamePrimitive primitive + && primitive.getType() == TypeNamePrimitive.PrimitiveTypeEnum.CHAR; + } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java index 65ce8500..84cbf089 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java @@ -133,9 +133,9 @@ protected BuilderMethodDto createBuildMethod(BuilderDefinitionDto builderDto) { // Create method implementation with validation and setter application StringBuilder code = new StringBuilder(); - // Add validation for non-nullable constructor fields + // Add validation for required constructor fields (non-nullable AND no default) for (var field : builderDto.getConstructorFieldsForBuilder()) { - if (field.isNonNullable()) { + if (field.isRequired()) { code.append("if (!this.") .append(field.getFieldNameInBuilder()) .append(".isSet()) {\n") @@ -153,12 +153,12 @@ protected BuilderMethodDto createBuildMethod(BuilderDefinitionDto builderDto) { } } - // Add validation for non-nullable setter fields + // Add validation for required setter fields (non-nullable AND no default) // Note: Primitives are stored as boxed types in TrackedValue, etc. // They can be null via Supplier methods: builder.pages(() -> null) - // So we need null checks for ALL non-nullable fields, including primitives + // So we need null checks for ALL required fields, including primitives for (var field : builderDto.getSetterFieldsForBuilder()) { - if (field.isNonNullable()) { + if (field.isRequired()) { code.append("if (this.") .append(field.getFieldNameInBuilder()) .append(".isSet() && this.") @@ -183,13 +183,17 @@ protected BuilderMethodDto createBuildMethod(BuilderDefinitionDto builderDto) { .append(");\n"); } - // Apply setter-based fields + // Apply setter-based fields, using default value if declared and field is unset for (var field : builderDto.getSetterFieldsForBuilder()) { code.append("this.") .append(field.getFieldNameInBuilder()) .append(".ifSet(result::") .append(field.getSetterName()) - .append(");\n"); + .append(")"); + if (field.getDefaultValue().isPresent()) { + code.append(".orElse(").append(field.getDefaultValue().get()).append(")"); + } + code.append(";\n"); } code.append("return result;"); @@ -292,7 +296,14 @@ protected BuilderMethodDto createToStringMethod(BuilderDefinitionDto builderDto) /** Creates the constructor arguments string for the build() method. */ private String createConstructorArgsString(BuilderDefinitionDto builderDto) { return builderDto.getConstructorFieldsForBuilder().stream() - .map(field -> "this." + field.getFieldNameInBuilder() + ".value()") + .map( + field -> { + String fieldRef = "this." + field.getFieldNameInBuilder(); + return field + .getDefaultValue() + .map(defaultExpr -> fieldRef + ".valueOr(" + defaultExpr + ")") + .orElseGet(() -> fieldRef + ".value()"); + }) .reduce((a, b) -> a + ", " + b) .orElse(""); } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/FieldDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/FieldDto.java index d5107f15..5de400ae 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/FieldDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/FieldDto.java @@ -82,6 +82,13 @@ public class FieldDto { */ private final List parameterAnnotations = new ArrayList<>(); + /** + * Default value expression for this field, or {@code null} if no default is declared. The + * expression is pre-formatted as a Java code snippet (e.g. {@code "GENERAL"} for strings, {@code + * 0.0} for primitives, {@code List.of()} for complex types). + */ + private String defaultValue; + /** * Gets the original field name from the DTO. This name is used for generating method names, * parameter names, and setter method names (e.g., "userName" becomes "setUserName"). @@ -308,4 +315,36 @@ public boolean hasAnnotation(String annotationFqn) { annotation.getAnnotationType() != null && annotationFqn.equals(annotation.getAnnotationType().getFullQualifiedName())); } + + /** + * Gets the default value expression for this field, if one was declared via {@code @Default} or a + * compatible annotation. + * + * @return an {@link Optional} containing the formatted Java expression, or empty if no default + */ + public Optional getDefaultValue() { + return Optional.ofNullable(defaultValue); + } + + /** + * Sets the default value expression for this field. + * + * @param defaultValue the formatted Java expression (e.g. {@code "GENERAL"}, {@code 0.0}, {@code + * List.of()}), or {@code null} to clear + */ + public void setDefaultValue(String defaultValue) { + this.defaultValue = defaultValue; + } + + /** + * Whether this field is required to be set at build time. + * + *

          A field is required if it is marked as non-nullable AND has no default value. A field with a + * default value is never required, even if annotated with {@code @NotNull}. + * + * @return {@code true} if the field must be explicitly set before {@code build()} + */ + public boolean isRequired() { + return nonNullable && defaultValue == null; + } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java index ae56959f..057b1344 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java @@ -632,6 +632,22 @@ private static Optional createFieldFromSetter( builderType, context); + // If no default was found on the setter parameter, check the field element itself + // (annotations like @Default may be placed on the field rather than the setter param) + if (result.isPresent() && result.get().getDefaultValue().isEmpty()) { + findFieldElement(dtoTypeElement, fieldName) + .ifPresent( + fieldElement -> + FieldAnnotationExtractor.extractDefaultValue(fieldElement) + .ifPresent( + rawDefault -> + result + .get() + .setDefaultValue( + FieldAnnotationExtractor.formatDefaultExpression( + rawDefault, result.get().getFieldType())))); + } + if (result.isPresent()) { fieldNameRegistry.put(finalFieldName, result.get()); } @@ -797,6 +813,13 @@ private static Optional createFieldDto( field.setNonNullable(true); } + // Extract default value from @Default or @DefaultValue annotation (if present) + FieldAnnotationExtractor.extractDefaultValue(param) + .ifPresent( + rawDefault -> + field.setDefaultValue( + FieldAnnotationExtractor.formatDefaultExpression(rawDefault, fieldType))); + // Builder and constructor information is now set when TypeName is created in JavaLangMapper // Use GeneratorRegistry to generate all methods for this field @@ -806,4 +829,20 @@ private static Optional createFieldDto( return Optional.of(field); } + + /** + * Finds a field element by name in the given class element. + * + * @param classElement the class to search in + * @param fieldName the simple field name to look for + * @return an {@link Optional} containing the field element, or empty if not found + */ + private static Optional findFieldElement( + TypeElement classElement, String fieldName) { + return classElement.getEnclosedElements().stream() + .filter(e -> e.getKind() == ElementKind.FIELD) + .filter(e -> e.getSimpleName().contentEquals(fieldName)) + .map(VariableElement.class::cast) + .findFirst(); + } } From 749a49997c1fd8a95df752f1a65999c8f22088af Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Wed, 5 Aug 2026 20:31:30 +0200 Subject: [PATCH 13/23] Adding a test for validation of defaultValue handling when creating builder functions --- .../builders/processor/DefaultValueTest.java | 384 ++++++++++++++++++ 1 file changed, 384 insertions(+) create mode 100644 processor/src/test/java/org/javahelpers/simple/builders/processor/DefaultValueTest.java diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/DefaultValueTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/DefaultValueTest.java new file mode 100644 index 00000000..a19b9c7c --- /dev/null +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/DefaultValueTest.java @@ -0,0 +1,384 @@ +/* + * MIT License + * + * Copyright (c) 2026 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.javahelpers.simple.builders.processor.testing.ProcessorAsserts.assertGenerationSucceeded; +import static org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils.loadGeneratedSource; + +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; + +/** + * Tests for default value support via {@code @Default} and third-party {@code @DefaultValue} + * annotations. + */ +class DefaultValueTest { + + protected Compilation compile(JavaFileObject... sourceFiles) { + return ProcessorTestUtils.createCompiler().compile(sourceFiles); + } + + /** + * Verifies that a {@code @Default} annotation on a record constructor parameter causes the + * generated {@code build()} method to use {@code valueOr("GENERAL")} instead of {@code value()}, + * and that the field is not subject to required-field validation. + * + *

          Also verifies the builder setter method for the defaulted field is still generated, so users + * can override the default with an explicit value. + */ + @Test + void defaultAppliedWhenUnset_constructorField_record() { + String recordName = "ProductRecord"; + String builderClassName = recordName + "Builder"; + + JavaFileObject sourceFile = + ProcessorTestUtils.forSource( + """ + package test; + + import org.javahelpers.simple.builders.core.annotations.Default; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + + @SimpleBuilder + public record ProductRecord( + String name, + double price, + @Default("GENERAL") String category) {} + """); + + Compilation compilation = compile(sourceFile); + String generatedCode = loadGeneratedSource(compilation, builderClassName); + assertGenerationSucceeded(compilation, builderClassName, generatedCode); + + // build() must use valueOr with the quoted String default for the category field + ProcessorAsserts.assertContaining( + generatedCode, + """ + 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.valueOr("GENERAL")); + return result; + } + """); + } + + /** + * Verifies that {@code @Default} on primitive-typed record parameters ({@code double}, {@code + * int}) produces unquoted raw expressions in {@code valueOr()} calls, since primitives cannot be + * null and do not need string quoting. + */ + @Test + void primitiveDefault_constructorField_record() { + String recordName = "MetricRecord"; + String builderClassName = recordName + "Builder"; + + JavaFileObject sourceFile = + ProcessorTestUtils.forSource( + """ + package test; + + import org.javahelpers.simple.builders.core.annotations.Default; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + + @SimpleBuilder + public record MetricRecord( + String name, + @Default("0.0") double price, + @Default("0") int quantity) {} + """); + + Compilation compilation = compile(sourceFile); + String generatedCode = loadGeneratedSource(compilation, builderClassName); + assertGenerationSucceeded(compilation, builderClassName, generatedCode); + + // Primitives: raw expression (no quoting) + ProcessorAsserts.assertContaining( + generatedCode, + """ + public MetricRecord build() { + MetricRecord result = new MetricRecord(this.name.value(), this.price.valueOr(0.0), this.quantity.valueOr(0)); + return result; + } + """); + } + + /** + * Verifies that a {@code @Default} annotation on a field in a setter-based class causes the + * generated {@code build()} method to use {@code ifSet(result::setStatus).orElse("PENDING")} + * instead of plain {@code ifSet(result::setStatus)}. + * + *

          Also verifies the builder setter method for the defaulted field is still generated, so users + * can override the default with an explicit value. + */ + @Test + void defaultAppliedWhenUnset_setterField_class() { + String className = "OrderDto"; + String builderClassName = className + "Builder"; + + JavaFileObject sourceFile = + ProcessorTestUtils.forSource( + """ + package test; + + import org.javahelpers.simple.builders.core.annotations.Default; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + + @SimpleBuilder + public class OrderDto { + private String id; + @Default("PENDING") + private String status; + + public String getId() { return id; } + public void setId(String id) { this.id = id; } + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + } + """); + + Compilation compilation = compile(sourceFile); + String generatedCode = loadGeneratedSource(compilation, builderClassName); + assertGenerationSucceeded(compilation, builderClassName, generatedCode); + + // build() must use ifSet().orElse() with the quoted String default for the status field + ProcessorAsserts.assertContaining( + generatedCode, + """ + public OrderDto build() { + OrderDto result = new OrderDto(); + this.id.ifSet(result::setId); + this.status.ifSet(result::setStatus).orElse("PENDING"); + return result; + } + """); + // Setter method for the defaulted field must still be generated + ProcessorAsserts.assertContaining( + generatedCode, "public OrderDtoBuilder status(String status)"); + } + + /** + * Verifies that a setter-based class field without {@code @Default} generates plain + * {@code ifSet(result::setStatus);} with no {@code .orElse()} call. This is a regression guard to + * ensure defaults are not accidentally applied when not declared. + */ + @Test + void setterFieldWithoutDefault_usesIfSetOnly() { + String className = "OrderDto"; + String builderClassName = className + "Builder"; + + JavaFileObject sourceFile = + ProcessorTestUtils.forSource( + """ + package test; + + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + + @SimpleBuilder + public class OrderDto { + private String id; + private String status; + + public String getId() { return id; } + public void setId(String id) { this.id = id; } + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + } + """); + + Compilation compilation = compile(sourceFile); + String generatedCode = loadGeneratedSource(compilation, builderClassName); + assertGenerationSucceeded(compilation, builderClassName, generatedCode); + + // Without default, should use plain ifSet (no .orElse) + ProcessorAsserts.assertContaining( + generatedCode, + """ + public OrderDto build() { + OrderDto result = new OrderDto(); + this.id.ifSet(result::setId); + this.status.ifSet(result::setStatus); + return result; + } + """); + } + + // === Default + non-null interaction === + + /** + * Verifies the interaction between {@code @NotNull} and {@code @Default}: + * + *

            + *
          • A field with both {@code @NotNull} and {@code @Default} is not required (the + * default makes it optional), so no required-field validation is generated. + *
          • A field with only {@code @NotNull} (no {@code @Default}) remains required, so + * required-field validation is still generated. + *
          • The defaulted field uses {@code valueOr()} in the constructor call. + *
          + */ + @Test + void defaultWithNonNull_skipsValidation() { + String recordName = "RequiredRecord"; + String builderClassName = recordName + "Builder"; + + JavaFileObject sourceFile = + ProcessorTestUtils.forSource( + """ + package test; + + import jakarta.validation.constraints.NotNull; + import org.javahelpers.simple.builders.core.annotations.Default; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + + @SimpleBuilder + public record RequiredRecord( + @NotNull @Default("UNKNOWN") String name, + @NotNull String required) {} + """); + + // Create mock for NotNull annotation + JavaFileObject notNullMock = + ProcessorTestUtils.createMockAnnotation( + "jakarta.validation.constraints", + "NotNull", + "ElementType.FIELD, ElementType.PARAMETER"); + + Compilation compilation = compile(notNullMock, sourceFile); + String generatedCode = loadGeneratedSource(compilation, builderClassName); + assertGenerationSucceeded(compilation, builderClassName, generatedCode); + + // name has @Default → not required → no required-field validation + // name uses valueOr with the default + // required has no @Default → still required → validation present + // required uses plain value() (no default) + ProcessorAsserts.assertContaining( + generatedCode, + """ + public RequiredRecord build() { + if (!this.required.isSet()) { + throw new IllegalStateException("Required field 'required' must be set before calling build()"); + } + if (this.required.value() == null) { + throw new IllegalStateException("Field 'required' is marked as non-null but null value was provided"); + } + RequiredRecord result = new RequiredRecord(this.name.valueOr("UNKNOWN"), this.required.value()); + return result; + } + """); + } + + // === Framework-agnostic detection === + + /** + * Verifies that the processor detects third-party annotations named {@code @DefaultValue} (e.g., + * Jakarta REST {@code jakarta.ws.rs.DefaultValue}) by simple name matching, not just our own + * {@code @Default}. The generated code should use {@code valueOr()} with the annotation's value. + */ + @Test + void detectsThirdPartyDefaultValueAnnotation() { + String recordName = "JakartaRecord"; + String builderClassName = recordName + "Builder"; + + // Create a mock @DefaultValue annotation (simulating Jakarta REST) + JavaFileObject defaultValueMock = + ProcessorTestUtils.createMockAnnotation( + "jakarta.ws.rs", + "DefaultValue", + "ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD", + "String value();"); + + JavaFileObject sourceFile = + ProcessorTestUtils.forSource( + """ + package test; + + import jakarta.ws.rs.DefaultValue; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + + @SimpleBuilder + public record JakartaRecord( + String name, + @DefaultValue("FALLBACK") String category) {} + """); + + Compilation compilation = compile(defaultValueMock, sourceFile); + String generatedCode = loadGeneratedSource(compilation, builderClassName); + assertGenerationSucceeded(compilation, builderClassName, generatedCode); + + // "category" field detects @DefaultValue → generates valueOr with quoted default + // "name" field is without @DefaultValue → must still use plain value() + ProcessorAsserts.assertContaining( + generatedCode, + """ + public JakartaRecord build() { + JakartaRecord result = new JakartaRecord(this.name.value(), this.category.valueOr("FALLBACK")); + return result; + } + """); + } + + /** + * Verifies that a record without any {@code @Default} annotations generates {@code + * build()} code using plain {@code value()} calls, with no {@code valueOr()} anywhere. This is a + * regression guard to ensure the default-value feature does not change behavior when not used. + */ + @Test + void noDefault_usesValueDirectly_constructorField() { + String recordName = "PlainRecord"; + String builderClassName = recordName + "Builder"; + + JavaFileObject sourceFile = + ProcessorTestUtils.forSource( + """ + package test; + + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + + @SimpleBuilder + public record PlainRecord(String name, Integer age) {} + """); + + Compilation compilation = compile(sourceFile); + String generatedCode = loadGeneratedSource(compilation, builderClassName); + assertGenerationSucceeded(compilation, builderClassName, generatedCode); + + // Without default, should use plain .value() + ProcessorAsserts.assertContaining( + generatedCode, + """ + public PlainRecord build() { + PlainRecord result = new PlainRecord(this.name.value(), this.age.value()); + return result; + } + """); + } +} From ec1f6370eb945f44314c16e3bc4fb81e18563500 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Wed, 5 Aug 2026 20:38:35 +0200 Subject: [PATCH 14/23] Adding example files for Default-Values --- .../builders/example/OrderWithDefaults.java | 87 +++ .../builders/example/ProductWithDefaults.java | 64 +++ .../example/OrderWithDefaultsBuilder.java | 418 +++++++++++++++ .../example/ProductWithDefaultsBuilder.java | 498 ++++++++++++++++++ 4 files changed, 1067 insertions(+) create mode 100644 example/src/main/java/org/javahelpers/simple/builders/example/OrderWithDefaults.java create mode 100644 example/src/main/java/org/javahelpers/simple/builders/example/ProductWithDefaults.java create mode 100644 example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/OrderWithDefaultsBuilder.java create mode 100644 example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/ProductWithDefaultsBuilder.java diff --git a/example/src/main/java/org/javahelpers/simple/builders/example/OrderWithDefaults.java b/example/src/main/java/org/javahelpers/simple/builders/example/OrderWithDefaults.java new file mode 100644 index 00000000..dc654e13 --- /dev/null +++ b/example/src/main/java/org/javahelpers/simple/builders/example/OrderWithDefaults.java @@ -0,0 +1,87 @@ +/* + * MIT License + * + * Copyright (c) 2026 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 org.javahelpers.simple.builders.core.annotations.Default; +import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + +/** + * Example showing default values for setter-based fields in a class using {@code @Default}. + * + *

          When a field annotated with {@code @Default} is not explicitly set on the builder, the + * declared default value is applied via the setter at {@code build()} time. + * + *

          Example usage: + * + *

          {@code
          + * // status uses default "PENDING"
          + * OrderWithDefaults order = OrderWithDefaultsBuilder.create()
          + *     .id("ORD-001")
          + *     .build();
          + * // order.getPriority() == 3
          + * // order.getStatus() == "PENDING"
          + *
          + * // explicit value overrides default
          + * OrderWithDefaults shipped = OrderWithDefaultsBuilder.create()
          + *     .id("ORD-002")
          + *     .status("SHIPPED")
          + *     .build();
          + * // shipped.getPriority() == 3
          + * // shipped.getStatus() == "SHIPPED"
          + * }
          + */ +@SimpleBuilder +public class OrderWithDefaults { + + private String id; + @Default("PENDING") + private String status; + @Default("2") + private int priority; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public int getPriority() { + return priority; + } + + public void setPriority(int priority) { + this.priority = priority; + } +} diff --git a/example/src/main/java/org/javahelpers/simple/builders/example/ProductWithDefaults.java b/example/src/main/java/org/javahelpers/simple/builders/example/ProductWithDefaults.java new file mode 100644 index 00000000..b58a91e0 --- /dev/null +++ b/example/src/main/java/org/javahelpers/simple/builders/example/ProductWithDefaults.java @@ -0,0 +1,64 @@ +/* + * MIT License + * + * Copyright (c) 2026 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 org.javahelpers.simple.builders.core.annotations.Default; +import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + +/** + * Example showing default values for unset builder fields using {@code @Default}. + * + *

          When a field annotated with {@code @Default} is not explicitly set on the builder, the + * declared default value is used at {@code build()} time. + * + *

          Example usage: + * + *

          {@code
          + * // category and active use defaults
          + * ProductWithDefaults product = ProductWithDefaultsBuilder.create()
          + *     .name("Laptop")
          + *     .price(1500.0)
          + *     .build();
          + * // product.category() == "GENERAL"
          + * // product.active() == true
          + *
          + * // explicit values override defaults
          + * ProductWithDefaults custom = ProductWithDefaultsBuilder.create()
          + *     .name("Widget")
          + *     .price(9.99)
          + *     .category("ACCESSORIES")
          + *     .active(false)
          + *     .build();
          + * // custom.category() == "ACCESSORIES"
          + * // custom.active() == false
          + * }
          + */ +@SimpleBuilder +public record ProductWithDefaults( + String name, + double price, + @Default("GENERAL") String category, + @Default("true") boolean active +) {} diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/OrderWithDefaultsBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/OrderWithDefaultsBuilder.java new file mode 100644 index 00000000..697ad094 --- /dev/null +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/OrderWithDefaultsBuilder.java @@ -0,0 +1,418 @@ +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.apache.commons.lang3.builder.ToStringBuilder; +import org.javahelpers.simple.builders.core.annotations.BuilderImplementation; +import org.javahelpers.simple.builders.core.interfaces.IBuilderBase; +import org.javahelpers.simple.builders.core.util.BuilderToStringStyle; +import org.javahelpers.simple.builders.core.util.TrackedValue; + +/** + * Builder for {@code org.javahelpers.simple.builders.example.OrderWithDefaults}. + *

          + * This builder provides a fluent API for creating instances of + * org.javahelpers.simple.builders.example.OrderWithDefaults with method chaining and validation. Use the static + * {@code create()} method to obtain a new builder instance, configure the desired properties using the setter methods, + * and then call {@code build()} to create the final DTO. + * + *

          Example:

          + * + *
          {@code
          + * OrderWithDefaults result = OrderWithDefaultsBuilder.create()
          + *     .id("example value")
          + *     .id("Hello %s", "World")
          + *     .id(() -> "example value")
          + *     .id(sb -> sb.append("text"))
          + *     .priority(42)
          + *     .priority(() -> 42)
          + *     .status("example value")
          + *     .status("Hello %s", "World")
          + *     .status(() -> "example value")
          + *     .status(sb -> sb.append("text"))
          + *     .build();
          + * }
          + */ +@Generated("Generated by org.javahelpers.simple.builders.processor.BuilderProcessor") +@BuilderImplementation(forClass = OrderWithDefaults.class) +public class OrderWithDefaultsBuilder implements IBuilderBase { + + /** + * Tracked value for id: id. + */ + private TrackedValue id = unsetValue(); + /** + * Tracked value for priority: priority. + */ + private TrackedValue priority = unsetValue(); + /** + * Tracked value for status: status. + */ + private TrackedValue status = unsetValue(); + + /** + * Empty constructor of builder for {@code org.javahelpers.simple.builders.example.OrderWithDefaults}. + */ + public OrderWithDefaultsBuilder() { + } + + /** + * Initialisation of builder for {@code org.javahelpers.simple.builders.example.OrderWithDefaults} by a instance. + * + * @param instance object instance for initialisiation + */ + public OrderWithDefaultsBuilder(OrderWithDefaults instance) { + this.id = initialValue(instance.getId()); + this.priority = initialValue(instance.getPriority()); + this.status = initialValue(instance.getStatus()); + } + + /** + * Creating a new builder for {@code org.javahelpers.simple.builders.example.OrderWithDefaults}. + * + *

          Example:

          + * + *
          {@code
          +   * OrderWithDefaultsBuilder builder = OrderWithDefaultsBuilder.create();
          +   * }
          + * + * @return builder for {@code org.javahelpers.simple.builders.example.OrderWithDefaults} + */ + public static OrderWithDefaultsBuilder create() { + return new OrderWithDefaultsBuilder(); + } + + /** + * Sets the value for id. + *

          + * Generated from setter {@link OrderWithDefaults#setId(String) setId(String id)} + * + *

          Example:

          + * + *
          {@code
          +   * builder.id("example value");
          +   * }
          + * + * @param id id + * @return current instance of builder + */ + public OrderWithDefaultsBuilder id(String id) { + this.id = changedValue(id); + return this; + } + + /** + * Sets the value for id by executing the provided consumer. + *

          + * Generated from setter {@link OrderWithDefaults#setId(String) setId(String id)} + * + *

          Example:

          + * + *
          {@code
          +   * builder.id(sb -> sb.append("text"));
          +   * }
          + * + * @param idStringBuilderConsumer consumer providing an instance of id + * @return current instance of builder + */ + public OrderWithDefaultsBuilder id(Consumer idStringBuilderConsumer) { + StringBuilder builder = new StringBuilder(); + idStringBuilderConsumer.accept(builder); + this.id = changedValue(builder.toString()); + return this; + } + + /** + * Sets the value for id by invoking the provided supplier. + *

          + * Generated from setter {@link OrderWithDefaults#setId(String) setId(String id)} + * + *

          Example:

          + * + *
          {@code
          +   * builder.id(() -> "example value");
          +   * }
          + * + * @param idSupplier supplier for id + * @return current instance of builder + */ + public OrderWithDefaultsBuilder id(Supplier idSupplier) { + this.id = changedValue(idSupplier.get()); + return this; + } + + /** + * Sets the String value for id by using String.format(format, args). See + * {@link String#format(String, Object...)} for details. + *

          + * Generated from setter {@link OrderWithDefaults#setId(String) setId(String id)} + * + *

          Example:

          + * + *
          {@code
          +   * builder.id("Hello %s", "World");
          +   * }
          + * + * @param format A format string + * @param args Arguments referenced by the format specifiers in the format string. + * @return current instance of builder + */ + public OrderWithDefaultsBuilder id(String format, Object... args) { + this.id = changedValue(String.format(format, args)); + return this; + } + + /** + * Sets the value for priority. + *

          + * Generated from setter {@link OrderWithDefaults#setPriority(int) setPriority(int priority)} + * + *

          Example:

          + * + *
          {@code
          +   * builder.priority(42);
          +   * }
          + * + * @param priority priority + * @return current instance of builder + */ + public OrderWithDefaultsBuilder priority(int priority) { + this.priority = changedValue(priority); + return this; + } + + /** + * Sets the value for priority by invoking the provided supplier. + *

          + * Generated from setter {@link OrderWithDefaults#setPriority(int) setPriority(int priority)} + * + *

          Example:

          + * + *
          {@code
          +   * builder.priority(() -> 42);
          +   * }
          + * + * @param prioritySupplier supplier for priority + * @return current instance of builder + */ + public OrderWithDefaultsBuilder priority(Supplier prioritySupplier) { + this.priority = changedValue(prioritySupplier.get()); + return this; + } + + /** + * Sets the value for status. + *

          + * Generated from setter {@link OrderWithDefaults#setStatus(String) setStatus(String status)} + * + *

          Example:

          + * + *
          {@code
          +   * builder.status("example value");
          +   * }
          + * + * @param status status + * @return current instance of builder + */ + public OrderWithDefaultsBuilder status(String status) { + this.status = changedValue(status); + return this; + } + + /** + * Sets the value for status by executing the provided consumer. + *

          + * Generated from setter {@link OrderWithDefaults#setStatus(String) setStatus(String status)} + * + *

          Example:

          + * + *
          {@code
          +   * builder.status(sb -> sb.append("text"));
          +   * }
          + * + * @param statusStringBuilderConsumer consumer providing an instance of status + * @return current instance of builder + */ + public OrderWithDefaultsBuilder status(Consumer statusStringBuilderConsumer) { + StringBuilder builder = new StringBuilder(); + statusStringBuilderConsumer.accept(builder); + this.status = changedValue(builder.toString()); + return this; + } + + /** + * Sets the value for status by invoking the provided supplier. + *

          + * Generated from setter {@link OrderWithDefaults#setStatus(String) setStatus(String status)} + * + *

          Example:

          + * + *
          {@code
          +   * builder.status(() -> "example value");
          +   * }
          + * + * @param statusSupplier supplier for status + * @return current instance of builder + */ + public OrderWithDefaultsBuilder status(Supplier statusSupplier) { + this.status = changedValue(statusSupplier.get()); + return this; + } + + /** + * Sets the String value for status by using String.format(format, args). See + * {@link String#format(String, Object...)} for details. + *

          + * Generated from setter {@link OrderWithDefaults#setStatus(String) setStatus(String status)} + * + *

          Example:

          + * + *
          {@code
          +   * builder.status("Hello %s", "World");
          +   * }
          + * + * @param format A format string + * @param args Arguments referenced by the format specifiers in the format string. + * @return current instance of builder + */ + public OrderWithDefaultsBuilder status(String format, Object... args) { + this.status = changedValue(String.format(format, args)); + return this; + } + + /** + * Validates that the id field is not null or empty. + *

          + * Generated from setter {@link OrderWithDefaults#setId(String) setId(String id)} + * + * @return this builder instance for chaining + * @throws IllegalArgumentException if id is null or empty + */ + OrderWithDefaultsBuilder validateId() { + if (!id.isSet() || id.value().trim().isEmpty()) { + throw new IllegalArgumentException("Id cannot be null or empty"); + } + return this; + } + + /** + * Validates that the status field is not null or empty. + *

          + * Generated from setter {@link OrderWithDefaults#setStatus(String) setStatus(String status)} + * + * @return this builder instance for chaining + * @throws IllegalArgumentException if status is null or empty + */ + OrderWithDefaultsBuilder validateStatus() { + if (!status.isSet() || status.value().trim().isEmpty()) { + throw new IllegalArgumentException("Status cannot be null or empty"); + } + 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 OrderWithDefaultsBuilder conditional(BooleanSupplier condition, + Consumer yesCondition) { + return conditional(condition, yesCondition, null); + } + + /** + * 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 OrderWithDefaultsBuilder conditional(BooleanSupplier condition, Consumer trueCase, + Consumer falseCase) { + if (condition.getAsBoolean()) { + trueCase.accept(this); + } else if (falseCase != null) { + falseCase.accept(this); + } + return this; + } + + /** + * Builds the configured DTO instance. + * + *

          Example:

          + * + *
          {@code
          +   * OrderWithDefaults result = builder.build();
          +   * }
          + */ + @Override + public OrderWithDefaults build() { + OrderWithDefaults result = new OrderWithDefaults(); + this.id.ifSet(result::setId); + this.priority.ifSet(result::setPriority).orElse(2); + this.status.ifSet(result::setStatus).orElse("PENDING"); + return result; + } + + /** + * Returns a string representation of this builder, including only fields that have been set. + * + * @return string representation of the builder + */ + @Override + public String toString() { + return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE).append("id", this.id) + .append("priority", this.priority) + .append("status", this.status) + .toString(); + } + + /** + * Interface that can be implemented by the DTO to provide fluent modification methods. + */ + public interface With { + /** + * Initializes a builder from an instance of this class, using methods of this builder to change values and returns + * the new built object. + * + * @param b the consumer to apply modifications + * @return the modified instance + */ + default OrderWithDefaults with(Consumer b) { + OrderWithDefaultsBuilder builder; + try { + builder = new OrderWithDefaultsBuilder(OrderWithDefaults.class.cast(this)); + } catch (ClassCastException ex) { + throw new IllegalArgumentException( + "The interface 'OrderWithDefaultsBuilder.With' should only be implemented by classes, which could be casted to 'OrderWithDefaults'", + ex); + } + b.accept(builder); + return builder.build(); + } + + /** + * Creates a builder initialized from this instance. + * + * @return a builder initialized with this instance's values + */ + default OrderWithDefaultsBuilder with() { + try { + return new OrderWithDefaultsBuilder(OrderWithDefaults.class.cast(this)); + } catch (ClassCastException ex) { + throw new IllegalArgumentException( + "The interface 'OrderWithDefaultsBuilder.With' should only be implemented by classes, which could be casted to 'OrderWithDefaults'", + ex); + } + } + } +} \ No newline at end of file diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/ProductWithDefaultsBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/ProductWithDefaultsBuilder.java new file mode 100644 index 00000000..2e2f8bf0 --- /dev/null +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/ProductWithDefaultsBuilder.java @@ -0,0 +1,498 @@ +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.apache.commons.lang3.builder.ToStringBuilder; +import org.javahelpers.simple.builders.core.annotations.BuilderImplementation; +import org.javahelpers.simple.builders.core.interfaces.IBuilderBase; +import org.javahelpers.simple.builders.core.util.BuilderToStringStyle; +import org.javahelpers.simple.builders.core.util.TrackedValue; + +/** + * Builder for {@code org.javahelpers.simple.builders.example.ProductWithDefaults}. + *

          + * This builder provides a fluent API for creating instances of + * org.javahelpers.simple.builders.example.ProductWithDefaults with method chaining and validation. Use the static + * {@code create()} method to obtain a new builder instance, configure the desired properties using the setter methods, + * and then call {@code build()} to create the final DTO. + * + *

          Example:

          + * + *
          {@code
          + * ProductWithDefaults result = ProductWithDefaultsBuilder.create()
          + *     .name("example value")
          + *     .name("Hello %s", "World")
          + *     .name(() -> "example value")
          + *     .name(sb -> sb.append("text"))
          + *     .price(3.14)
          + *     .price(() -> 3.14)
          + *     .category("example value")
          + *     .category("Hello %s", "World")
          + *     .category(() -> "example value")
          + *     .category(sb -> sb.append("text"))
          + *     .active(true)
          + *     .active(() -> true)
          + *     .build();
          + * }
          + */ +@Generated("Generated by org.javahelpers.simple.builders.processor.BuilderProcessor") +@BuilderImplementation(forClass = ProductWithDefaults.class) +public class ProductWithDefaultsBuilder 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(); + /** + * Tracked value for active: active. + */ + private TrackedValue active = unsetValue(); + + /** + * Empty constructor of builder for {@code org.javahelpers.simple.builders.example.ProductWithDefaults}. + */ + public ProductWithDefaultsBuilder() { + } + + /** + * Initialisation of builder for {@code org.javahelpers.simple.builders.example.ProductWithDefaults} by a instance. + * + * @param instance object instance for initialisiation + */ + public ProductWithDefaultsBuilder(ProductWithDefaults instance) { + this.name = initialValue(instance.name()); + this.price = initialValue(instance.price()); + this.category = initialValue(instance.category()); + this.active = initialValue(instance.active()); + } + + /** + * Creating a new builder for {@code org.javahelpers.simple.builders.example.ProductWithDefaults}. + * + *

          Example:

          + * + *
          {@code
          +   * ProductWithDefaultsBuilder builder = ProductWithDefaultsBuilder.create();
          +   * }
          + * + * @return builder for {@code org.javahelpers.simple.builders.example.ProductWithDefaults} + */ + public static ProductWithDefaultsBuilder create() { + return new ProductWithDefaultsBuilder(); + } + + /** + * Sets the value for active. + *

          + * Generated from parameter in constructor + * {@link ProductWithDefaults#ProductWithDefaults(String, double, String, boolean) ProductWithDefaults(String name, + * double price, String category, boolean active)} + * + *

          Example:

          + * + *
          {@code
          +   * builder.active(true);
          +   * }
          + * + * @param active active + * @return current instance of builder + */ + public ProductWithDefaultsBuilder active(boolean active) { + this.active = changedValue(active); + return this; + } + + /** + * Sets the value for active by invoking the provided supplier. + *

          + * Generated from parameter in constructor + * {@link ProductWithDefaults#ProductWithDefaults(String, double, String, boolean) ProductWithDefaults(String name, + * double price, String category, boolean active)} + * + *

          Example:

          + * + *
          {@code
          +   * builder.active(() -> true);
          +   * }
          + * + * @param activeSupplier supplier for active + * @return current instance of builder + */ + public ProductWithDefaultsBuilder active(Supplier activeSupplier) { + this.active = changedValue(activeSupplier.get()); + return this; + } + + /** + * Sets the value for category. + *

          + * Generated from parameter in constructor + * {@link ProductWithDefaults#ProductWithDefaults(String, double, String, boolean) ProductWithDefaults(String name, + * double price, String category, boolean active)} + * + *

          Example:

          + * + *
          {@code
          +   * builder.category("example value");
          +   * }
          + * + * @param category category + * @return current instance of builder + */ + public ProductWithDefaultsBuilder category(String category) { + this.category = changedValue(category); + return this; + } + + /** + * Sets the value for category by executing the provided consumer. + *

          + * Generated from parameter in constructor + * {@link ProductWithDefaults#ProductWithDefaults(String, double, String, boolean) ProductWithDefaults(String name, + * double price, String category, boolean active)} + * + *

          Example:

          + * + *
          {@code
          +   * builder.category(sb -> sb.append("text"));
          +   * }
          + * + * @param categoryStringBuilderConsumer consumer providing an instance of category + * @return current instance of builder + */ + public ProductWithDefaultsBuilder 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. + *

          + * Generated from parameter in constructor + * {@link ProductWithDefaults#ProductWithDefaults(String, double, String, boolean) ProductWithDefaults(String name, + * double price, String category, boolean active)} + * + *

          Example:

          + * + *
          {@code
          +   * builder.category(() -> "example value");
          +   * }
          + * + * @param categorySupplier supplier for category + * @return current instance of builder + */ + public ProductWithDefaultsBuilder category(Supplier categorySupplier) { + this.category = changedValue(categorySupplier.get()); + return this; + } + + /** + * Sets the String value for category by using String.format(format, args). See + * {@link String#format(String, Object...)} for details. + *

          + * Generated from parameter in constructor + * {@link ProductWithDefaults#ProductWithDefaults(String, double, String, boolean) ProductWithDefaults(String name, + * double price, String category, boolean active)} + * + *

          Example:

          + * + *
          {@code
          +   * builder.category("Hello %s", "World");
          +   * }
          + * + * @param format A format string + * @param args Arguments referenced by the format specifiers in the format string. + * @return current instance of builder + */ + public ProductWithDefaultsBuilder category(String format, Object... args) { + this.category = changedValue(String.format(format, args)); + return this; + } + + /** + * Sets the value for name. + *

          + * Generated from parameter in constructor + * {@link ProductWithDefaults#ProductWithDefaults(String, double, String, boolean) ProductWithDefaults(String name, + * double price, String category, boolean active)} + * + *

          Example:

          + * + *
          {@code
          +   * builder.name("example value");
          +   * }
          + * + * @param name name + * @return current instance of builder + */ + public ProductWithDefaultsBuilder name(String name) { + this.name = changedValue(name); + return this; + } + + /** + * Sets the value for name by executing the provided consumer. + *

          + * Generated from parameter in constructor + * {@link ProductWithDefaults#ProductWithDefaults(String, double, String, boolean) ProductWithDefaults(String name, + * double price, String category, boolean active)} + * + *

          Example:

          + * + *
          {@code
          +   * builder.name(sb -> sb.append("text"));
          +   * }
          + * + * @param nameStringBuilderConsumer consumer providing an instance of name + * @return current instance of builder + */ + public ProductWithDefaultsBuilder 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. + *

          + * Generated from parameter in constructor + * {@link ProductWithDefaults#ProductWithDefaults(String, double, String, boolean) ProductWithDefaults(String name, + * double price, String category, boolean active)} + * + *

          Example:

          + * + *
          {@code
          +   * builder.name(() -> "example value");
          +   * }
          + * + * @param nameSupplier supplier for name + * @return current instance of builder + */ + public ProductWithDefaultsBuilder name(Supplier nameSupplier) { + this.name = changedValue(nameSupplier.get()); + return this; + } + + /** + * Sets the String value for name by using String.format(format, args). See + * {@link String#format(String, Object...)} for details. + *

          + * Generated from parameter in constructor + * {@link ProductWithDefaults#ProductWithDefaults(String, double, String, boolean) ProductWithDefaults(String name, + * double price, String category, boolean active)} + * + *

          Example:

          + * + *
          {@code
          +   * builder.name("Hello %s", "World");
          +   * }
          + * + * @param format A format string + * @param args Arguments referenced by the format specifiers in the format string. + * @return current instance of builder + */ + public ProductWithDefaultsBuilder name(String format, Object... args) { + this.name = changedValue(String.format(format, args)); + return this; + } + + /** + * Sets the value for price. + *

          + * Generated from parameter in constructor + * {@link ProductWithDefaults#ProductWithDefaults(String, double, String, boolean) ProductWithDefaults(String name, + * double price, String category, boolean active)} + * + *

          Example:

          + * + *
          {@code
          +   * builder.price(3.14);
          +   * }
          + * + * @param price price + * @return current instance of builder + */ + public ProductWithDefaultsBuilder price(double price) { + this.price = changedValue(price); + return this; + } + + /** + * Sets the value for price by invoking the provided supplier. + *

          + * Generated from parameter in constructor + * {@link ProductWithDefaults#ProductWithDefaults(String, double, String, boolean) ProductWithDefaults(String name, + * double price, String category, boolean active)} + * + *

          Example:

          + * + *
          {@code
          +   * builder.price(() -> 3.14);
          +   * }
          + * + * @param priceSupplier supplier for price + * @return current instance of builder + */ + public ProductWithDefaultsBuilder price(Supplier priceSupplier) { + this.price = changedValue(priceSupplier.get()); + return this; + } + + /** + * Validates that the category field is not null or empty. + *

          + * Generated from parameter in constructor + * {@link ProductWithDefaults#ProductWithDefaults(String, double, String, boolean) ProductWithDefaults(String name, + * double price, String category, boolean active)} + * + * @return this builder instance for chaining + * @throws IllegalArgumentException if category is null or empty + */ + ProductWithDefaultsBuilder validateCategory() { + if (!category.isSet() || category.value().trim().isEmpty()) { + throw new IllegalArgumentException("Category cannot be null or empty"); + } + return this; + } + + /** + * Validates that the name field is not null or empty. + *

          + * Generated from parameter in constructor + * {@link ProductWithDefaults#ProductWithDefaults(String, double, String, boolean) ProductWithDefaults(String name, + * double price, String category, boolean active)} + * + * @return this builder instance for chaining + * @throws IllegalArgumentException if name is null or empty + */ + ProductWithDefaultsBuilder validateName() { + if (!name.isSet() || name.value().trim().isEmpty()) { + throw new IllegalArgumentException("Name cannot be null or empty"); + } + 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 ProductWithDefaultsBuilder conditional(BooleanSupplier condition, + Consumer yesCondition) { + return conditional(condition, yesCondition, null); + } + + /** + * 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 ProductWithDefaultsBuilder conditional(BooleanSupplier condition, + Consumer trueCase, Consumer falseCase) { + if (condition.getAsBoolean()) { + trueCase.accept(this); + } else if (falseCase != null) { + falseCase.accept(this); + } + return this; + } + + /** + * Builds the configured DTO instance. + * + *

          Example:

          + * + *
          {@code
          +   * ProductWithDefaults result = builder.build();
          +   * }
          + */ + @Override + public ProductWithDefaults 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"); + } + ProductWithDefaults result = new ProductWithDefaults(this.name.value(), + this.price.value(), + this.category.valueOr("GENERAL"), + this.active.valueOr(true)); + return result; + } + + /** + * Returns a string representation of this builder, including only fields that have been set. + * + * @return string representation of the builder + */ + @Override + public String toString() { + return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE).append("name", this.name) + .append("price", this.price) + .append("category", this.category) + .append("active", this.active) + .toString(); + } + + /** + * Interface that can be implemented by the DTO to provide fluent modification methods. + */ + public interface With { + /** + * Initializes a builder from an instance of this class, using methods of this builder to change values and returns + * the new built object. + * + * @param b the consumer to apply modifications + * @return the modified instance + */ + default ProductWithDefaults with(Consumer b) { + ProductWithDefaultsBuilder builder; + try { + builder = new ProductWithDefaultsBuilder(ProductWithDefaults.class.cast(this)); + } catch (ClassCastException ex) { + throw new IllegalArgumentException( + "The interface 'ProductWithDefaultsBuilder.With' should only be implemented by classes, which could be casted to 'ProductWithDefaults'", + ex); + } + b.accept(builder); + return builder.build(); + } + + /** + * Creates a builder initialized from this instance. + * + * @return a builder initialized with this instance's values + */ + default ProductWithDefaultsBuilder with() { + try { + return new ProductWithDefaultsBuilder(ProductWithDefaults.class.cast(this)); + } catch (ClassCastException ex) { + throw new IllegalArgumentException( + "The interface 'ProductWithDefaultsBuilder.With' should only be implemented by classes, which could be casted to 'ProductWithDefaults'", + ex); + } + } + } +} \ No newline at end of file From c0b81a85bc8c946f4e487cb1137e16d68a35af1e Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Wed, 5 Aug 2026 20:43:13 +0200 Subject: [PATCH 15/23] Refactoring code for better reading of it --- .../analysis/FieldAnnotationExtractor.java | 34 ++++++++------ .../processing/BuilderDefinitionCreator.java | 46 ++++++++++++++----- 2 files changed, 53 insertions(+), 27 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/FieldAnnotationExtractor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/FieldAnnotationExtractor.java index 9bf6278e..4257aefa 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/FieldAnnotationExtractor.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/FieldAnnotationExtractor.java @@ -28,6 +28,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; @@ -263,20 +264,23 @@ private static boolean isNonNullAnnotation(String simpleName) { } /** - * Extracts a default value expression from annotations named {@code Default} or {@code - * DefaultValue} on the given parameter, regardless of package. + * Extracts the {@code value()} member from any annotation on the given element whose simple name + * matches one of the provided names, regardless of package. * - *

          Detects annotations from any package (e.g. {@code + *

          For example, if {@code annotationNames} contains {@code "Default"} and {@code + * "DefaultValue"}, this will detect annotations from any package (e.g. {@code * org.javahelpers.simple.builders.core.annotations.Default}, {@code jakarta.ws.rs.DefaultValue}) * as long as they have a {@code String value()} member. * - * @param param the parameter element to check - * @return an {@link Optional} containing the raw default value string, or empty if no default - * annotation is present + * @param element the element to check for annotations + * @param annotationNames the set of annotation simple names to look for + * @return an {@link Optional} containing the raw value string, or empty if no matching annotation + * with a {@code value()} member is present */ - public static Optional extractDefaultValue(VariableElement param) { - return param.getAnnotationMirrors().stream() - .filter(FieldAnnotationExtractor::isDefaultAnnotation) + public static Optional extractAnnotationValue( + Element element, Set annotationNames) { + return element.getAnnotationMirrors().stream() + .filter(mirror -> isAnnotationWithName(mirror, annotationNames)) .map(FieldAnnotationExtractor::getValueMember) .filter(Optional::isPresent) .map(Optional::get) @@ -284,18 +288,18 @@ public static Optional extractDefaultValue(VariableElement param) { } /** - * Checks if the given annotation mirror represents a default-value annotation (named {@code - * Default} or {@code DefaultValue} from any package). + * Checks if the given annotation mirror's simple name matches one of the provided names. * * @param mirror the annotation mirror to check - * @return {@code true} if the annotation is a recognized default-value annotation + * @param annotationNames the set of annotation simple names to match against + * @return {@code true} if the annotation's simple name is in the provided set */ - private static boolean isDefaultAnnotation(AnnotationMirror mirror) { + private static boolean isAnnotationWithName( + AnnotationMirror mirror, Set annotationNames) { if (!(mirror.getAnnotationType().asElement() instanceof TypeElement type)) { return false; } - String name = type.getSimpleName().toString(); - return "Default".equals(name) || "DefaultValue".equals(name); + return annotationNames.contains(type.getSimpleName().toString()); } /** diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java index 057b1344..92ed43a5 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java @@ -60,6 +60,13 @@ /** Class for creating a specific BuilderDefinitionDto for an annotated DTO class. */ public class BuilderDefinitionCreator { + /** + * Annotation simple names that are recognized as default-value annotations, regardless of + * package. This includes our own {@code @Default} as well as third-party annotations like Jakarta + * REST's {@code @DefaultValue}. + */ + private static final Set DEFAULT_ANNOTATION_NAMES = Set.of("Default", "DefaultValue"); + private BuilderDefinitionCreator() { // Private constructor to prevent instantiation } @@ -635,17 +642,7 @@ private static Optional createFieldFromSetter( // If no default was found on the setter parameter, check the field element itself // (annotations like @Default may be placed on the field rather than the setter param) if (result.isPresent() && result.get().getDefaultValue().isEmpty()) { - findFieldElement(dtoTypeElement, fieldName) - .ifPresent( - fieldElement -> - FieldAnnotationExtractor.extractDefaultValue(fieldElement) - .ifPresent( - rawDefault -> - result - .get() - .setDefaultValue( - FieldAnnotationExtractor.formatDefaultExpression( - rawDefault, result.get().getFieldType())))); + tryApplyDefaultFromField(result.get(), dtoTypeElement, fieldName); } if (result.isPresent()) { @@ -814,7 +811,7 @@ private static Optional createFieldDto( } // Extract default value from @Default or @DefaultValue annotation (if present) - FieldAnnotationExtractor.extractDefaultValue(param) + FieldAnnotationExtractor.extractAnnotationValue(param, DEFAULT_ANNOTATION_NAMES) .ifPresent( rawDefault -> field.setDefaultValue( @@ -830,6 +827,31 @@ private static Optional createFieldDto( return Optional.of(field); } + /** + * Extracts and applies a default value from the field declaration itself, if the field carries a + * recognized default annotation (e.g. {@code @Default}). This is used as a fallback when no + * default was found on the setter parameter. + * + * @param field the field DTO to update with a default value if one is found + * @param dtoTypeElement the enclosing class element to search for the field + * @param fieldName the simple field name to look for + */ + private static void tryApplyDefaultFromField( + FieldDto field, TypeElement dtoTypeElement, String fieldName) { + Optional fieldElement = findFieldElement(dtoTypeElement, fieldName); + if (fieldElement.isEmpty()) { + return; + } + Optional rawDefault = + FieldAnnotationExtractor.extractAnnotationValue( + fieldElement.get(), DEFAULT_ANNOTATION_NAMES); + if (rawDefault.isEmpty()) { + return; + } + field.setDefaultValue( + FieldAnnotationExtractor.formatDefaultExpression(rawDefault.get(), field.getFieldType())); + } + /** * Finds a field element by name in the given class element. * From 7ba604ba78b6176b7018d81a9d8279f0a096e208 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Wed, 5 Aug 2026 20:54:43 +0200 Subject: [PATCH 16/23] Adding usage in FeatureIntegrationTest --- .../processor/ComprehensiveFeatureIntegrationTest.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/ComprehensiveFeatureIntegrationTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/ComprehensiveFeatureIntegrationTest.java index f38d7b5e..32c852ce 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/ComprehensiveFeatureIntegrationTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/ComprehensiveFeatureIntegrationTest.java @@ -92,7 +92,10 @@ public AddressDto(String street, String city) { import java.util.Set; import java.util.Map; import java.util.Optional; - @org.javahelpers.simple.builders.core.annotations.SimpleBuilder + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + import org.javahelpers.simple.builders.core.annotations.Default; + + @SimpleBuilder public class PersonDto { private final String name; private final int age; @@ -104,7 +107,8 @@ public class PersonDto { private final List previousAddresses; private final LinkedList phoneNumbers; - public PersonDto(String name, int age, Optional email, + public PersonDto(String name, int age, + @Default("Optional.empty()") Optional email, List nicknames, Set tags, Map metadata, AddressDto address, List previousAddresses, @@ -1197,7 +1201,7 @@ public PersonDto build() { } PersonDto result = new PersonDto(this.name.value(), this.age.value(), - this.email.value(), + this.email.valueOr(Optional.empty()), this.nicknames.value(), this.tags.value(), this.metadata.value(), From 1a2b0f6d7a7e5c2e3175c9698fce2f2c19410d9c Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Wed, 5 Aug 2026 21:29:48 +0200 Subject: [PATCH 17/23] Adding generated source to building context --- example/pom.xml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/example/pom.xml b/example/pom.xml index a1ad407a..75b0d56e 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -72,6 +72,26 @@ + + org.codehaus.mojo + build-helper-maven-plugin + 3.6.0 + + + add-generated-source + generate-sources + + add-source + + + + ${project.basedir}/generated-example-builder + + + + + + org.apache.maven.plugins maven-compiler-plugin From 537f726e7ed6a7699766aedca5826575c52303e6 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Wed, 5 Aug 2026 21:30:12 +0200 Subject: [PATCH 18/23] Fixing codeformat --- .../processor/generators/util/MethodGeneratorUtil.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java index fc79e59a..328ba5b2 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java @@ -415,9 +415,9 @@ public static void addExampleChainFragment(BuilderMethodDto methodDto, TypeName * *

          This helper method retrieves an example value for the given element type and formats it as a * fluent-chain fragment showing two values (e.g., {@code methodName(value1, value2)}) to make it - * clear that the method accepts multiple arguments. The fragment is stored on the BuilderMethodDto for - * later use by the ClassJavaDocEnhancer to synthesize both method-level and class-level Javadoc - * examples. + * clear that the method accepts multiple arguments. The fragment is stored on the + * BuilderMethodDto for later use by the ClassJavaDocEnhancer to synthesize both method-level and + * class-level Javadoc examples. * *

          If no example value is available for the element type, the fragment is not added. * @@ -436,8 +436,8 @@ public static void addExampleChainFragmentVarArgs( *

          This helper method retrieves an example value for the given field type and formats it as a * fluent-chain fragment with a supplier. For types with an empty constructor, a method reference * is used (e.g., {@code methodName(Type::new)}); otherwise a lambda is used (e.g., {@code - * methodName(() -> exampleValue)}). The fragment is stored on the BuilderMethodDto for later use by the - * ClassJavaDocEnhancer to synthesize both method-level and class-level Javadoc examples. + * methodName(() -> exampleValue)}). The fragment is stored on the BuilderMethodDto for later use + * by the ClassJavaDocEnhancer to synthesize both method-level and class-level Javadoc examples. * *

          If no example value is available for the field type, the fragment is not added. * From 7fd1eb9434395e979995730ece5e1ccb4feec5bd Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Wed, 5 Aug 2026 21:36:23 +0200 Subject: [PATCH 19/23] Undo unneeded changes --- example/pom.xml | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/example/pom.xml b/example/pom.xml index 75b0d56e..a1ad407a 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -72,26 +72,6 @@ - - org.codehaus.mojo - build-helper-maven-plugin - 3.6.0 - - - add-generated-source - generate-sources - - add-source - - - - ${project.basedir}/generated-example-builder - - - - - - org.apache.maven.plugins maven-compiler-plugin From e4fedb88f960ff337501b1fec61c9e8568d734af Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 5 Aug 2026 19:43:17 +0000 Subject: [PATCH 20/23] Fix Sonar findings in default value support Co-Authored-By: Andreas Igel --- .../builder/CoreMethodsEnhancer.java | 6 +- .../builders/processor/DefaultValueTest.java | 150 +++++++----------- 2 files changed, 56 insertions(+), 100 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java index 1484430b..e848e9fb 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java @@ -190,9 +190,9 @@ protected BuilderMethodDto createBuildMethod(BuilderDefinitionDto builderDto) { .append(".ifSet(result::") .append(field.getSetterName()) .append(")"); - if (field.getDefaultValue().isPresent()) { - code.append(".orElse(").append(field.getDefaultValue().get()).append(")"); - } + field + .getDefaultValue() + .ifPresent(defaultValue -> code.append(".orElse(").append(defaultValue).append(")")); code.append(";\n"); } diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/DefaultValueTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/DefaultValueTest.java index a19b9c7c..c4307074 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/DefaultValueTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/DefaultValueTest.java @@ -28,10 +28,14 @@ import static org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils.loadGeneratedSource; import com.google.testing.compile.Compilation; +import java.util.stream.Stream; 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; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; /** * Tests for default value support via {@code @Default} and third-party {@code @DefaultValue} @@ -43,21 +47,10 @@ protected Compilation compile(JavaFileObject... sourceFiles) { return ProcessorTestUtils.createCompiler().compile(sourceFiles); } - /** - * Verifies that a {@code @Default} annotation on a record constructor parameter causes the - * generated {@code build()} method to use {@code valueOr("GENERAL")} instead of {@code value()}, - * and that the field is not subject to required-field validation. - * - *

          Also verifies the builder setter method for the defaulted field is still generated, so users - * can override the default with an explicit value. - */ - @Test - void defaultAppliedWhenUnset_constructorField_record() { - String recordName = "ProductRecord"; - String builderClassName = recordName + "Builder"; - - JavaFileObject sourceFile = - ProcessorTestUtils.forSource( + private static Stream constructorDefaultCases() { + return Stream.of( + Arguments.of( + "ProductRecord", """ package test; @@ -69,41 +62,21 @@ public record ProductRecord( String name, double price, @Default("GENERAL") String category) {} - """); - - Compilation compilation = compile(sourceFile); - String generatedCode = loadGeneratedSource(compilation, builderClassName); - assertGenerationSucceeded(compilation, builderClassName, generatedCode); - - // build() must use valueOr with the quoted String default for the category field - ProcessorAsserts.assertContaining( - generatedCode, - """ - 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.valueOr("GENERAL")); - return result; - } - """); - } - - /** - * Verifies that {@code @Default} on primitive-typed record parameters ({@code double}, {@code - * int}) produces unquoted raw expressions in {@code valueOr()} calls, since primitives cannot be - * null and do not need string quoting. - */ - @Test - void primitiveDefault_constructorField_record() { - String recordName = "MetricRecord"; - String builderClassName = recordName + "Builder"; - - JavaFileObject sourceFile = - ProcessorTestUtils.forSource( + """, + """ + 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.valueOr("GENERAL")); + return result; + } + """), + Arguments.of( + "MetricRecord", """ package test; @@ -115,21 +88,40 @@ public record MetricRecord( String name, @Default("0.0") double price, @Default("0") int quantity) {} - """); - - Compilation compilation = compile(sourceFile); - String generatedCode = loadGeneratedSource(compilation, builderClassName); - assertGenerationSucceeded(compilation, builderClassName, generatedCode); - - // Primitives: raw expression (no quoting) - ProcessorAsserts.assertContaining( - generatedCode, - """ + """, + """ public MetricRecord build() { MetricRecord result = new MetricRecord(this.name.value(), this.price.valueOr(0.0), this.quantity.valueOr(0)); return result; } - """); + """), + Arguments.of( + "PlainRecord", + """ + package test; + + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + + @SimpleBuilder + public record PlainRecord(String name, Integer age) {} + """, + """ + public PlainRecord build() { + PlainRecord result = new PlainRecord(this.name.value(), this.age.value()); + return result; + } + """)); + } + + @ParameterizedTest + @MethodSource("constructorDefaultCases") + void constructorDefaultCases_generateExpectedBuildMethod( + String recordName, String source, String expectedBuildMethod) { + String builderClassName = recordName + "Builder"; + Compilation compilation = compile(ProcessorTestUtils.forSource(source)); + String generatedCode = loadGeneratedSource(compilation, builderClassName); + assertGenerationSucceeded(compilation, builderClassName, generatedCode); + ProcessorAsserts.assertContaining(generatedCode, expectedBuildMethod); } /** @@ -345,40 +337,4 @@ public JakartaRecord build() { } """); } - - /** - * Verifies that a record without any {@code @Default} annotations generates {@code - * build()} code using plain {@code value()} calls, with no {@code valueOr()} anywhere. This is a - * regression guard to ensure the default-value feature does not change behavior when not used. - */ - @Test - void noDefault_usesValueDirectly_constructorField() { - String recordName = "PlainRecord"; - String builderClassName = recordName + "Builder"; - - JavaFileObject sourceFile = - ProcessorTestUtils.forSource( - """ - package test; - - import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; - - @SimpleBuilder - public record PlainRecord(String name, Integer age) {} - """); - - Compilation compilation = compile(sourceFile); - String generatedCode = loadGeneratedSource(compilation, builderClassName); - assertGenerationSucceeded(compilation, builderClassName, generatedCode); - - // Without default, should use plain .value() - ProcessorAsserts.assertContaining( - generatedCode, - """ - public PlainRecord build() { - PlainRecord result = new PlainRecord(this.name.value(), this.age.value()); - return result; - } - """); - } } From 6910a4bd2363fcefcdb78a795f25e5f76080fedd Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Wed, 5 Aug 2026 22:09:40 +0200 Subject: [PATCH 21/23] Improving code quality --- .../processing/BuilderDefinitionCreator.java | 38 +++++++++++++------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java index e23e986f..7421595f 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java @@ -639,18 +639,7 @@ private static Optional createFieldFromSetter( // If no default was found on the setter parameter, check the field element itself // (annotations like @Default may be placed on the field rather than the setter param) if (result.isPresent() && result.get().getDefaultValue().isEmpty()) { - findFieldElement(dtoTypeElement, fieldName) - .ifPresent( - fieldElement -> - FieldAnnotationExtractor.extractAnnotationValue( - fieldElement, DEFAULT_ANNOTATION_NAMES) - .ifPresent( - rawDefault -> - result - .get() - .setDefaultValue( - FieldAnnotationExtractor.formatDefaultExpression( - rawDefault, result.get().getFieldType())))); + tryApplyDefaultFromField(result.get(), dtoTypeElement, fieldName); } if (result.isPresent()) { @@ -835,6 +824,31 @@ private static Optional createFieldDto( return Optional.of(field); } + /** + * Attempts to extract and apply a default value from the field declaration itself, if the field + * carries a recognized default annotation (e.g. {@code @Default}). Used as a fallback when no + * default was found on the setter parameter. + * + * @param field the field DTO to update with a default value if one is found + * @param dtoTypeElement the enclosing class element to search for the field + * @param fieldName the simple field name to look for + */ + private static void tryApplyDefaultFromField( + FieldDto field, TypeElement dtoTypeElement, String fieldName) { + Optional fieldElement = findFieldElement(dtoTypeElement, fieldName); + if (fieldElement.isEmpty()) { + return; + } + Optional rawDefault = + FieldAnnotationExtractor.extractAnnotationValue( + fieldElement.get(), DEFAULT_ANNOTATION_NAMES); + if (rawDefault.isEmpty()) { + return; + } + field.setDefaultValue( + FieldAnnotationExtractor.formatDefaultExpression(rawDefault.get(), field.getFieldType())); + } + /** * Finds a field element by name in the given class element. * From 6b1ea9225f272e70d245de2ef9c8c50ebbe12761 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Wed, 5 Aug 2026 22:39:20 +0200 Subject: [PATCH 22/23] Increasing code quality and coverage topics --- .../processing/BuilderDefinitionCreator.java | 14 ++++-- .../processor/BuilderProcessorTest.java | 46 +++++++++++++++++++ .../builders/processor/DefaultValueTest.java | 19 ++++++++ 3 files changed, 74 insertions(+), 5 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java index 7421595f..a722e76c 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java @@ -636,15 +636,19 @@ private static Optional createFieldFromSetter( builderType, context); + if (result.isEmpty()) { + return result; + } + + FieldDto field = result.get(); + // If no default was found on the setter parameter, check the field element itself // (annotations like @Default may be placed on the field rather than the setter param) - if (result.isPresent() && result.get().getDefaultValue().isEmpty()) { - tryApplyDefaultFromField(result.get(), dtoTypeElement, fieldName); + if (field.getDefaultValue().isEmpty()) { + tryApplyDefaultFromField(field, dtoTypeElement, fieldName); } - if (result.isPresent()) { - fieldNameRegistry.put(finalFieldName, result.get()); - } + fieldNameRegistry.put(finalFieldName, field); return result; } 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 27b6fde2..4090dbee 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 @@ -3626,4 +3626,50 @@ public class OrderDto { "public class OrderDtoFactory implements IBuilderBase", "@BuilderImplementation(forClass = Order.class)"); } + + /** + * Verifies that a setter with field-specific generics (e.g. {@code public void setValue(T + * value)}) is skipped by {@code createFieldFromSetter} and does not produce a builder field, + * while normal setters in the same class are still processed. + */ + @Test + void shouldSkipSetterWithFieldSpecificGenerics() { + String className = "GenericSetterDto"; + String builderClassName = className + "Builder"; + + JavaFileObject sourceFile = + ProcessorTestUtils.forSource( + """ + package test; + + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + + @SimpleBuilder + public class GenericSetterDto { + private String name; + private Object value; + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + + public Object getValue() { return value; } + public void setValue(T value) { this.value = value; } + } + """); + + Compilation compilation = compile(sourceFile); + assertThat(compilation).succeeded(); + String generatedCode = loadGeneratedSource(compilation, builderClassName); + + // The normal setter for "name" must produce a builder method + ProcessorAsserts.assertContaining( + generatedCode, "public GenericSetterDtoBuilder name(String name)"); + + // The generic setter for "value" must NOT produce a builder method + ProcessorAsserts.assertNotContaining(generatedCode, "public GenericSetterDtoBuilder value("); + + // Compilation should have a warning about the ignored generic setter + assertThat(compilation) + .hadWarningContaining("Field 'value' has field-specific generics, so it will be ignored"); + } } diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/DefaultValueTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/DefaultValueTest.java index c4307074..2fa5c7d6 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/DefaultValueTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/DefaultValueTest.java @@ -95,6 +95,25 @@ public MetricRecord build() { return result; } """), + Arguments.of( + "CharRecord", + """ + package test; + + import org.javahelpers.simple.builders.core.annotations.Default; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + + @SimpleBuilder + public record CharRecord( + String name, + @Default("X") char grade) {} + """, + """ + public CharRecord build() { + CharRecord result = new CharRecord(this.name.value(), this.grade.valueOr('X')); + return result; + } + """), Arguments.of( "PlainRecord", """ From 99b323633ed3ff0a41d40a282cec915441ff69b3 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Wed, 5 Aug 2026 22:51:33 +0200 Subject: [PATCH 23/23] Improving codequality --- .../analysis/FieldAnnotationExtractor.java | 25 ++----------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/FieldAnnotationExtractor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/FieldAnnotationExtractor.java index 4257aefa..3d4e1289 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/FieldAnnotationExtractor.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/FieldAnnotationExtractor.java @@ -334,33 +334,12 @@ private static Optional getValueMember(AnnotationMirror mirror) { * @return a formatted Java expression string suitable for code generation */ public static String formatDefaultExpression(String rawValue, TypeName fieldType) { - if (isStringType(fieldType)) { + if (fieldType.equals(TypeName.of(String.class))) { return "\"%s\"".formatted(rawValue); } - if (isCharType(fieldType)) { + if (fieldType.equals(TypeNamePrimitive.CHAR)) { return "'%s'".formatted(rawValue); } return rawValue; } - - /** - * Checks if the given type is {@code java.lang.String}. - * - * @param type the type to check - * @return {@code true} if the type is String - */ - private static boolean isStringType(TypeName type) { - return "java.lang".equals(type.getPackageName()) && "String".equals(type.getClassName()); - } - - /** - * Checks if the given type is the {@code char} primitive. - * - * @param type the type to check - * @return {@code true} if the type is char - */ - private static boolean isCharType(TypeName type) { - return type instanceof TypeNamePrimitive primitive - && primitive.getType() == TypeNamePrimitive.PrimitiveTypeEnum.CHAR; - } }