diff --git a/core/pom.xml b/core/pom.xml
index eb125219..7a5c8e7e 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -66,6 +66,7 @@
6.0.1
+ 3.20.0
0.9.0
@@ -80,6 +81,13 @@
+
+
+ org.apache.commons
+ commons-lang3
+ ${apache-commons-lang3.version}
+
+
org.junit.jupiter
diff --git a/core/src/main/java/org/javahelpers/simple/builders/core/util/BuilderToStringStyle.java b/core/src/main/java/org/javahelpers/simple/builders/core/util/BuilderToStringStyle.java
new file mode 100644
index 00000000..a9f421af
--- /dev/null
+++ b/core/src/main/java/org/javahelpers/simple/builders/core/util/BuilderToStringStyle.java
@@ -0,0 +1,56 @@
+/*
+ * 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.util;
+
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+/**
+ * Custom ToStringStyle for builders that automatically handles TrackedValue fields. This style only
+ * includes fields that have been set (TrackedValue.isSet() returns true) and unwraps their values
+ * automatically.
+ */
+public class BuilderToStringStyle extends ToStringStyle {
+
+ private static final long serialVersionUID = 1L;
+
+ public static final BuilderToStringStyle INSTANCE = new BuilderToStringStyle();
+
+ public BuilderToStringStyle() {
+ super();
+ this.setUseClassName(true);
+ this.setUseIdentityHashCode(false);
+ }
+
+ @Override
+ public void append(StringBuffer buffer, String fieldName, Object value, Boolean fullDetail) {
+ if (value instanceof TrackedValue> trackedValue) {
+ if (trackedValue.isSet()) {
+ super.append(buffer, fieldName, trackedValue.value(), fullDetail);
+ }
+ } else {
+ super.append(buffer, fieldName, value, fullDetail);
+ }
+ }
+}
diff --git a/core/src/test/java/org/javahelpers/simple/builders/core/util/BuilderToStringStyleTest.java b/core/src/test/java/org/javahelpers/simple/builders/core/util/BuilderToStringStyleTest.java
new file mode 100644
index 00000000..95bd72ce
--- /dev/null
+++ b/core/src/test/java/org/javahelpers/simple/builders/core/util/BuilderToStringStyleTest.java
@@ -0,0 +1,232 @@
+/*
+ * 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.util;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for {@link BuilderToStringStyle}.
+ *
+ * Verifies that:
+ *
+ *
+ * - TrackedValue fields that are set are included in toString output
+ *
- TrackedValue fields that are unset are excluded from toString output
+ *
- Regular (non-TrackedValue) fields are always included
+ *
- The style correctly unwraps TrackedValue.value()
+ *
+ */
+class BuilderToStringStyleTest {
+
+ @Test
+ void instance_isNotNull() {
+ assertNotNull(BuilderToStringStyle.INSTANCE, "INSTANCE should not be null");
+ }
+
+ @Test
+ void toString_includesSetTrackedValue() {
+ TestObject obj = new TestObject();
+ obj.setField = TrackedValue.changedValue("SetValue");
+
+ String result =
+ new ToStringBuilder(obj, BuilderToStringStyle.INSTANCE)
+ .append("setField", obj.setField)
+ .toString();
+
+ assertTrue(
+ result.contains("setField=SetValue"), "Should include set TrackedValue field: " + result);
+ }
+
+ @Test
+ void toString_excludesUnsetTrackedValue() {
+ TestObject obj = new TestObject();
+ obj.unsetField = TrackedValue.unsetValue();
+
+ String result =
+ new ToStringBuilder(obj, BuilderToStringStyle.INSTANCE)
+ .append("unsetField", obj.unsetField)
+ .toString();
+
+ assertFalse(
+ result.contains("unsetField"), "Should NOT include unset TrackedValue field: " + result);
+ }
+
+ @Test
+ void toString_includesInitialTrackedValue() {
+ TestObject obj = new TestObject();
+ obj.initialField = TrackedValue.initialValue("InitialValue");
+
+ String result =
+ new ToStringBuilder(obj, BuilderToStringStyle.INSTANCE)
+ .append("initialField", obj.initialField)
+ .toString();
+
+ assertTrue(
+ result.contains("initialField=InitialValue"),
+ "Should include initial TrackedValue field: " + result);
+ }
+
+ @Test
+ void toString_includesRegularField() {
+ TestObject obj = new TestObject();
+ obj.regularField = "RegularValue";
+
+ String result =
+ new ToStringBuilder(obj, BuilderToStringStyle.INSTANCE)
+ .append("regularField", obj.regularField)
+ .toString();
+
+ assertTrue(
+ result.contains("regularField=RegularValue"), "Should include regular field: " + result);
+ }
+
+ @Test
+ void toString_mixedFieldsShowCorrectly() {
+ TestObject obj = new TestObject();
+ obj.setField = TrackedValue.changedValue("SetValue");
+ obj.unsetField = TrackedValue.unsetValue();
+ obj.initialField = TrackedValue.initialValue("InitialValue");
+ obj.regularField = "RegularValue";
+
+ String result =
+ new ToStringBuilder(obj, BuilderToStringStyle.INSTANCE)
+ .append("setField", obj.setField)
+ .append("unsetField", obj.unsetField)
+ .append("initialField", obj.initialField)
+ .append("regularField", obj.regularField)
+ .toString();
+
+ assertTrue(result.contains("setField=SetValue"), "Should include set field: " + result);
+ assertFalse(result.contains("unsetField"), "Should NOT include unset field: " + result);
+ assertTrue(
+ result.contains("initialField=InitialValue"), "Should include initial field: " + result);
+ assertTrue(
+ result.contains("regularField=RegularValue"), "Should include regular field: " + result);
+ }
+
+ @Test
+ void toString_nullTrackedValueIsShown() {
+ TestObject obj = new TestObject();
+ obj.setField = TrackedValue.changedValue(null);
+
+ String result =
+ new ToStringBuilder(obj, BuilderToStringStyle.INSTANCE)
+ .append("setField", obj.setField)
+ .toString();
+
+ assertTrue(
+ result.contains("setField=") || result.contains("setField=null"),
+ "Should show null value for set TrackedValue: " + result);
+ }
+
+ @Test
+ void toString_nullRegularFieldIsShown() {
+ TestObject obj = new TestObject();
+ obj.regularField = null;
+
+ String result =
+ new ToStringBuilder(obj, BuilderToStringStyle.INSTANCE)
+ .append("regularField", obj.regularField)
+ .toString();
+
+ assertTrue(
+ result.contains("regularField=") || result.contains("regularField=null"),
+ "Should show null value for regular field: " + result);
+ }
+
+ @Test
+ void toString_containsClassName() {
+ TestObject obj = new TestObject();
+
+ String result = new ToStringBuilder(obj, BuilderToStringStyle.INSTANCE).toString();
+
+ assertTrue(result.contains("TestObject"), "Should contain class name: " + result);
+ }
+
+ @Test
+ void toString_doesNotContainIdentityHashCode() {
+ TestObject obj = new TestObject();
+
+ String result = new ToStringBuilder(obj, BuilderToStringStyle.INSTANCE).toString();
+
+ assertFalse(result.contains("@"), "Should NOT contain identity hash code (@): " + result);
+ }
+
+ @Test
+ void toString_emptyBuilderShowsOnlyClassName() {
+ TestObject obj = new TestObject();
+
+ String result = new ToStringBuilder(obj, BuilderToStringStyle.INSTANCE).toString();
+
+ assertTrue(result.contains("TestObject"), "Should contain class name: " + result);
+ assertTrue(result.contains("["), "Should contain opening bracket: " + result);
+ assertTrue(result.contains("]"), "Should contain closing bracket: " + result);
+ }
+
+ @Test
+ void toString_multipleSetTrackedValues() {
+ TestObject obj = new TestObject();
+ obj.setField = TrackedValue.changedValue("Value1");
+ obj.initialField = TrackedValue.initialValue("Value2");
+
+ String result =
+ new ToStringBuilder(obj, BuilderToStringStyle.INSTANCE)
+ .append("setField", obj.setField)
+ .append("initialField", obj.initialField)
+ .toString();
+
+ assertTrue(result.contains("setField=Value1"), "Should include first set field: " + result);
+ assertTrue(
+ result.contains("initialField=Value2"), "Should include second set field: " + result);
+ }
+
+ @Test
+ void toString_multipleUnsetTrackedValues() {
+ TestObject obj = new TestObject();
+ obj.setField = TrackedValue.unsetValue();
+ obj.unsetField = TrackedValue.unsetValue();
+
+ String result =
+ new ToStringBuilder(obj, BuilderToStringStyle.INSTANCE)
+ .append("setField", obj.setField)
+ .append("unsetField", obj.unsetField)
+ .toString();
+
+ assertFalse(result.contains("setField"), "Should NOT include first unset field: " + result);
+ assertFalse(result.contains("unsetField"), "Should NOT include second unset field: " + result);
+ }
+
+ static class TestObject {
+ TrackedValue setField;
+ TrackedValue unsetField;
+ TrackedValue initialField;
+ String regularField;
+ }
+}
diff --git a/example/src/test/java/org/javahelpers/simple/builders/example/ToStringBehaviorTest.java b/example/src/test/java/org/javahelpers/simple/builders/example/ToStringBehaviorTest.java
new file mode 100644
index 00000000..05ea1860
--- /dev/null
+++ b/example/src/test/java/org/javahelpers/simple/builders/example/ToStringBehaviorTest.java
@@ -0,0 +1,147 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2025 Andreas Igel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.javahelpers.simple.builders.example;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Runtime tests for toString behavior in generated builders.
+ *
+ * Verifies that:
+ *
+ *
+ * - Only set fields appear in toString output
+ *
- Unset fields are hidden from toString output
+ *
- BuilderToStringStyle correctly filters TrackedValue fields
+ *
+ */
+class ToStringBehaviorTest {
+
+ @Test
+ void toString_onlyShowsSetFields() {
+ BookDtoBuilder builder = BookDtoBuilder.create().title("The Great Book").author("John Doe");
+
+ String result = builder.toString();
+
+ assertTrue(result.contains("title=The Great Book"), "Should show set title field: " + result);
+ assertTrue(result.contains("author=John Doe"), "Should show set author field: " + result);
+ assertFalse(result.contains("isbn"), "Should NOT show unset isbn field: " + result);
+ assertFalse(result.contains("pages"), "Should NOT show unset pages field: " + result);
+ assertFalse(result.contains("price"), "Should NOT show unset price field: " + result);
+ }
+
+ @Test
+ void toString_emptyBuilderShowsNoFields() {
+ BookDtoBuilder builder = BookDtoBuilder.create();
+
+ String result = builder.toString();
+
+ assertTrue(result.contains("BookDtoBuilder"), "Should contain builder class name: " + result);
+ assertFalse(result.contains("title="), "Should NOT show unset title field: " + result);
+ assertFalse(result.contains("author="), "Should NOT show unset author field: " + result);
+ assertFalse(result.contains("isbn="), "Should NOT show unset isbn field: " + result);
+ }
+
+ @Test
+ void toString_partiallySetFieldsOnlyShowsSet() {
+ BookDtoBuilder builder =
+ BookDtoBuilder.create().title("Partial Book").pages(250);
+
+ String result = builder.toString();
+
+ assertTrue(result.contains("title=Partial Book"), "Should show set title field: " + result);
+ assertTrue(result.contains("pages=250"), "Should show set pages field: " + result);
+ assertFalse(result.contains("author="), "Should NOT show unset author field: " + result);
+ assertFalse(result.contains("isbn"), "Should NOT show unset isbn field: " + result);
+ }
+
+ @Test
+ void toString_allFieldsSetShowsAll() {
+ BookDtoBuilder builder =
+ BookDtoBuilder.create()
+ .title("Complete Book")
+ .author("Jane Smith")
+ .isbn("978-1234567890")
+ .pages(350);
+
+ String result = builder.toString();
+
+ assertTrue(result.contains("title=Complete Book"), "Should show title: " + result);
+ assertTrue(result.contains("author=Jane Smith"), "Should show author: " + result);
+ assertTrue(result.contains("isbn=978-1234567890"), "Should show isbn: " + result);
+ assertTrue(result.contains("pages=350"), "Should show pages: " + result);
+ }
+
+ @Test
+ void toString_builderFromInstanceShowsAllFields() {
+ BookDto book = new BookDto();
+ book.setTitle("Instance Book");
+ book.setAuthor("Bob Johnson");
+ book.setIsbn("978-9876543210");
+ book.setPages(400);
+
+ BookDtoBuilder builder = new BookDtoBuilder(book);
+
+ String result = builder.toString();
+
+ assertTrue(result.contains("title=Instance Book"), "Should show title from instance: " + result);
+ assertTrue(result.contains("author=Bob Johnson"), "Should show author from instance: " + result);
+ assertTrue(result.contains("isbn=978-9876543210"), "Should show isbn from instance: " + result);
+ assertTrue(result.contains("pages=400"), "Should show pages from instance: " + result);
+ }
+
+ @Test
+ void toString_modifiedBuilderShowsUpdatedValues() {
+ BookDtoBuilder builder =
+ BookDtoBuilder.create()
+ .title("Original Title")
+ .author("Original Author")
+ .pages(200);
+
+ builder.title("Updated Title");
+
+ String result = builder.toString();
+
+ assertTrue(result.contains("title=Updated Title"), "Should show updated title: " + result);
+ assertFalse(result.contains("Original Title"), "Should NOT show old title: " + result);
+ assertTrue(result.contains("author=Original Author"), "Should still show author: " + result);
+ assertTrue(result.contains("pages=200"), "Should still show pages: " + result);
+ }
+
+ @Test
+ void toString_nullValueIsShown() {
+ BookDtoBuilder builder = BookDtoBuilder.create().title("Test Book").author(null);
+
+ String result = builder.toString();
+
+ assertTrue(result.contains("title=Test Book"), "Should show title: " + result);
+ assertTrue(
+ result.contains("author=") || result.contains("author=null"),
+ "Should show null value for set field: " + result);
+ }
+}
diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java
index a0b0cd9e..f9aee825 100644
--- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java
+++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java
@@ -11,8 +11,10 @@
import java.util.Map;
import java.util.Optional;
import java.util.Set;
+import org.apache.commons.lang3.builder.ToStringBuilder;
import org.javahelpers.simple.builders.core.annotations.BuilderImplementation;
import org.javahelpers.simple.builders.core.interfaces.IBuilderBase;
+import org.javahelpers.simple.builders.core.util.BuilderToStringStyle;
import org.javahelpers.simple.builders.core.util.TrackedValue;
/**
@@ -440,4 +442,34 @@ public BookDto build() {
public static BookDtoBuilder create() {
return new BookDtoBuilder();
}
+
+ /**
+ * Returns a string representation of this builder, including only fields that have been set.
+ *
+ * @return string representation of the builder
+ */
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE)
+ .append("title", this.title)
+ .append("author", this.author)
+ .append("isbn", this.isbn)
+ .append("pages", this.pages)
+ .append("price", this.price)
+ .append("exactPrice", this.exactPrice)
+ .append("available", this.available)
+ .append("rating", this.rating)
+ .append("edition", this.edition)
+ .append("salesCount", this.salesCount)
+ .append("discount", this.discount)
+ .append("category", this.category)
+ .append("publishDate", this.publishDate)
+ .append("lastUpdated", this.lastUpdated)
+ .append("subtitle", this.subtitle)
+ .append("tags", this.tags)
+ .append("genres", this.genres)
+ .append("metadata", this.metadata)
+ .append("publisher", this.publisher)
+ .toString();
+ }
}
diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/MannschaftDtoBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/MannschaftDtoBuilder.java
index 96783a5d..5f344cce 100644
--- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/MannschaftDtoBuilder.java
+++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/MannschaftDtoBuilder.java
@@ -9,9 +9,11 @@
import java.util.function.Consumer;
import java.util.function.Supplier;
import javax.annotation.processing.Generated;
+import org.apache.commons.lang3.builder.ToStringBuilder;
import org.javahelpers.simple.builders.core.annotations.BuilderImplementation;
import org.javahelpers.simple.builders.core.builders.HashSetBuilderWithElementBuilders;
import org.javahelpers.simple.builders.core.interfaces.IBuilderBase;
+import org.javahelpers.simple.builders.core.util.BuilderToStringStyle;
import org.javahelpers.simple.builders.core.util.TrackedValue;
/**
@@ -189,6 +191,19 @@ public MannschaftDtoBuilder conditional(BooleanSupplier condition,
return conditional(condition, yesCondition, null);
}
+ /**
+ * Returns a string representation of this builder, including only fields that have been set.
+ *
+ * @return string representation of the builder
+ */
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE)
+ .append("name", this.name)
+ .append("sponsoren", this.sponsoren)
+ .toString();
+ }
+
/**
* Interface that can be implemented by the DTO to provide fluent modification methods.
*/
diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/PersonDtoBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/PersonDtoBuilder.java
index 7aa00344..b1d8f8d2 100644
--- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/PersonDtoBuilder.java
+++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/PersonDtoBuilder.java
@@ -10,9 +10,11 @@
import java.util.function.Consumer;
import java.util.function.Supplier;
import javax.annotation.processing.Generated;
+import org.apache.commons.lang3.builder.ToStringBuilder;
import org.javahelpers.simple.builders.core.annotations.BuilderImplementation;
import org.javahelpers.simple.builders.core.builders.ArrayListBuilder;
import org.javahelpers.simple.builders.core.interfaces.IBuilderBase;
+import org.javahelpers.simple.builders.core.util.BuilderToStringStyle;
import org.javahelpers.simple.builders.core.util.TrackedValue;
/**
@@ -311,6 +313,22 @@ public PersonDtoBuilder conditional(BooleanSupplier condition,
return conditional(condition, yesCondition, null);
}
+ /**
+ * Returns a string representation of this builder, including only fields that have been set.
+ *
+ * @return string representation of the builder
+ */
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE)
+ .append("name", this.name)
+ .append("nickNames", this.nickNames)
+ .append("nickNames2", this.nickNames2)
+ .append("birthdate", this.birthdate)
+ .append("mannschaft", this.mannschaft)
+ .toString();
+ }
+
/**
* Interface that can be implemented by the DTO to provide fluent modification methods.
*/
diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/ProductRecordBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/ProductRecordBuilder.java
index 3a6ab3c8..eb6fdee0 100644
--- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/ProductRecordBuilder.java
+++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/ProductRecordBuilder.java
@@ -8,8 +8,10 @@
import java.util.function.Consumer;
import java.util.function.Supplier;
import javax.annotation.processing.Generated;
+import org.apache.commons.lang3.builder.ToStringBuilder;
import org.javahelpers.simple.builders.core.annotations.BuilderImplementation;
import org.javahelpers.simple.builders.core.interfaces.IBuilderBase;
+import org.javahelpers.simple.builders.core.util.BuilderToStringStyle;
import org.javahelpers.simple.builders.core.util.TrackedValue;
/**
@@ -222,6 +224,20 @@ public ProductRecordBuilder conditional(BooleanSupplier condition,
return conditional(condition, yesCondition, null);
}
+ /**
+ * Returns a string representation of this builder, including only fields that have been set.
+ *
+ * @return string representation of the builder
+ */
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE)
+ .append("name", this.name)
+ .append("price", this.price)
+ .append("category", this.category)
+ .toString();
+ }
+
/**
* Interface that can be implemented by the DTO to provide fluent modification methods.
*/
diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SponsorDtoBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SponsorDtoBuilder.java
index adf74258..5bbfaed8 100644
--- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SponsorDtoBuilder.java
+++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SponsorDtoBuilder.java
@@ -8,8 +8,10 @@
import java.util.function.Consumer;
import java.util.function.Supplier;
import javax.annotation.processing.Generated;
+import org.apache.commons.lang3.builder.ToStringBuilder;
import org.javahelpers.simple.builders.core.annotations.BuilderImplementation;
import org.javahelpers.simple.builders.core.interfaces.IBuilderBase;
+import org.javahelpers.simple.builders.core.util.BuilderToStringStyle;
import org.javahelpers.simple.builders.core.util.TrackedValue;
/**
@@ -133,6 +135,18 @@ public SponsorDtoBuilder conditional(BooleanSupplier condition,
return conditional(condition, yesCondition, null);
}
+ /**
+ * Returns a string representation of this builder, including only fields that have been set.
+ *
+ * @return string representation of the builder
+ */
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE)
+ .append("name", this.name)
+ .toString();
+ }
+
/**
* Interface that can be implemented by the DTO to provide fluent modification methods.
*/
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java
index 4e60003a..e4c2b58a 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java
@@ -191,6 +191,11 @@ public void generateBuilder(BuilderDefinitionDto builderDef) throws BuilderExcep
createMethodConditionalPositiveOnly(builderTypeName, methodAccessModifier));
}
+ // Add toString method
+ classBuilder.addMethod(
+ createMethodToString(
+ builderDef.getConstructorFieldsForBuilder(), builderDef.getSetterFieldsForBuilder()));
+
// Adding nested types (e.g., With interface)
for (NestedTypeDto nestedType : builderDef.getNestedTypes()) {
TypeSpec nestedTypeSpec = createNestedType(nestedType);
@@ -538,6 +543,42 @@ private MethodSpec createMethodConditional(
return mb.build();
}
+ private MethodSpec createMethodToString(
+ List constructorFields, List setterFields) {
+ MethodSpec.Builder mb =
+ MethodSpec.methodBuilder("toString")
+ .addModifiers(PUBLIC)
+ .addAnnotation(Override.class)
+ .returns(String.class)
+ .addJavadoc(
+ """
+ Returns a string representation of this builder, including only fields that have been set.
+
+ @return string representation of the builder
+ """);
+
+ // Combine all fields
+ List allFields = new java.util.ArrayList<>();
+ allFields.addAll(constructorFields);
+ allFields.addAll(setterFields);
+
+ // Build fluent chain of append calls using CodeBlock.Builder
+ CodeBlock.Builder codeBuilder = CodeBlock.builder();
+ codeBuilder.add(
+ "return new $T(this, $T.INSTANCE)",
+ ClassName.get("org.apache.commons.lang3.builder", "ToStringBuilder"),
+ ClassName.get("org.javahelpers.simple.builders.core.util", "BuilderToStringStyle"));
+
+ for (FieldDto field : allFields) {
+ codeBuilder.add("\n .append($S, this.$N)", field.getFieldName(), field.getFieldName());
+ }
+
+ codeBuilder.add("\n .toString()");
+ mb.addStatement(codeBuilder.build());
+
+ return mb.build();
+ }
+
private MethodSpec createMethodConditionalPositiveOnly(
com.palantir.javapoet.TypeName builderType, Modifier methodAccessModifier) {
MethodSpec.Builder mb = MethodSpec.methodBuilder("conditional");
diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderConfigurationReaderTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderConfigurationReaderTest.java
index 53120c6a..ff164509 100644
--- a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderConfigurationReaderTest.java
+++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderConfigurationReaderTest.java
@@ -461,7 +461,9 @@ public class PersonDto {
import static org.javahelpers.simple.builders.core.util.TrackedValue.unsetValue;
import java.util.List;
+ import org.apache.commons.lang3.builder.ToStringBuilder;
import org.javahelpers.simple.builders.core.interfaces.IBuilderBase;
+ import org.javahelpers.simple.builders.core.util.BuilderToStringStyle;
import org.javahelpers.simple.builders.core.util.TrackedValue;
/**
@@ -532,6 +534,19 @@ public PersonDto build() {
public static PersonDtoMinimalBuilder create() {
return new PersonDtoMinimalBuilder();
}
+
+ /**
+ * Returns a string representation of this builder, including only fields that have been set.
+ *
+ * @return string representation of the builder
+ */
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE)
+ .append("name", this.name)
+ .append("tags", this.tags)
+ .toString();
+ }
}
""";
diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/ToStringGenerationTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/ToStringGenerationTest.java
new file mode 100644
index 00000000..457448e3
--- /dev/null
+++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/ToStringGenerationTest.java
@@ -0,0 +1,240 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2025 Andreas Igel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.javahelpers.simple.builders.processor;
+
+import static com.google.testing.compile.CompilationSubject.assertThat;
+import static org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils.loadGeneratedSource;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.testing.compile.Compilation;
+import javax.tools.JavaFileObject;
+import org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for toString method generation in builders.
+ *
+ * Verifies that:
+ *
+ *
+ * - toString method is generated with fluent ToStringBuilder API
+ *
- Custom BuilderToStringStyle is used
+ *
- All fields are included in the toString output
+ *
- The generated code compiles successfully
+ *
+ */
+class ToStringGenerationTest {
+
+ @Test
+ void toString_generatedWithFluentAPI() {
+ JavaFileObject dtoSource =
+ ProcessorTestUtils.forSource(
+ """
+ package test;
+ import org.javahelpers.simple.builders.core.annotations.SimpleBuilder;
+
+ @SimpleBuilder
+ public class PersonDto {
+ private String name;
+ private int age;
+ private String email;
+
+ public String getName() { return name; }
+ public void setName(String name) { this.name = name; }
+ public int getAge() { return age; }
+ public void setAge(int age) { this.age = age; }
+ public String getEmail() { return email; }
+ public void setEmail(String email) { this.email = email; }
+ }
+ """);
+
+ Compilation compilation = ProcessorTestUtils.createCompiler().compile(dtoSource);
+
+ assertThat(compilation).succeeded();
+
+ String generatedCode = loadGeneratedSource(compilation, "PersonDtoBuilder");
+
+ // Verify toString method exists
+ assertTrue(
+ generatedCode.contains("public String toString()"), "toString method should be generated");
+
+ // Verify it uses BuilderToStringStyle
+ assertTrue(
+ generatedCode.contains("BuilderToStringStyle.INSTANCE"),
+ "Should use BuilderToStringStyle.INSTANCE");
+
+ // Verify it uses ToStringBuilder
+ assertTrue(
+ generatedCode.contains("new ToStringBuilder(this, BuilderToStringStyle.INSTANCE)"),
+ "Should create ToStringBuilder with custom style");
+
+ // Verify fluent API with all fields
+ assertTrue(generatedCode.contains(".append(\"name\", this.name)"), "Should append name field");
+ assertTrue(generatedCode.contains(".append(\"age\", this.age)"), "Should append age field");
+ assertTrue(
+ generatedCode.contains(".append(\"email\", this.email)"), "Should append email field");
+
+ // Verify method ends with .toString()
+ assertTrue(generatedCode.contains(".toString();"), "Should end with .toString() call");
+
+ // Verify imports
+ assertTrue(
+ generatedCode.contains("import org.apache.commons.lang3.builder.ToStringBuilder;"),
+ "Should import ToStringBuilder");
+ assertTrue(
+ generatedCode.contains(
+ "import org.javahelpers.simple.builders.core.util.BuilderToStringStyle;"),
+ "Should import BuilderToStringStyle");
+ }
+
+ @Test
+ void toString_includesConstructorAndSetterFields() {
+ JavaFileObject dtoSource =
+ ProcessorTestUtils.forSource(
+ """
+ package test;
+ import org.javahelpers.simple.builders.core.annotations.SimpleBuilder;
+
+ @SimpleBuilder
+ public class BookDto {
+ private String title;
+ private String author;
+ private int pages;
+
+ public BookDto(String title, String author) {
+ this.title = title;
+ this.author = author;
+ }
+
+ public String getTitle() { return title; }
+ public String getAuthor() { return author; }
+ public int getPages() { return pages; }
+ public void setPages(int pages) { this.pages = pages; }
+ }
+ """);
+
+ Compilation compilation = ProcessorTestUtils.createCompiler().compile(dtoSource);
+
+ assertThat(compilation).succeeded();
+
+ String generatedCode = loadGeneratedSource(compilation, "BookDtoBuilder");
+
+ // Verify constructor fields are included
+ assertTrue(
+ generatedCode.contains(".append(\"title\", this.title)"),
+ "Should append constructor field: title");
+ assertTrue(
+ generatedCode.contains(".append(\"author\", this.author)"),
+ "Should append constructor field: author");
+
+ // Verify setter fields are included
+ assertTrue(
+ generatedCode.contains(".append(\"pages\", this.pages)"),
+ "Should append setter field: pages");
+ }
+
+ @Test
+ void toString_generatedCodeStructure() {
+ JavaFileObject dtoSource =
+ ProcessorTestUtils.forSource(
+ """
+ package test;
+ import org.javahelpers.simple.builders.core.annotations.SimpleBuilder;
+
+ @SimpleBuilder
+ public class ProductDto {
+ private String name;
+ private double price;
+
+ public String getName() { return name; }
+ public void setName(String name) { this.name = name; }
+ public double getPrice() { return price; }
+ public void setPrice(double price) { this.price = price; }
+ }
+ """);
+
+ Compilation compilation = ProcessorTestUtils.createCompiler().compile(dtoSource);
+
+ assertThat(compilation).succeeded();
+
+ String generatedCode = loadGeneratedSource(compilation, "ProductDtoBuilder");
+
+ // Verify the complete structure matches expected pattern
+ String expectedPattern =
+ """
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE)
+ .append("name", this.name)
+ .append("price", this.price)
+ .toString();
+ }
+ """;
+
+ // Normalize whitespace for comparison
+ String normalizedGenerated = generatedCode.replaceAll("\\s+", " ").trim();
+ String normalizedExpected = expectedPattern.replaceAll("\\s+", " ").trim();
+
+ assertTrue(
+ normalizedGenerated.contains(normalizedExpected),
+ "Generated toString method should match expected structure.\nExpected pattern: "
+ + normalizedExpected
+ + "\nGenerated code: "
+ + normalizedGenerated);
+ }
+
+ @Test
+ void toString_hasCorrectJavadoc() {
+ JavaFileObject dtoSource =
+ ProcessorTestUtils.forSource(
+ """
+ package test;
+ import org.javahelpers.simple.builders.core.annotations.SimpleBuilder;
+
+ @SimpleBuilder
+ public class SimpleDto {
+ private String value;
+ public String getValue() { return value; }
+ public void setValue(String value) { this.value = value; }
+ }
+ """);
+
+ Compilation compilation = ProcessorTestUtils.createCompiler().compile(dtoSource);
+
+ assertThat(compilation).succeeded();
+
+ String generatedCode = loadGeneratedSource(compilation, "SimpleDtoBuilder");
+
+ // Verify javadoc is present
+ assertTrue(
+ generatedCode.contains(
+ "Returns a string representation of this builder, including only fields that have been"
+ + " set."),
+ "Should have descriptive javadoc");
+ assertTrue(
+ generatedCode.contains("@return string representation of the builder"),
+ "Should have @return tag in javadoc");
+ }
+}