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 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(); } /**