diff --git a/README.md b/README.md
index 567ef660..9646c7db 100644
--- a/README.md
+++ b/README.md
@@ -152,18 +152,19 @@ public enum ProjectStatus {
Usage with collections and nested builders:
```java
+String version = "1.0.0";
Project project = ProjectBuilder.create()
- .name("Simple Builders with a bit of complexity")
+ .name("Simple Builders in version '%s' with a bit of complexity", version)
.status(ProjectStatus.IN_PROGRESS)
.tasks(tasks -> tasks
- .add(TaskBuilder.create()
+ .add(taskBuilder -> taskBuilder
.title("Implement core functionality")
.completed(true)
- .build())
- .add(TaskBuilder.create()
+ )
+ .add(taskBuilder -> taskBuilder
.title("Add documentation")
.description("Update README and add Javadocs")
- .build())
+ )
)
.metadata(metadata -> metadata
.put("version", "1.0.0")
diff --git a/core/src/main/java/org/javahelpers/simple/builders/core/builders/ArrayListBuilderWithElementBuilders.java b/core/src/main/java/org/javahelpers/simple/builders/core/builders/ArrayListBuilderWithElementBuilders.java
new file mode 100644
index 00000000..3354e27c
--- /dev/null
+++ b/core/src/main/java/org/javahelpers/simple/builders/core/builders/ArrayListBuilderWithElementBuilders.java
@@ -0,0 +1,112 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2025 Andreas Igel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.javahelpers.simple.builders.core.builders;
+
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+import org.javahelpers.simple.builders.core.interfaces.IBuilderBase;
+
+/**
+ * Enhanced builder for Lists that supports adding elements via builder consumers. This class
+ * extends {@link ArrayListBuilder} and adds methods to accept {@link Consumer} functions that
+ * configure element builders, eliminating the need to manually call {@code build()} on each element
+ * builder.
+ *
+ *
Example usage:
+ *
+ *
{@code
+ * List people = new ArrayListBuilderWithElementBuilders<>(PersonBuilder::create)
+ * .add(personBuilder -> personBuilder
+ * .name("John Doe")
+ * .age(30))
+ * .add(personBuilder -> personBuilder
+ * .name("Jane Smith")
+ * .age(25))
+ * .build();
+ * }
+ *
+ * @param the type of elements in the targeting list
+ * @param the type of builder used to create elements, must implement {@link IBuilderBase} and
+ * building objects of type T
+ */
+public class ArrayListBuilderWithElementBuilders>
+ extends ArrayListBuilder {
+ private final Supplier elementBuilderProvider;
+
+ /**
+ * Creates an empty ArrayListBuilderWithElementBuilders.
+ *
+ * @param elementBuilderProvider supplier that provides new instances of element builders
+ */
+ public ArrayListBuilderWithElementBuilders(Supplier elementBuilderProvider) {
+ this.elementBuilderProvider = elementBuilderProvider;
+ }
+
+ /**
+ * Creates an ArrayListBuilderWithElementBuilders initialized with the elements from the given
+ * list.
+ *
+ * @param initialList list to initialize from
+ * @param elementBuilderProvider supplier that provides new instances of element builders
+ */
+ public ArrayListBuilderWithElementBuilders(
+ List initialList, Supplier elementBuilderProvider) {
+ super(initialList);
+ this.elementBuilderProvider = elementBuilderProvider;
+ }
+
+ /**
+ * Adds an element to the list by providing a consumer that configures an element builder. The
+ * builder is automatically created using the elementBuilderProvider and built after
+ * configuration.
+ *
+ * @param elementBuilderConsumer consumer that configures the element builder
+ * @return this builder instance for method chaining
+ */
+ public ArrayListBuilderWithElementBuilders add(Consumer elementBuilderConsumer) {
+ B elementBuilder = elementBuilderProvider.get();
+ elementBuilderConsumer.accept(elementBuilder);
+ super.add(elementBuilder.build());
+ return this;
+ }
+
+ /**
+ * Adds multiple elements to the list by providing consumers that configure element builders. Each
+ * builder is automatically created using the elementBuilderProvider and built after
+ * configuration.
+ *
+ * @param elementBuilderConsumers varargs of consumers that configure element builders
+ * @return this builder instance for method chaining
+ */
+ @SafeVarargs
+ public final ArrayListBuilderWithElementBuilders add(
+ Consumer... elementBuilderConsumers) {
+ for (Consumer elementBuilderConsumer : elementBuilderConsumers) {
+ this.add(elementBuilderConsumer);
+ }
+ return this;
+ }
+}
diff --git a/core/src/main/java/org/javahelpers/simple/builders/core/builders/HashSetBuilderWithElementBuilders.java b/core/src/main/java/org/javahelpers/simple/builders/core/builders/HashSetBuilderWithElementBuilders.java
new file mode 100644
index 00000000..3176f390
--- /dev/null
+++ b/core/src/main/java/org/javahelpers/simple/builders/core/builders/HashSetBuilderWithElementBuilders.java
@@ -0,0 +1,108 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2025 Andreas Igel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.javahelpers.simple.builders.core.builders;
+
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+import org.javahelpers.simple.builders.core.interfaces.IBuilderBase;
+
+/**
+ * Enhanced builder for Sets that supports adding elements via builder consumers. This class extends
+ * {@link HashSetBuilder} and adds methods to accept {@link Consumer} functions that configure
+ * element builders, eliminating the need to manually call {@code build()} on each element builder.
+ *
+ * Example usage:
+ *
+ *
{@code
+ * Set people = new HashSetBuilderWithElementBuilders<>(PersonBuilder::create)
+ * .add(personBuilder -> personBuilder
+ * .name("John Doe")
+ * .age(30))
+ * .add(personBuilder -> personBuilder
+ * .name("Jane Smith")
+ * .age(25))
+ * .build();
+ * }
+ *
+ * @param the type of elements in the targeting set
+ * @param the type of builder used to create elements, must implement {@link IBuilderBase} and
+ * building objects of type T
+ */
+public class HashSetBuilderWithElementBuilders>
+ extends HashSetBuilder {
+ private final Supplier elementBuilderProvider;
+
+ /**
+ * Creates an empty HashSetBuilderWithElementBuilders.
+ *
+ * @param elementBuilderProvider supplier that provides new instances of element builders
+ */
+ public HashSetBuilderWithElementBuilders(Supplier elementBuilderProvider) {
+ this.elementBuilderProvider = elementBuilderProvider;
+ }
+
+ /**
+ * Creates a HashSetBuilderWithElementBuilders initialized with the elements from the given set.
+ *
+ * @param initialSet set to initialize from
+ * @param elementBuilderProvider supplier that provides new instances of element builders
+ */
+ public HashSetBuilderWithElementBuilders(Set initialSet, Supplier elementBuilderProvider) {
+ super(initialSet);
+ this.elementBuilderProvider = elementBuilderProvider;
+ }
+
+ /**
+ * Adds an element to the set by providing a consumer that configures an element builder. The
+ * builder is automatically created using the elementBuilderProvider and built after
+ * configuration.
+ *
+ * @param elementBuilderConsumer consumer that configures the element builder
+ * @return this builder instance for method chaining
+ */
+ public HashSetBuilderWithElementBuilders add(Consumer elementBuilderConsumer) {
+ B elementBuilder = elementBuilderProvider.get();
+ elementBuilderConsumer.accept(elementBuilder);
+ super.add(elementBuilder.build());
+ return this;
+ }
+
+ /**
+ * Adds multiple elements to the set by providing consumers that configure element builders. Each
+ * builder is automatically created using the elementBuilderProvider and built after
+ * configuration.
+ *
+ * @param elementBuilderConsumers varargs of consumers that configure element builders
+ * @return this builder instance for method chaining
+ */
+ @SafeVarargs
+ public final HashSetBuilderWithElementBuilders add(Consumer... elementBuilderConsumers) {
+ for (Consumer elementBuilderConsumer : elementBuilderConsumers) {
+ this.add(elementBuilderConsumer);
+ }
+ return this;
+ }
+}
diff --git a/core/src/test/java/org/javahelpers/simple/builders/core/builders/ArrayListBuilderWithElementBuildersTest.java b/core/src/test/java/org/javahelpers/simple/builders/core/builders/ArrayListBuilderWithElementBuildersTest.java
new file mode 100644
index 00000000..b85d1ab0
--- /dev/null
+++ b/core/src/test/java/org/javahelpers/simple/builders/core/builders/ArrayListBuilderWithElementBuildersTest.java
@@ -0,0 +1,197 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2025 Andreas Igel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.javahelpers.simple.builders.core.builders;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Arrays;
+import java.util.List;
+import org.javahelpers.simple.builders.core.interfaces.IBuilderBase;
+import org.junit.jupiter.api.Test;
+
+class ArrayListBuilderWithElementBuildersTest {
+
+ @Test
+ void shouldCreateEmptyBuilder() {
+ ArrayListBuilderWithElementBuilders builder =
+ new ArrayListBuilderWithElementBuilders<>(TestPersonBuilder::new);
+ List result = builder.build();
+
+ assertNotNull(result);
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ void shouldCreateBuilderWithInitialList() {
+ List initial =
+ Arrays.asList(new TestPerson("Alice", 30), new TestPerson("Bob", 25));
+
+ ArrayListBuilderWithElementBuilders builder =
+ new ArrayListBuilderWithElementBuilders<>(initial, TestPersonBuilder::new);
+ List result = builder.build();
+
+ assertNotNull(result);
+ assertEquals(2, result.size());
+ assertEquals("Alice", result.get(0).name);
+ assertEquals(30, result.get(0).age);
+ assertEquals("Bob", result.get(1).name);
+ assertEquals(25, result.get(1).age);
+ }
+
+ @Test
+ void shouldAddSingleElementViaConsumer() {
+ ArrayListBuilderWithElementBuilders builder =
+ new ArrayListBuilderWithElementBuilders<>(TestPersonBuilder::new);
+
+ builder.add(person -> person.name("John").age(35));
+ List result = builder.build();
+
+ assertEquals(1, result.size());
+ assertEquals("John", result.get(0).name);
+ assertEquals(35, result.get(0).age);
+ }
+
+ @Test
+ void shouldAddMultipleElementsViaConsumers() {
+ ArrayListBuilderWithElementBuilders builder =
+ new ArrayListBuilderWithElementBuilders<>(TestPersonBuilder::new);
+
+ builder
+ .add(person -> person.name("Alice").age(30))
+ .add(person -> person.name("Bob").age(25))
+ .add(person -> person.name("Charlie").age(40));
+
+ List result = builder.build();
+
+ assertEquals(3, result.size());
+ assertEquals("Alice", result.get(0).name);
+ assertEquals(30, result.get(0).age);
+ assertEquals("Bob", result.get(1).name);
+ assertEquals(25, result.get(1).age);
+ assertEquals("Charlie", result.get(2).name);
+ assertEquals(40, result.get(2).age);
+ }
+
+ @Test
+ void shouldAddMultipleElementsViaVarargs() {
+ ArrayListBuilderWithElementBuilders builder =
+ new ArrayListBuilderWithElementBuilders<>(TestPersonBuilder::new);
+
+ builder.add(
+ person -> person.name("Alice").age(30),
+ person -> person.name("Bob").age(25),
+ person -> person.name("Charlie").age(40));
+
+ List result = builder.build();
+
+ assertEquals(3, result.size());
+ assertEquals("Alice", result.get(0).name);
+ assertEquals(30, result.get(0).age);
+ assertEquals("Bob", result.get(1).name);
+ assertEquals(25, result.get(1).age);
+ assertEquals("Charlie", result.get(2).name);
+ assertEquals(40, result.get(2).age);
+ }
+
+ @Test
+ void shouldCombineInitialListAndConsumers() {
+ List initial = Arrays.asList(new TestPerson("Initial", 20));
+
+ ArrayListBuilderWithElementBuilders builder =
+ new ArrayListBuilderWithElementBuilders<>(initial, TestPersonBuilder::new);
+
+ builder.add(person -> person.name("Added").age(30));
+
+ List result = builder.build();
+
+ assertEquals(2, result.size());
+ assertEquals("Initial", result.get(0).name);
+ assertEquals(20, result.get(0).age);
+ assertEquals("Added", result.get(1).name);
+ assertEquals(30, result.get(1).age);
+ }
+
+ @Test
+ void shouldReturnThisForFluentInterface() {
+ ArrayListBuilderWithElementBuilders builder =
+ new ArrayListBuilderWithElementBuilders<>(TestPersonBuilder::new);
+
+ ArrayListBuilderWithElementBuilders result =
+ builder.add(person -> person.name("Test").age(25));
+
+ assertSame(builder, result);
+ }
+
+ @Test
+ void shouldAllowMixingConsumerAndDirectAdd() {
+ ArrayListBuilderWithElementBuilders builder =
+ new ArrayListBuilderWithElementBuilders<>(TestPersonBuilder::new);
+
+ builder.add(person -> person.name("Consumer").age(30)).add(new TestPerson("Direct", 25));
+
+ List result = builder.build();
+
+ assertEquals(2, result.size());
+ assertEquals("Consumer", result.get(0).name);
+ assertEquals(30, result.get(0).age);
+ assertEquals("Direct", result.get(1).name);
+ assertEquals(25, result.get(1).age);
+ }
+
+ // Test helper classes
+ static class TestPerson {
+ String name;
+ int age;
+
+ TestPerson() {}
+
+ TestPerson(String name, int age) {
+ this.name = name;
+ this.age = age;
+ }
+ }
+
+ static class TestPersonBuilder implements IBuilderBase {
+ private final TestPerson person = new TestPerson();
+
+ TestPersonBuilder name(String name) {
+ person.name = name;
+ return this;
+ }
+
+ TestPersonBuilder age(int age) {
+ person.age = age;
+ return this;
+ }
+
+ @Override
+ public TestPerson build() {
+ return person;
+ }
+ }
+}
diff --git a/core/src/test/java/org/javahelpers/simple/builders/core/builders/HashSetBuilderWithElementBuildersTest.java b/core/src/test/java/org/javahelpers/simple/builders/core/builders/HashSetBuilderWithElementBuildersTest.java
new file mode 100644
index 00000000..df010d93
--- /dev/null
+++ b/core/src/test/java/org/javahelpers/simple/builders/core/builders/HashSetBuilderWithElementBuildersTest.java
@@ -0,0 +1,216 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2025 Andreas Igel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.javahelpers.simple.builders.core.builders;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+import org.javahelpers.simple.builders.core.interfaces.IBuilderBase;
+import org.junit.jupiter.api.Test;
+
+class HashSetBuilderWithElementBuildersTest {
+
+ @Test
+ void shouldCreateEmptyBuilder() {
+ HashSetBuilderWithElementBuilders builder =
+ new HashSetBuilderWithElementBuilders<>(TestPersonBuilder::new);
+ Set result = builder.build();
+
+ assertNotNull(result);
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ void shouldCreateBuilderWithInitialSet() {
+ Set initial =
+ new HashSet<>(Arrays.asList(new TestPerson("Alice", 30), new TestPerson("Bob", 25)));
+
+ HashSetBuilderWithElementBuilders builder =
+ new HashSetBuilderWithElementBuilders<>(initial, TestPersonBuilder::new);
+ Set result = builder.build();
+
+ assertNotNull(result);
+ assertEquals(2, result.size());
+ assertTrue(result.contains(new TestPerson("Alice", 30)));
+ assertTrue(result.contains(new TestPerson("Bob", 25)));
+ }
+
+ @Test
+ void shouldAddSingleElementViaConsumer() {
+ HashSetBuilderWithElementBuilders builder =
+ new HashSetBuilderWithElementBuilders<>(TestPersonBuilder::new);
+
+ builder.add(person -> person.name("John").age(35));
+ Set result = builder.build();
+
+ assertEquals(1, result.size());
+ assertTrue(result.contains(new TestPerson("John", 35)));
+ }
+
+ @Test
+ void shouldAddMultipleElementsViaConsumers() {
+ HashSetBuilderWithElementBuilders builder =
+ new HashSetBuilderWithElementBuilders<>(TestPersonBuilder::new);
+
+ builder
+ .add(person -> person.name("Alice").age(30))
+ .add(person -> person.name("Bob").age(25))
+ .add(person -> person.name("Charlie").age(40));
+
+ Set result = builder.build();
+
+ assertEquals(3, result.size());
+ assertTrue(result.contains(new TestPerson("Alice", 30)));
+ assertTrue(result.contains(new TestPerson("Bob", 25)));
+ assertTrue(result.contains(new TestPerson("Charlie", 40)));
+ }
+
+ @Test
+ void shouldAddMultipleElementsViaVarargs() {
+ HashSetBuilderWithElementBuilders builder =
+ new HashSetBuilderWithElementBuilders<>(TestPersonBuilder::new);
+
+ builder.add(
+ person -> person.name("Alice").age(30),
+ person -> person.name("Bob").age(25),
+ person -> person.name("Charlie").age(40));
+
+ Set result = builder.build();
+
+ assertEquals(3, result.size());
+ assertTrue(result.contains(new TestPerson("Alice", 30)));
+ assertTrue(result.contains(new TestPerson("Bob", 25)));
+ assertTrue(result.contains(new TestPerson("Charlie", 40)));
+ }
+
+ @Test
+ void shouldCombineInitialSetAndConsumers() {
+ Set initial = new HashSet<>(Arrays.asList(new TestPerson("Initial", 20)));
+
+ HashSetBuilderWithElementBuilders builder =
+ new HashSetBuilderWithElementBuilders<>(initial, TestPersonBuilder::new);
+
+ builder.add(person -> person.name("Added").age(30));
+
+ Set result = builder.build();
+
+ assertEquals(2, result.size());
+ assertTrue(result.contains(new TestPerson("Initial", 20)));
+ assertTrue(result.contains(new TestPerson("Added", 30)));
+ }
+
+ @Test
+ void shouldReturnThisForFluentInterface() {
+ HashSetBuilderWithElementBuilders builder =
+ new HashSetBuilderWithElementBuilders<>(TestPersonBuilder::new);
+
+ HashSetBuilderWithElementBuilders result =
+ builder.add(person -> person.name("Test").age(25));
+
+ assertSame(builder, result);
+ }
+
+ @Test
+ void shouldAllowMixingConsumerAndDirectAdd() {
+ HashSetBuilderWithElementBuilders builder =
+ new HashSetBuilderWithElementBuilders<>(TestPersonBuilder::new);
+
+ builder.add(person -> person.name("Consumer").age(30)).add(new TestPerson("Direct", 25));
+
+ Set result = builder.build();
+
+ assertEquals(2, result.size());
+ assertTrue(result.contains(new TestPerson("Consumer", 30)));
+ assertTrue(result.contains(new TestPerson("Direct", 25)));
+ }
+
+ @Test
+ void shouldHandleDuplicateElementsFromConsumers() {
+ HashSetBuilderWithElementBuilders builder =
+ new HashSetBuilderWithElementBuilders<>(TestPersonBuilder::new);
+
+ builder
+ .add(person -> person.name("Duplicate").age(30))
+ .add(person -> person.name("Duplicate").age(30))
+ .add(person -> person.name("Unique").age(25));
+
+ Set result = builder.build();
+
+ assertEquals(2, result.size());
+ assertTrue(result.contains(new TestPerson("Duplicate", 30)));
+ assertTrue(result.contains(new TestPerson("Unique", 25)));
+ }
+
+ // Test helper classes
+ static class TestPerson {
+ String name;
+ int age;
+
+ TestPerson() {}
+
+ TestPerson(String name, int age) {
+ this.name = name;
+ this.age = age;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ TestPerson that = (TestPerson) o;
+ return age == that.age && Objects.equals(name, that.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name, age);
+ }
+ }
+
+ static class TestPersonBuilder implements IBuilderBase {
+ private final TestPerson person = new TestPerson();
+
+ TestPersonBuilder name(String name) {
+ person.name = name;
+ return this;
+ }
+
+ TestPersonBuilder age(int age) {
+ person.age = age;
+ return this;
+ }
+
+ @Override
+ public TestPerson build() {
+ return person;
+ }
+ }
+}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java
index 1499b520..f2cab96f 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java
@@ -34,20 +34,22 @@
import java.util.LinkedList;
import java.util.List;
+import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Supplier;
import javax.lang.model.element.*;
import javax.lang.model.type.TypeMirror;
-import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Strings;
import org.javahelpers.simple.builders.core.annotations.IgnoreInBuilder;
import org.javahelpers.simple.builders.core.annotations.SimpleBuilder;
import org.javahelpers.simple.builders.core.builders.ArrayListBuilder;
+import org.javahelpers.simple.builders.core.builders.ArrayListBuilderWithElementBuilders;
import org.javahelpers.simple.builders.core.builders.HashMapBuilder;
import org.javahelpers.simple.builders.core.builders.HashSetBuilder;
+import org.javahelpers.simple.builders.core.builders.HashSetBuilderWithElementBuilders;
import org.javahelpers.simple.builders.core.util.TrackedValue;
import org.javahelpers.simple.builders.processor.dtos.*;
import org.javahelpers.simple.builders.processor.exceptions.BuilderException;
@@ -293,51 +295,157 @@ private static void addConsumerMethodsForField(
return;
}
- Optional builderTypeOpt = findBuilderType(fieldParameter, context);
+ if (!tryAddBuilderConsumer(result, fieldName, fieldParameter, context)
+ && !tryAddFieldConsumer(result, fieldName, fieldType, fieldTypeElement, context)
+ && !tryAddListConsumer(result, fieldName, fieldType, fieldParameter, context)
+ && !tryAddMapConsumer(result, fieldName, fieldType)
+ && !tryAddSetConsumer(result, fieldName, fieldType, fieldParameter, context)) {
+ tryAddStringBuilderConsumer(result, fieldName, fieldType);
+ }
+ }
+
+ /** Tries to add a direct builder-based consumer when the field type itself has a builder. */
+ private static boolean tryAddBuilderConsumer(
+ FieldDto result,
+ String fieldName,
+ VariableElement fieldParameter,
+ ProcessingContext context) {
+ Optional builderTypeOpt = resolveBuilderType(fieldParameter, context);
if (builderTypeOpt.isPresent()) {
TypeName builderType = builderTypeOpt.get();
result.addMethod(
BuilderDefinitionCreator.createFieldConsumerWithBuilder(fieldName, builderType));
- } else if (!isJavaClass(fieldType)
+ return true;
+ }
+ return false;
+ }
+
+ /** Tries to add a consumer using an empty constructor of a concrete non-java class. */
+ private static boolean tryAddFieldConsumer(
+ FieldDto result,
+ String fieldName,
+ TypeName fieldType,
+ TypeElement fieldTypeElement,
+ ProcessingContext context) {
+ if (!isJavaClass(fieldType)
&& fieldTypeElement != null
&& fieldTypeElement.getKind() == javax.lang.model.element.ElementKind.CLASS
&& !fieldTypeElement.getModifiers().contains(Modifier.ABSTRACT)
&& hasEmptyConstructor(fieldTypeElement, context)) {
// Only generate a Consumer for concrete classes with an accessible empty constructor
result.addMethod(createFieldConsumer(fieldName, fieldType));
- } else if (isList(fieldType)
+ return true;
+ }
+ return false;
+ }
+
+ /** Tries to add StringBuilder-based consumer for String and Optional. */
+ private static boolean tryAddStringBuilderConsumer(
+ FieldDto result, String fieldName, TypeName fieldType) {
+ if (shouldGenerateStringBuilderConsumer(fieldType)) {
+ String transform =
+ isOptionalString(fieldType) ? "Optional.of(builder.toString())" : "builder.toString()";
+ result.addMethod(createStringBuilderConsumer(fieldName, transform));
+ return true;
+ }
+ return false;
+ }
+
+ /** Tries to add List-specific consumer methods. Returns true if handled. */
+ private static boolean tryAddListConsumer(
+ FieldDto result,
+ String fieldName,
+ TypeName fieldType,
+ VariableElement fieldParameter,
+ ProcessingContext context) {
+ if (!(isList(fieldType)
&& fieldType instanceof TypeNameGeneric fieldTypeGeneric
- && fieldTypeGeneric.getInnerTypeArguments().size() == 1) {
+ && fieldTypeGeneric.getInnerTypeArguments().size() == 1)) {
+ return false;
+ }
+
+ TypeName elementType = fieldTypeGeneric.getInnerTypeArguments().get(0);
+
+ // Get the TypeMirror of the element type from the parameter's type
+ TypeMirror fieldTypeMirror = fieldParameter.asType();
+ TypeMirror elementTypeMirror = extractFirstTypeArgument(fieldTypeMirror);
+
+ Optional elementBuilderType =
+ resolveBuilderType(elementType, elementTypeMirror, context);
+
+ if (elementBuilderType.isPresent()) {
+ // Element type has a builder - use ArrayListBuilderWithElementBuilders
+ TypeName builderType =
+ new TypeNameGeneric(
+ map2TypeName(ArrayListBuilderWithElementBuilders.class),
+ elementType,
+ elementBuilderType.get());
result.addMethod(
- createFieldConsumerWithBuilder(
- fieldName,
- map2TypeName(ArrayListBuilder.class),
- fieldTypeGeneric.getInnerTypeArguments().get(0)));
- } else if (isMap(fieldType)
+ createFieldConsumerWithElementBuilders(fieldName, builderType, elementBuilderType.get()));
+ } else {
+ // Regular ArrayListBuilder
+ TypeName builderType = map2TypeName(ArrayListBuilder.class);
+ result.addMethod(createFieldConsumerWithBuilder(fieldName, builderType, elementType));
+ }
+ return true;
+ }
+
+ /** Tries to add Map-specific consumer methods. Returns true if handled. */
+ private static boolean tryAddMapConsumer(FieldDto result, String fieldName, TypeName fieldType) {
+ if (!(isMap(fieldType)
&& fieldType instanceof TypeNameGeneric fieldTypeGeneric
- && fieldTypeGeneric.getInnerTypeArguments().size() == 2) {
- TypeName builderTargetTypeName =
- new TypeNameGeneric(
- map2TypeName(HashMapBuilder.class),
- fieldTypeGeneric.getInnerTypeArguments().get(0),
- fieldTypeGeneric.getInnerTypeArguments().get(1));
- MethodDto mapConsumerWithBuilder =
- BuilderDefinitionCreator.createFieldConsumerWithBuilder(fieldName, builderTargetTypeName);
- result.addMethod(mapConsumerWithBuilder);
- } else if (isSet(fieldType)
+ && fieldTypeGeneric.getInnerTypeArguments().size() == 2)) {
+ return false;
+ }
+
+ TypeName builderTargetTypeName =
+ new TypeNameGeneric(
+ map2TypeName(HashMapBuilder.class),
+ fieldTypeGeneric.getInnerTypeArguments().get(0),
+ fieldTypeGeneric.getInnerTypeArguments().get(1));
+ MethodDto mapConsumerWithBuilder =
+ BuilderDefinitionCreator.createFieldConsumerWithBuilder(fieldName, builderTargetTypeName);
+ result.addMethod(mapConsumerWithBuilder);
+ return true;
+ }
+
+ /** Tries to add Set-specific consumer methods. Returns true if handled. */
+ private static boolean tryAddSetConsumer(
+ FieldDto result,
+ String fieldName,
+ TypeName fieldType,
+ VariableElement fieldParameter,
+ ProcessingContext context) {
+ if (!(isSet(fieldType)
&& fieldType instanceof TypeNameGeneric fieldTypeGeneric
- && fieldTypeGeneric.getInnerTypeArguments().size() == 1) {
+ && fieldTypeGeneric.getInnerTypeArguments().size() == 1)) {
+ return false;
+ }
+
+ TypeName elementType = fieldTypeGeneric.getInnerTypeArguments().get(0);
+
+ // Get the TypeMirror of the element type from the parameter's type
+ TypeMirror fieldTypeMirror = fieldParameter.asType();
+ TypeMirror elementTypeMirror = extractFirstTypeArgument(fieldTypeMirror);
+
+ Optional elementBuilderType =
+ resolveBuilderType(elementType, elementTypeMirror, context);
+
+ if (elementBuilderType.isPresent()) {
+ // Element type has a builder - use HashSetBuilderWithElementBuilders
+ TypeName builderType =
+ new TypeNameGeneric(
+ map2TypeName(HashSetBuilderWithElementBuilders.class),
+ elementType,
+ elementBuilderType.get());
result.addMethod(
- createFieldConsumerWithBuilder(
- fieldName,
- map2TypeName(HashSetBuilder.class),
- fieldTypeGeneric.getInnerTypeArguments().get(0)));
- } else if (shouldGenerateStringBuilderConsumer(fieldType)) {
- // Generate StringBuilder consumer for String or Optional
- String transform =
- isOptionalString(fieldType) ? "Optional.of(builder.toString())" : "builder.toString()";
- result.addMethod(createStringBuilderConsumer(fieldName, transform));
+ createFieldConsumerWithElementBuilders(fieldName, builderType, elementBuilderType.get()));
+ } else {
+ // Regular HashSetBuilder
+ TypeName builderType = map2TypeName(HashSetBuilder.class);
+ result.addMethod(createFieldConsumerWithBuilder(fieldName, builderType, elementType));
}
+ return true;
}
private static void addSupplierMethodsForField(
@@ -367,7 +475,7 @@ private static Optional createFieldFromSetter(
// Serializable>
// If there are field-specific generics, no field in builder could be generated for it, so it
// needs to be ignored
- if (CollectionUtils.isNotEmpty(mth.getTypeParameters())) {
+ if (JavaLangAnalyser.hasGenericTypes(mth)) {
context.warning(
mth.getEnclosingElement(),
"Field '%s' has field-specific generics, so it will be ignored",
@@ -534,6 +642,41 @@ private static MethodDto createFieldConsumerWithBuilder(
}
private static MethodDto createFieldConsumerWithBuilder(String fieldName, TypeName builderType) {
+ return createFieldConsumerWithBuilder(
+ fieldName, builderType, "this.$fieldName:N.value()", "", Map.of());
+ }
+
+ /**
+ * Creates a consumer method for collection builders with element builders. Used for
+ * ArrayListBuilderWithElementBuilders and HashSetBuilderWithElementBuilders.
+ */
+ private static MethodDto createFieldConsumerWithElementBuilders(
+ String fieldName, TypeName collectionBuilderType, TypeName elementBuilderType) {
+ return createFieldConsumerWithBuilder(
+ fieldName,
+ collectionBuilderType,
+ "this.$fieldName:N.value(), $elementBuilderType:T::create",
+ "$elementBuilderType:T::create",
+ Map.of("elementBuilderType", elementBuilderType));
+ }
+
+ /**
+ * Creates a consumer method for a field with a builder type.
+ *
+ * @param fieldName the field name
+ * @param builderType the builder type (e.g., ArrayListBuilder or
+ * ArrayListBuilderWithElementBuilders)
+ * @param constructorArgsWithValue constructor arguments when field is already set
+ * @param constructorArgsEmpty constructor arguments when field is empty
+ * @param additionalArguments additional template arguments to add to the method (must be TypeName
+ * values)
+ */
+ private static MethodDto createFieldConsumerWithBuilder(
+ String fieldName,
+ TypeName builderType,
+ String constructorArgsWithValue,
+ String constructorArgsEmpty,
+ Map additionalArguments) {
TypeNameGeneric consumerType = new TypeNameGeneric(map2TypeName(Consumer.class), builderType);
MethodParameterDto parameter = new MethodParameterDto();
parameter.setParameterName(fieldName + BUILDER_SUFFIX + SUFFIX_CONSUMER);
@@ -545,14 +688,16 @@ private static MethodDto createFieldConsumerWithBuilder(String fieldName, TypeNa
methodDto.setMethodType(MethodTypes.CONSUMER_BY_BUILDER);
methodDto.setCode(
"""
- $helperType:T builder = this.$fieldName:N.isSet() ? new $helperType:T(this.$fieldName:N.value()) : new $helperType:T();
+ $helperType:T builder = this.$fieldName:N.isSet() ? new $helperType:T(%s) : new $helperType:T(%s);
$dtoMethodParam:N.accept(builder);
this.$fieldName:N = $builderFieldWrapper:T.changedValue(builder.build());
return this;
- """);
+ """
+ .formatted(constructorArgsWithValue, constructorArgsEmpty));
methodDto.addArgument(ARG_FIELD_NAME, fieldName);
methodDto.addArgument(ARG_DTO_METHOD_PARAM, parameter.getParameterName());
methodDto.addArgument(ARG_HELPER_TYPE, builderType);
+ additionalArguments.forEach(methodDto::addArgument);
methodDto.addArgument(ARG_BUILDER_FIELD_WRAPPER, TRACKED_VALUE_TYPE);
return methodDto;
}
@@ -624,25 +769,97 @@ private static boolean shouldGenerateStringBuilderConsumer(TypeName fieldType) {
return isOptionalString(fieldType);
}
- private static Optional findBuilderType(
+ private static Optional resolveBuilderType(
VariableElement param, ProcessingContext context) {
TypeMirror typeOfParameter = param.asType();
if (typeOfParameter.getKind() == ARRAY || typeOfParameter.getKind().isPrimitive()) {
return Optional.empty();
}
Element elementOfParameter = context.asElement(typeOfParameter);
- if (elementOfParameter == null) {
+ if (!(elementOfParameter instanceof TypeElement typeElement)) {
// Can happen for primitives or certain compiler-internal types; nothing to build
return Optional.empty();
}
- String simpleClassName = elementOfParameter.getSimpleName().toString();
- String packageName = context.getPackageName(elementOfParameter);
+ // For direct field types disallow generics here in the caller
+ if (JavaLangAnalyser.hasGenericTypes(typeElement)) {
+ context.debug(
+ " -> Skipping builder lookup for generic type %s", typeElement.getSimpleName());
+ return Optional.empty();
+ }
+ return resolveBuilderTypeFromTypeElement(typeElement, context);
+ }
+
+ /**
+ * Finds the builder type for a given element type (used for collection elements).
+ *
+ * @param elementType the type of the collection element
+ * @param elementTypeMirror the TypeMirror of the element type (from the parameter)
+ * @param context processing context
+ * @return Optional containing the builder TypeName if the element type has @SimpleBuilder
+ */
+ private static Optional resolveBuilderType(
+ TypeName elementType, TypeMirror elementTypeMirror, ProcessingContext context) {
+
+ // Skip cases without type mirror
+ if (elementTypeMirror == null) {
+ return Optional.empty();
+ }
+
+ // Skip type variables and primitives
+ if (elementType instanceof TypeNameVariable || elementType instanceof TypeNamePrimitive) {
+ context.debug(" -> Skipping type variable or primitive: %s", elementType);
+ return Optional.empty();
+ }
+
+ // Try to get the element from the TypeMirror
+ Element element = context.asElement(elementTypeMirror);
+ if (!(element instanceof TypeElement typeElement)) {
+ context.debug(" -> Element is not a TypeElement: %s", element);
+ return Optional.empty();
+ }
+ // For collection element types: delegate to shared helper
+ return resolveBuilderTypeFromTypeElement(typeElement, context);
+ }
+
+ /**
+ * Shared helper resolving a builder {@link TypeName} from a {@link TypeElement} if it is
+ * annotated with {@code @SimpleBuilder}. Generics policy is enforced by the callers.
+ *
+ * @param typeElement the type element to inspect
+ * @param context processing context
+ */
+ private static Optional resolveBuilderTypeFromTypeElement(
+ TypeElement typeElement, ProcessingContext context) {
+ // Check annotation presence first
Optional foundBuilderAnnotation =
- findAnnotation(elementOfParameter, SimpleBuilder.class);
- boolean isClassWithoutGenerics = StringUtils.containsNone(simpleClassName, "<");
- if (foundBuilderAnnotation.isPresent() && isClassWithoutGenerics) {
- return Optional.of(new TypeName(packageName, simpleClassName + BUILDER_SUFFIX));
+ findAnnotation(typeElement, SimpleBuilder.class);
+ if (foundBuilderAnnotation.isEmpty()) {
+ context.debug(" -> Type %s has no @SimpleBuilder", typeElement.getSimpleName());
+ return Optional.empty();
+ }
+
+ String packageName = context.getPackageName(typeElement);
+ String simpleClassName = typeElement.getSimpleName().toString();
+ context.debug(
+ " -> Found @SimpleBuilder on type %s.%s, will use %sBuilder",
+ packageName, simpleClassName, simpleClassName);
+ return Optional.of(new TypeName(packageName, simpleClassName + BUILDER_SUFFIX));
+ }
+
+ /**
+ * Extracts the first type argument from a parameterized type mirror. For example, from
+ * List<Task>, this extracts the Task TypeMirror.
+ *
+ * @param typeMirror the parameterized type mirror
+ * @return the first type argument, or null if not a parameterized type
+ */
+ private static TypeMirror extractFirstTypeArgument(TypeMirror typeMirror) {
+ if (typeMirror instanceof javax.lang.model.type.DeclaredType declaredType) {
+ List extends TypeMirror> typeArguments = declaredType.getTypeArguments();
+ if (!typeArguments.isEmpty()) {
+ return typeArguments.get(0);
+ }
}
- return Optional.empty();
+ return null;
}
}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaLangAnalyser.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaLangAnalyser.java
index 68cbf071..bc33c47a 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaLangAnalyser.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaLangAnalyser.java
@@ -38,6 +38,7 @@
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
+import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Strings;
import org.javahelpers.simple.builders.core.annotations.SimpleBuilderConstructor;
@@ -130,6 +131,33 @@ public static boolean isNotStatic(ExecutableElement mth) {
return !mth.getModifiers().contains(STATIC);
}
+ /**
+ * Checks whether the given {@link TypeElement} declares generic type parameters.
+ *
+ * @param typeElement the type element to inspect
+ * @return {@code true} if the type declares one or more type parameters; {@code false} otherwise
+ */
+ public static boolean hasGenericTypes(TypeElement typeElement) {
+ if (typeElement == null) {
+ return false;
+ }
+ return CollectionUtils.isNotEmpty(typeElement.getTypeParameters());
+ }
+
+ /**
+ * Checks whether the given {@link ExecutableElement} declares generic type parameters.
+ *
+ * @param executableElement the executable element to inspect
+ * @return {@code true} if the executable declares one or more type parameters; {@code false}
+ * otherwise
+ */
+ public static boolean hasGenericTypes(ExecutableElement executableElement) {
+ if (executableElement == null) {
+ return false;
+ }
+ return CollectionUtils.isNotEmpty(executableElement.getTypeParameters());
+ }
+
/**
* Helper to check if the method is a setter for a field.
*
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 c86be7db..85a8e6d3 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
@@ -2696,4 +2696,160 @@ public class RawCollections {
"rawSet(String...)",
"rawMap(Map.Entry)");
}
+
+ @Test
+ void shouldGenerateArrayListBuilderWithElementBuildersForAnnotatedElements() {
+ // Given: List of custom type where element has @SimpleBuilder
+ String packageName = "test";
+ String className = "HasListWithBuilders";
+ String builderClassName = className + "Builder";
+
+ JavaFileObject dto =
+ ProcessorTestUtils.simpleBuilderClass(
+ packageName,
+ className,
+ """
+ private java.util.List tasks;
+
+ public java.util.List getTasks() { return tasks; }
+ public void setTasks(java.util.List tasks) { this.tasks = tasks; }
+ """);
+
+ JavaFileObject task =
+ ProcessorTestUtils.forSource(
+ """
+ package test;
+ import org.javahelpers.simple.builders.core.annotations.SimpleBuilder;
+ @SimpleBuilder
+ public class Task {
+ private String title;
+ public String getTitle() { return title; }
+ public void setTitle(String title) { this.title = title; }
+ }
+ """);
+
+ // When
+ Compilation compilation = compile(dto, task);
+
+ // Then
+ String generatedCode = loadGeneratedSource(compilation, builderClassName);
+ assertGenerationSucceeded(compilation, builderClassName, generatedCode);
+
+ // Verify WithElementBuilders consumer is generated instead of regular ArrayListBuilder
+ ProcessorAsserts.assertingResult(
+ generatedCode,
+ contains("public HasListWithBuildersBuilder tasks(List tasks)"),
+ contains("public HasListWithBuildersBuilder tasks(Supplier> tasksSupplier)"),
+ contains("public HasListWithBuildersBuilder tasks(Task... tasks)"),
+ // Key assertion: uses ArrayListBuilderWithElementBuilders with TaskBuilder
+ contains(
+ "public HasListWithBuildersBuilder tasks(Consumer> tasksBuilderConsumer)"),
+ contains(
+ "new ArrayListBuilderWithElementBuilders(this.tasks.value(), TaskBuilder::create)"),
+ contains("new ArrayListBuilderWithElementBuilders(TaskBuilder::create)"),
+ // Should NOT use regular ArrayListBuilder
+ notContains("Consumer>"));
+ }
+
+ @Test
+ void shouldGenerateHashSetBuilderWithElementBuildersForAnnotatedElements() {
+ // Given: Set of custom type where element has @SimpleBuilder
+ String packageName = "test";
+ String className = "HasSetWithBuilders";
+ String builderClassName = className + "Builder";
+
+ JavaFileObject dto =
+ ProcessorTestUtils.simpleBuilderClass(
+ packageName,
+ className,
+ """
+ private java.util.Set tags;
+
+ public java.util.Set getTags() { return tags; }
+ public void setTags(java.util.Set tags) { this.tags = tags; }
+ """);
+
+ JavaFileObject tag =
+ ProcessorTestUtils.forSource(
+ """
+ package test;
+ import org.javahelpers.simple.builders.core.annotations.SimpleBuilder;
+ @SimpleBuilder
+ public class Tag {
+ private String name;
+ public String getName() { return name; }
+ public void setName(String name) { this.name = name; }
+ }
+ """);
+
+ // When
+ Compilation compilation = compile(dto, tag);
+
+ // Then
+ String generatedCode = loadGeneratedSource(compilation, builderClassName);
+ assertGenerationSucceeded(compilation, builderClassName, generatedCode);
+
+ // Verify WithElementBuilders consumer is generated for Set
+ ProcessorAsserts.assertingResult(
+ generatedCode,
+ contains("public HasSetWithBuildersBuilder tags(Set tags)"),
+ contains("public HasSetWithBuildersBuilder tags(Supplier> tagsSupplier)"),
+ contains("public HasSetWithBuildersBuilder tags(Tag... tags)"),
+ // Key assertion: uses HashSetBuilderWithElementBuilders with TagBuilder
+ contains(
+ "public HasSetWithBuildersBuilder tags(Consumer> tagsBuilderConsumer)"),
+ contains(
+ "new HashSetBuilderWithElementBuilders(this.tags.value(), TagBuilder::create)"),
+ contains("new HashSetBuilderWithElementBuilders(TagBuilder::create)"),
+ // Should NOT use regular HashSetBuilder
+ notContains("Consumer>"));
+ }
+
+ @Test
+ void shouldUseRegularBuilderWhenElementTypeHasNoSimpleBuilder() {
+ // Given: List of custom type WITHOUT @SimpleBuilder annotation
+ String packageName = "test";
+ String className = "HasListWithoutBuilders";
+ String builderClassName = className + "Builder";
+
+ JavaFileObject dto =
+ ProcessorTestUtils.simpleBuilderClass(
+ packageName,
+ className,
+ """
+ private java.util.List items;
+
+ public java.util.List getItems() { return items; }
+ public void setItems(java.util.List items) { this.items = items; }
+ """);
+
+ JavaFileObject plain =
+ ProcessorTestUtils.forSource(
+ """
+ package test;
+ public class PlainClass {
+ private String value;
+ public PlainClass() {}
+ public String getValue() { return value; }
+ public void setValue(String value) { this.value = value; }
+ }
+ """);
+
+ // When
+ Compilation compilation = compile(dto, plain);
+
+ // Then
+ String generatedCode = loadGeneratedSource(compilation, builderClassName);
+ assertGenerationSucceeded(compilation, builderClassName, generatedCode);
+
+ // Verify regular ArrayListBuilder is used (not WithElementBuilders)
+ ProcessorAsserts.assertingResult(
+ generatedCode,
+ contains("public HasListWithoutBuildersBuilder items(List items)"),
+ contains(
+ "public HasListWithoutBuildersBuilder items(Consumer> itemsBuilderConsumer)"),
+ // Should NOT use WithElementBuilders version
+ notContains("ArrayListBuilderWithElementBuilders"),
+ notContains("PlainClassBuilder"));
+ }
}
diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/ReadmeExampleTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/ReadmeExampleTest.java
index 3ce9eafd..dcc6002b 100644
--- a/processor/src/test/java/org/javahelpers/simple/builders/processor/ReadmeExampleTest.java
+++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/ReadmeExampleTest.java
@@ -146,18 +146,19 @@ public enum ProjectStatus { PLANNING, IN_PROGRESS, COMPLETED, ON_HOLD }
package readme;
public class Usage {
public static void main(){
+ String version = "1.0.0";
Project project = ProjectBuilder.create()
- .name("Simple Builders with a bit of complexity")
+ .name("Simple Builders in version %s with a bit of complexity", version)
.status(ProjectStatus.IN_PROGRESS)
.tasks(tasks -> tasks
- .add(TaskBuilder.create()
+ .add(taskBuilder -> taskBuilder
.title("Implement core functionality")
.completed(true)
- .build())
- .add(TaskBuilder.create()
+ )
+ .add(taskBuilder -> taskBuilder
.title("Add documentation")
.description("Update README and add Javadocs")
- .build())
+ )
)
.metadata(metadata -> metadata
.put("version", "1.0.0")
@@ -178,7 +179,7 @@ public static void main(){
contains("public static ProjectBuilder create()"),
contains("public ProjectBuilder name(String name)"),
contains(
- "public ProjectBuilder tasks(Consumer> tasksBuilderConsumer)"),
+ "public ProjectBuilder tasks(Consumer> tasksBuilderConsumer)"),
contains(
"public ProjectBuilder metadata(Consumer> metadataBuilderConsumer"),
contains("public ProjectBuilder status(ProjectStatus status)"),
diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/testing/ProcessorAsserts.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/testing/ProcessorAsserts.java
index ec219347..4fddc926 100644
--- a/processor/src/test/java/org/javahelpers/simple/builders/processor/testing/ProcessorAsserts.java
+++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/testing/ProcessorAsserts.java
@@ -19,7 +19,7 @@ private ProcessorAsserts() {}
/** Collapse all whitespace (spaces, tabs, newlines) to single spaces and trim. */
private static String normalizeWhitespace(String input) {
if (input == null) return null;
- return input.replaceAll("\\s+", " ").trim();
+ return input.replaceAll("[\\t\\n\\r\\s]+", "").trim();
}
/**