From 9807df7b4553fd3240aed0ac6eaf70d58182e0a5 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 19 Oct 2025 19:47:56 +0200 Subject: [PATCH 01/12] Adding support for additional helper methods for java.util.optional --- .../util/BuilderDefinitionCreator.java | 46 ++++++++++++++++++- .../processor/util/TypeNameAnalyser.java | 11 +++++ 2 files changed, 55 insertions(+), 2 deletions(-) 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 d6b78afa..8a169c97 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 @@ -230,7 +230,7 @@ private static boolean isMethodRelevantForBuilder( private static void addAdditionalHelperMethodsForField( FieldDto result, String fieldName, TypeName fieldType) { - // Only process generic types (List, Set, Map, etc.) + // Only process generic types (List, Set, Map, Optional, etc.) if (!(fieldType instanceof TypeNameGeneric fieldTypeGeneric)) { return; } @@ -253,6 +253,10 @@ private static void addAdditionalHelperMethodsForField( false); result.addMethod( createFieldSetterWithTransform(fieldName, "Map.ofEntries(%s)", mapEntryType)); + } else if (isOptional(fieldType) && innerTypesCnt == 1) { + // Add setter that accepts the inner type T and wraps it in Optional.of() + result.addMethod( + createFieldSetterWithTransform(fieldName, "Optional.of(%s)", innerTypes.get(0))); } } @@ -319,7 +323,16 @@ private static void addSupplierMethodsForField( if (isFunctionalInterface(fieldTypeElement)) { return; } - result.addMethod(createFieldSupplier(fieldName, fieldType)); + + // For Optional fields, create a supplier that provides T and wraps it in Optional + if (isOptional(fieldType) + && fieldType instanceof TypeNameGeneric fieldTypeGeneric + && fieldTypeGeneric.getInnerTypeArguments().size() == 1) { + TypeName innerType = fieldTypeGeneric.getInnerTypeArguments().get(0); + result.addMethod(createFieldSupplierWithTransform(fieldName, "Optional.of(%s)", innerType)); + } else { + result.addMethod(createFieldSupplier(fieldName, fieldType)); + } } private static Optional createFieldFromSetter( @@ -523,6 +536,35 @@ private static MethodDto createFieldSupplier(String fieldName, TypeName fieldTyp return methodDto; } + private static MethodDto createFieldSupplierWithTransform( + String fieldName, String transform, TypeName supplierReturnType) { + TypeNameGeneric supplierType = + new TypeNameGeneric(map2TypeName(Supplier.class), supplierReturnType); + MethodParameterDto parameter = new MethodParameterDto(); + parameter.setParameterName(fieldName + SUFFIX_SUPPLIER); + parameter.setParameterTypeName(supplierType); + MethodDto methodDto = new MethodDto(); + methodDto.setMethodName(fieldName); + methodDto.addParameter(parameter); + methodDto.setModifier(Modifier.PUBLIC); + methodDto.setMethodType(MethodTypes.SUPPLIER); + String params; + if (StringUtils.isBlank(transform)) { + params = parameter.getParameterName() + ".get()"; + } else { + params = String.format(transform, parameter.getParameterName() + ".get()"); + } + methodDto.setCode( + """ + this.$fieldName:N = $builderFieldWrapper:T.changedValue($dtoMethodParams:N); + return this; + """); + methodDto.addArgument(ARG_FIELD_NAME, fieldName); + methodDto.addArgument(ARG_DTO_METHOD_PARAMS, params); + methodDto.addArgument(ARG_BUILDER_FIELD_WRAPPER, TypeName.of(TrackedValue.class)); + return methodDto; + } + private static Optional findBuilderType( VariableElement param, ProcessingContext context) { TypeMirror typeOfParameter = param.asType(); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/TypeNameAnalyser.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/TypeNameAnalyser.java index 7f3b9391..ec01125c 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/TypeNameAnalyser.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/TypeNameAnalyser.java @@ -76,4 +76,15 @@ public static boolean isList(TypeName typeName) { return Strings.CI.equals(typeName.getPackageName(), JAVA_UTIL_PACKAGE) && Strings.CI.equals(typeName.getClassName(), "List"); } + + /** + * Helper to check if the type is a {@code java.util.Optional}. + * + * @param typeName Type to be validated + * @return {@code true}, if it is a {@code java.util.Optional} + */ + public static boolean isOptional(TypeName typeName) { + return Strings.CI.equals(typeName.getPackageName(), JAVA_UTIL_PACKAGE) + && Strings.CI.equals(typeName.getClassName(), "Optional"); + } } From c72a95ef4ecbf93fe3743aad5f767d76257369f2 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 19 Oct 2025 19:54:54 +0200 Subject: [PATCH 02/12] Removing supplier without Optional, because it could not have both --- .../util/BuilderDefinitionCreator.java | 40 +------------------ 1 file changed, 2 insertions(+), 38 deletions(-) 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 8a169c97..92bae7a5 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 @@ -324,15 +324,8 @@ private static void addSupplierMethodsForField( return; } - // For Optional fields, create a supplier that provides T and wraps it in Optional - if (isOptional(fieldType) - && fieldType instanceof TypeNameGeneric fieldTypeGeneric - && fieldTypeGeneric.getInnerTypeArguments().size() == 1) { - TypeName innerType = fieldTypeGeneric.getInnerTypeArguments().get(0); - result.addMethod(createFieldSupplierWithTransform(fieldName, "Optional.of(%s)", innerType)); - } else { - result.addMethod(createFieldSupplier(fieldName, fieldType)); - } + // For all fields including Optional, use the real field type for suppliers + result.addMethod(createFieldSupplier(fieldName, fieldType)); } private static Optional createFieldFromSetter( @@ -536,35 +529,6 @@ private static MethodDto createFieldSupplier(String fieldName, TypeName fieldTyp return methodDto; } - private static MethodDto createFieldSupplierWithTransform( - String fieldName, String transform, TypeName supplierReturnType) { - TypeNameGeneric supplierType = - new TypeNameGeneric(map2TypeName(Supplier.class), supplierReturnType); - MethodParameterDto parameter = new MethodParameterDto(); - parameter.setParameterName(fieldName + SUFFIX_SUPPLIER); - parameter.setParameterTypeName(supplierType); - MethodDto methodDto = new MethodDto(); - methodDto.setMethodName(fieldName); - methodDto.addParameter(parameter); - methodDto.setModifier(Modifier.PUBLIC); - methodDto.setMethodType(MethodTypes.SUPPLIER); - String params; - if (StringUtils.isBlank(transform)) { - params = parameter.getParameterName() + ".get()"; - } else { - params = String.format(transform, parameter.getParameterName() + ".get()"); - } - methodDto.setCode( - """ - this.$fieldName:N = $builderFieldWrapper:T.changedValue($dtoMethodParams:N); - return this; - """); - methodDto.addArgument(ARG_FIELD_NAME, fieldName); - methodDto.addArgument(ARG_DTO_METHOD_PARAMS, params); - methodDto.addArgument(ARG_BUILDER_FIELD_WRAPPER, TypeName.of(TrackedValue.class)); - return methodDto; - } - private static Optional findBuilderType( VariableElement param, ProcessingContext context) { TypeMirror typeOfParameter = param.asType(); From 8ef770ccaa03d8836ec8b7029b1dd753badb4c9e Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 19 Oct 2025 19:58:31 +0200 Subject: [PATCH 03/12] Extending tests in shouldGenerateOptionalSettersAndProviders --- .../processor/BuilderProcessorTest.java | 46 ++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) 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 ec4673b2..1fe1676c 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 @@ -582,6 +582,50 @@ void shouldGenerateCollectionSettersAndProviders() { "private TrackedValue> map = unsetValue();"); } + @Test + void shouldGenerateOptionalSettersAndProviders() { + // Given + String packageName = "test"; + String className = "WithOptionals"; + String builderClassName = className + "Builder"; + + JavaFileObject sourceFile = + ProcessorTestUtils.simpleBuilderClass( + packageName, + className, + """ + private java.util.Optional description; + private java.util.Optional age; + + public java.util.Optional getDescription() { return description; } + public void setDescription(java.util.Optional description) { this.description = description; } + + public java.util.Optional getAge() { return age; } + public void setAge(java.util.Optional age) { this.age = age; } + """); + + // When + Compilation compilation = compile(sourceFile); + + // Then + String generatedCode = loadGeneratedSource(compilation, builderClassName); + assertGenerationSucceeded(compilation, builderClassName, generatedCode); + + // Direct setters with Optional and unwrapped T convenience + ProcessorAsserts.assertContaining( + generatedCode, + "public WithOptionalsBuilder description(Optional description)", + "public WithOptionalsBuilder description(Supplier> descriptionSupplier)", + "public WithOptionalsBuilder description(String description)", + "this.description = changedValue(Optional.of(description));", + "private TrackedValue> description = unsetValue();", + "public WithOptionalsBuilder age(Optional age)", + "public WithOptionalsBuilder age(Supplier> ageSupplier)", + "public WithOptionalsBuilder age(Integer age)", + "this.age = changedValue(Optional.of(age));", + "private TrackedValue> age = unsetValue();"); + } + @Test void shouldDeclarePerFieldBackingField() { // Given @@ -1205,7 +1249,7 @@ public class DtoWithWrappedTypes { "public DtoWithWrappedTypes build()"); // Verify no additional helper methods are generated for these generic types - // (since they don't match List/Set/Map/Optional patterns) + // (since they don't match List/Set/Map/Optional patterns with recognized inner types) ProcessorAsserts.assertNotContaining( optionalBuilder, "nameBuilderConsumer", From 517a61ec7121fe340a7e2e3001f154c20552366a Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 19 Oct 2025 20:11:57 +0200 Subject: [PATCH 04/12] Improving BuilderProcessorTest.shouldGenerateSupplierMethodForField --- .../builders/processor/BuilderProcessorTest.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) 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 1fe1676c..c267edc0 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 @@ -1112,12 +1112,16 @@ void shouldGenerateSupplierMethodForField() { String generatedCode = loadGeneratedSource(compilation, builderClassName); assertGenerationSucceeded(compilation, builderClassName, generatedCode); - // Expect supplier-based setter usage + // Expect supplier-based setter and backing field ProcessorAsserts.assertContaining( generatedCode, - "nameSupplier", "private TrackedValue name = unsetValue();", - "this.name = changedValue(nameSupplier.get());"); + """ + public HasSupplierBuilder name(Supplier nameSupplier) { + this.name = changedValue(nameSupplier.get()); + return this; + } + """); } protected Compilation compile(JavaFileObject... sourceFiles) { From c55690e24aef11e98079d9e1b7a800ec33849ee4 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 19 Oct 2025 20:21:03 +0200 Subject: [PATCH 05/12] Extending tests for string-format helper functions --- .../processor/BuilderProcessorTest.java | 98 ++++++++++++++++++- 1 file changed, 93 insertions(+), 5 deletions(-) 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 c267edc0..b4956eb0 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 @@ -24,24 +24,24 @@ void shouldGenerateBasicBuilder() { String className = "SimpleDto"; String builderClassName = className + "Builder"; + // When JavaFileObject sourceFile = ProcessorTestUtils.simpleBuilderClass( "test", className, """ - private String value; - public String getValue() { return value; } - public void setValue(String value) { this.value = value; } + private Integer value; + public Integer getValue() { return value; } + public void setValue(Integer value) { this.value = value; } """); - // When Compilation compilation = compile(sourceFile); // Then: Builder should be generated successfully String generatedCode = loadGeneratedSource(compilation, builderClassName); assertGenerationSucceeded(compilation, builderClassName, generatedCode); ProcessorAsserts.assertContaining( - generatedCode, "public SimpleDtoBuilder value(String value)", "public SimpleDto build()"); + generatedCode, "public SimpleDtoBuilder value(Integer value)", "public SimpleDto build()"); } @Test @@ -391,12 +391,14 @@ public RequiredFieldViaConstructor(String name, int age) { generatedCode, // Basic setters for constructor fields "public RequiredFieldViaConstructorBuilder name(String name)", + "public RequiredFieldViaConstructorBuilder name(String format, Object... args)", "public RequiredFieldViaConstructorBuilder age(int age)", // Supplier methods for constructor fields (the bug we fixed!) "public RequiredFieldViaConstructorBuilder name(Supplier nameSupplier)", "public RequiredFieldViaConstructorBuilder age(Supplier ageSupplier)", // Setter field should also have supplier "public RequiredFieldViaConstructorBuilder description(String description)", + "public RequiredFieldViaConstructorBuilder description(String format, Object... args)", "public RequiredFieldViaConstructorBuilder description(Supplier descriptionSupplier)"); } @@ -626,6 +628,92 @@ void shouldGenerateOptionalSettersAndProviders() { "private TrackedValue> age = unsetValue();"); } + @Test + void shouldGenerateStringFormatMethodForStringField() { + // Given: String field to verify format method generation + String packageName = "test"; + String className = "WithString"; + String builderClassName = className + "Builder"; + + JavaFileObject sourceFile = + ProcessorTestUtils.simpleBuilderClass( + packageName, + className, + """ + private String message; + + public String getMessage() { return message; } + public void setMessage(String message) { this.message = message; } + """); + + // When + Compilation compilation = compile(sourceFile); + + // Then + String generatedCode = loadGeneratedSource(compilation, builderClassName); + assertGenerationSucceeded(compilation, builderClassName, generatedCode); + + // Verify both basic String setter and format method are generated + ProcessorAsserts.assertContaining( + generatedCode, + "private TrackedValue message = unsetValue();", + """ + public WithStringBuilder message(String message) { + this.message = changedValue(message); + return this; + } + """, + """ + public WithStringBuilder message(String format, Object... args) { + this.message = changedValue(String.format(format, args)); + return this; + } + """); + } + + @Test + void shouldGenerateStringFormatMethodForOptionalString() { + // Given: Optional field to verify format method generation + String packageName = "test"; + String className = "WithOptionalString"; + String builderClassName = className + "Builder"; + + JavaFileObject sourceFile = + ProcessorTestUtils.simpleBuilderClass( + packageName, + className, + """ + private java.util.Optional message; + + public java.util.Optional getMessage() { return message; } + public void setMessage(java.util.Optional message) { this.message = message; } + """); + + // When + Compilation compilation = compile(sourceFile); + + // Then + String generatedCode = loadGeneratedSource(compilation, builderClassName); + assertGenerationSucceeded(compilation, builderClassName, generatedCode); + + // Verify both unwrapped String setter and format method are generated + ProcessorAsserts.assertContaining( + generatedCode, + "private TrackedValue> message = unsetValue();", + """ + public WithOptionalStringBuilder message(String message) { + this.message = changedValue(Optional.of(message)); + return this; + } + """, + """ + public WithOptionalStringBuilder message(String format, Object... args) { + this.message = changedValue(Optional.of(String.format(format, args))); + return this; + } + """); + } + @Test void shouldDeclarePerFieldBackingField() { // Given From a3519dc91115fd669a7afcaa02b1597ad62445f4 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 19 Oct 2025 21:04:04 +0200 Subject: [PATCH 06/12] Adding implementation of String.format for String fields --- .../util/BuilderDefinitionCreator.java | 46 +++++++++++++++++++ .../processor/util/TypeNameAnalyser.java | 11 +++++ 2 files changed, 57 insertions(+) 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 92bae7a5..e90269b2 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 @@ -63,6 +63,9 @@ public class BuilderDefinitionCreator { private static final String ARG_BUILDER_FIELD_WRAPPER = "builderFieldWrapper"; private static final String ARG_HELPER_TYPE = "helperType"; + // Type constants + private static final TypeName TRACKED_VALUE_TYPE = TypeName.of(TrackedValue.class); + // Parameter name suffixes private static final String SUFFIX_CONSUMER = "Consumer"; private static final String SUFFIX_SUPPLIER = "Supplier"; @@ -230,6 +233,12 @@ private static boolean isMethodRelevantForBuilder( private static void addAdditionalHelperMethodsForField( FieldDto result, String fieldName, TypeName fieldType) { + // Check for String type (not array) and add format method + if (isString(fieldType) && !(fieldType instanceof TypeNameArray)) { + result.addMethod( + createStringFormatMethodWithTransform(fieldName, "String.format(format, args)")); + } + // Only process generic types (List, Set, Map, Optional, etc.) if (!(fieldType instanceof TypeNameGeneric fieldTypeGeneric)) { return; @@ -257,6 +266,14 @@ private static void addAdditionalHelperMethodsForField( // Add setter that accepts the inner type T and wraps it in Optional.of() result.addMethod( createFieldSetterWithTransform(fieldName, "Optional.of(%s)", innerTypes.get(0))); + + // If Optional, add format method + TypeName innerType = innerTypes.get(0); + if (isString(innerType)) { + result.addMethod( + createStringFormatMethodWithTransform( + fieldName, "Optional.of(String.format(format, args))")); + } } } @@ -529,6 +546,35 @@ private static MethodDto createFieldSupplier(String fieldName, TypeName fieldTyp return methodDto; } + private static MethodDto createStringFormatMethodWithTransform( + String fieldName, String transform) { + TypeName stringType = new TypeName("java.lang", "String"); + + MethodParameterDto formatParam = new MethodParameterDto(); + formatParam.setParameterName("format"); + formatParam.setParameterTypeName(stringType); + + MethodParameterDto argsParam = new MethodParameterDto(); + argsParam.setParameterName("args"); + argsParam.setParameterTypeName(new TypeNameArray(TypeName.of(Object.class), false)); + + MethodDto methodDto = new MethodDto(); + methodDto.setMethodName(fieldName); + methodDto.addParameter(formatParam); + methodDto.addParameter(argsParam); + methodDto.setModifier(Modifier.PUBLIC); + methodDto.setMethodType(MethodTypes.PROXY); + methodDto.setCode( + """ + this.$fieldName:N = $builderFieldWrapper:T.changedValue($transform:N); + return this; + """); + methodDto.addArgument(ARG_FIELD_NAME, fieldName); + methodDto.addArgument("transform", transform); + methodDto.addArgument(ARG_BUILDER_FIELD_WRAPPER, TypeName.of(TrackedValue.class)); + return methodDto; + } + private static Optional findBuilderType( VariableElement param, ProcessingContext context) { TypeMirror typeOfParameter = param.asType(); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/TypeNameAnalyser.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/TypeNameAnalyser.java index ec01125c..1aa365c6 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/TypeNameAnalyser.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/TypeNameAnalyser.java @@ -87,4 +87,15 @@ public static boolean isOptional(TypeName typeName) { return Strings.CI.equals(typeName.getPackageName(), JAVA_UTIL_PACKAGE) && Strings.CI.equals(typeName.getClassName(), "Optional"); } + + /** + * Helper to check if the type is a {@code java.lang.String}. + * + * @param typeName Type to be validated + * @return {@code true}, if it is a {@code java.lang.String} + */ + public static boolean isString(TypeName typeName) { + return Strings.CI.equals(typeName.getPackageName(), "java.lang") + && Strings.CI.equals(typeName.getClassName(), "String"); + } } From 8358e4e2bef7daae4e41d53788bbb5f5d25cf6d6 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 19 Oct 2025 21:06:08 +0200 Subject: [PATCH 07/12] Replacing TypeName.of(TrackedValue.class) by TRACKED_VALUE_TYPE --- .../processor/util/BuilderDefinitionCreator.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 e90269b2..5a82ccdd 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 @@ -467,7 +467,7 @@ private static MethodDto createFieldSetterWithTransform( """); methodDto.addArgument(ARG_FIELD_NAME, fieldName); methodDto.addArgument(ARG_DTO_METHOD_PARAMS, params); - methodDto.addArgument(ARG_BUILDER_FIELD_WRAPPER, TypeName.of(TrackedValue.class)); + methodDto.addArgument(ARG_BUILDER_FIELD_WRAPPER, TRACKED_VALUE_TYPE); return methodDto; } @@ -491,7 +491,7 @@ private static MethodDto createFieldConsumer(String fieldName, TypeName fieldTyp methodDto.addArgument(ARG_FIELD_NAME, fieldName); methodDto.addArgument(ARG_DTO_METHOD_PARAM, parameter.getParameterName()); methodDto.addArgument(ARG_HELPER_TYPE, fieldType); - methodDto.addArgument(ARG_BUILDER_FIELD_WRAPPER, TypeName.of(TrackedValue.class)); + methodDto.addArgument(ARG_BUILDER_FIELD_WRAPPER, TRACKED_VALUE_TYPE); return methodDto; } @@ -521,7 +521,7 @@ private static MethodDto createFieldConsumerWithBuilder(String fieldName, TypeNa methodDto.addArgument(ARG_FIELD_NAME, fieldName); methodDto.addArgument(ARG_DTO_METHOD_PARAM, parameter.getParameterName()); methodDto.addArgument(ARG_HELPER_TYPE, builderType); - methodDto.addArgument(ARG_BUILDER_FIELD_WRAPPER, TypeName.of(TrackedValue.class)); + methodDto.addArgument(ARG_BUILDER_FIELD_WRAPPER, TRACKED_VALUE_TYPE); return methodDto; } @@ -542,7 +542,7 @@ private static MethodDto createFieldSupplier(String fieldName, TypeName fieldTyp """); methodDto.addArgument(ARG_FIELD_NAME, fieldName); methodDto.addArgument(ARG_DTO_METHOD_PARAM, parameter.getParameterName()); - methodDto.addArgument(ARG_BUILDER_FIELD_WRAPPER, TypeName.of(TrackedValue.class)); + methodDto.addArgument(ARG_BUILDER_FIELD_WRAPPER, TRACKED_VALUE_TYPE); return methodDto; } @@ -571,7 +571,7 @@ private static MethodDto createStringFormatMethodWithTransform( """); methodDto.addArgument(ARG_FIELD_NAME, fieldName); methodDto.addArgument("transform", transform); - methodDto.addArgument(ARG_BUILDER_FIELD_WRAPPER, TypeName.of(TrackedValue.class)); + methodDto.addArgument(ARG_BUILDER_FIELD_WRAPPER, TRACKED_VALUE_TYPE); return methodDto; } From 65558998719b27250ca7d95635618ba4be479e8e Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 19 Oct 2025 21:06:23 +0200 Subject: [PATCH 08/12] Extending a test, description gets format too --- .../simple/builders/processor/BuilderProcessorTest.java | 1 + 1 file changed, 1 insertion(+) 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 b4956eb0..e1a7027d 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 @@ -619,6 +619,7 @@ void shouldGenerateOptionalSettersAndProviders() { "public WithOptionalsBuilder description(Optional description)", "public WithOptionalsBuilder description(Supplier> descriptionSupplier)", "public WithOptionalsBuilder description(String description)", + "public WithOptionalsBuilder description(String format, Object... args)", "this.description = changedValue(Optional.of(description));", "private TrackedValue> description = unsetValue();", "public WithOptionalsBuilder age(Optional age)", From b4b6eee9bfe95038234bb41eaaae78fe5d0e7f10 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 19 Oct 2025 21:23:45 +0200 Subject: [PATCH 09/12] Adding tests for StringBuilder-Helper method for strings --- .../processor/BuilderProcessorTest.java | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) 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 e1a7027d..22eda7c2 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 @@ -715,6 +715,83 @@ public WithOptionalStringBuilder message(String format, Object... args) { """); } + @Test + void shouldGenerateStringBuilderConsumerForStringField() { + // Given: String field to verify StringBuilder consumer generation + String packageName = "test"; + String className = "WithStringBuilder"; + String builderClassName = className + "Builder"; + + JavaFileObject sourceFile = + ProcessorTestUtils.simpleBuilderClass( + packageName, + className, + """ + private String message; + + public String getMessage() { return message; } + public void setMessage(String message) { this.message = message; } + """); + + // When + Compilation compilation = compile(sourceFile); + + // Then + String generatedCode = loadGeneratedSource(compilation, builderClassName); + assertGenerationSucceeded(compilation, builderClassName, generatedCode); + + // Verify StringBuilder consumer method is generated + ProcessorAsserts.assertContaining( + generatedCode, + "private TrackedValue message = unsetValue();", + """ + public WithStringBuilderBuilder message(Consumer messageStringBuilderConsumer) { + StringBuilder builder = new StringBuilder(); + messageStringBuilderConsumer.accept(builder); + this.message = changedValue(builder.toString()); + return this; + } + """); + } + + @Test + void shouldGenerateStringBuilderConsumerForOptionalString() { + // Given: Optional field to verify StringBuilder consumer generation + String packageName = "test"; + String className = "WithOptString"; + String builderClassName = className + "Builder"; + + JavaFileObject sourceFile = + ProcessorTestUtils.simpleBuilderClass( + packageName, + className, + """ + private java.util.Optional message; + + public java.util.Optional getMessage() { return message; } + public void setMessage(java.util.Optional message) { this.message = message; } + """); + + // When + Compilation compilation = compile(sourceFile); + + // Then + String generatedCode = loadGeneratedSource(compilation, builderClassName); + assertGenerationSucceeded(compilation, builderClassName, generatedCode); + + // Verify StringBuilder consumer method is generated with Optional wrapping + ProcessorAsserts.assertContaining( + generatedCode, + """ + public WithOptStringBuilder message(Consumer messageStringBuilderConsumer) { + StringBuilder builder = new StringBuilder(); + messageStringBuilderConsumer.accept(builder); + this.message = changedValue(Optional.of(builder.toString())); + return this; + } + """); + } + @Test void shouldDeclarePerFieldBackingField() { // Given From 0e9df8164bc223703238bc1e9ad8236c4e980f42 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 19 Oct 2025 21:24:23 +0200 Subject: [PATCH 10/12] Adding first implementation of StringBuilder for helper-function generation --- .../util/BuilderDefinitionCreator.java | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) 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 5a82ccdd..f6997a4e 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 @@ -292,6 +292,20 @@ private static void addConsumerMethodsForField( if (isFunctionalInterface(fieldTypeElement)) { return; } + + // Add StringBuilder consumer for String fields + if (isString(fieldType) && !(fieldType instanceof TypeNameArray)) { + result.addMethod(createStringBuilderConsumer(fieldName, "builder.toString()")); + } + + // Add StringBuilder consumer for Optional fields + if (fieldType instanceof TypeNameGeneric fieldTypeGeneric + && isOptional(fieldType) + && fieldTypeGeneric.getInnerTypeArguments().size() == 1 + && isString(fieldTypeGeneric.getInnerTypeArguments().get(0))) { + result.addMethod(createStringBuilderConsumer(fieldName, "Optional.of(builder.toString())")); + } + Optional builderTypeOpt = findBuilderType(fieldParameter, context); if (builderTypeOpt.isPresent()) { TypeName builderType = builderTypeOpt.get(); @@ -495,6 +509,32 @@ private static MethodDto createFieldConsumer(String fieldName, TypeName fieldTyp return methodDto; } + private static MethodDto createStringBuilderConsumer(String fieldName, String transform) { + TypeName stringBuilderType = new TypeName("java.lang", "StringBuilder"); + TypeNameGeneric consumerType = + new TypeNameGeneric(map2TypeName(Consumer.class), stringBuilderType); + MethodParameterDto parameter = new MethodParameterDto(); + parameter.setParameterName(fieldName + "StringBuilderConsumer"); + parameter.setParameterTypeName(consumerType); + MethodDto methodDto = new MethodDto(); + methodDto.setMethodName(fieldName); + methodDto.addParameter(parameter); + methodDto.setModifier(Modifier.PUBLIC); + methodDto.setMethodType(MethodTypes.CONSUMER); + methodDto.setCode( + """ + StringBuilder builder = new StringBuilder(); + $dtoMethodParam:N.accept(builder); + this.$fieldName:N = $builderFieldWrapper:T.changedValue($transform:N); + return this; + """); + methodDto.addArgument(ARG_FIELD_NAME, fieldName); + methodDto.addArgument(ARG_DTO_METHOD_PARAM, parameter.getParameterName()); + methodDto.addArgument("transform", transform); + methodDto.addArgument(ARG_BUILDER_FIELD_WRAPPER, TRACKED_VALUE_TYPE); + return methodDto; + } + private static MethodDto createFieldConsumerWithBuilder( String fieldName, TypeName builderType, TypeName builderTargetType) { TypeNameGeneric builderTypeGeneric = new TypeNameGeneric(builderType, builderTargetType); From 8e9acdcc1c244619753216831f3126870a5d7e38 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 19 Oct 2025 21:31:11 +0200 Subject: [PATCH 11/12] Refactoring code so that having StringBuilder generation only in one place --- .../util/BuilderDefinitionCreator.java | 50 ++++++++++++++----- .../processor/BuilderProcessorTest.java | 2 +- 2 files changed, 38 insertions(+), 14 deletions(-) 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 f6997a4e..1f00efc0 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 @@ -293,19 +293,6 @@ private static void addConsumerMethodsForField( return; } - // Add StringBuilder consumer for String fields - if (isString(fieldType) && !(fieldType instanceof TypeNameArray)) { - result.addMethod(createStringBuilderConsumer(fieldName, "builder.toString()")); - } - - // Add StringBuilder consumer for Optional fields - if (fieldType instanceof TypeNameGeneric fieldTypeGeneric - && isOptional(fieldType) - && fieldTypeGeneric.getInnerTypeArguments().size() == 1 - && isString(fieldTypeGeneric.getInnerTypeArguments().get(0))) { - result.addMethod(createStringBuilderConsumer(fieldName, "Optional.of(builder.toString())")); - } - Optional builderTypeOpt = findBuilderType(fieldParameter, context); if (builderTypeOpt.isPresent()) { TypeName builderType = builderTypeOpt.get(); @@ -345,6 +332,11 @@ && hasEmptyConstructor(fieldTypeElement, context)) { 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)); } } @@ -615,6 +607,38 @@ private static MethodDto createStringFormatMethodWithTransform( return methodDto; } + /** + * Checks if a StringBuilder consumer should be generated for the given field type. This applies + * to plain String fields and Optional<String> fields. + * + * @param fieldType the type of the field + * @return true if StringBuilder consumer should be generated, false otherwise + */ + private static boolean shouldGenerateStringBuilderConsumer(TypeName fieldType) { + // Check for plain String (not array) + if (isString(fieldType) && !(fieldType instanceof TypeNameArray)) { + return true; + } + + // Check for Optional + return isOptionalString(fieldType); + } + + /** + * Checks if the field type is Optional<String>. + * + * @param fieldType the type of the field + * @return true if the type is Optional<String>, false otherwise + */ + private static boolean isOptionalString(TypeName fieldType) { + if (fieldType instanceof TypeNameGeneric fieldTypeGeneric + && isOptional(fieldType) + && fieldTypeGeneric.getInnerTypeArguments().size() == 1) { + return isString(fieldTypeGeneric.getInnerTypeArguments().get(0)); + } + return false; + } + private static Optional findBuilderType( VariableElement param, ProcessingContext context) { TypeMirror typeOfParameter = param.asType(); 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 22eda7c2..6db98c43 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 @@ -790,7 +790,7 @@ public WithOptStringBuilder message(Consumer messageStringBuilder return this; } """); - } + } @Test void shouldDeclarePerFieldBackingField() { From b9869c4875f7a646f0175e6c67d46aedf8da8899 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 19 Oct 2025 21:35:58 +0200 Subject: [PATCH 12/12] Moving isOptionalString helper-function to TypeNameAnalyser --- .../processor/util/BuilderDefinitionCreator.java | 15 --------------- .../processor/util/TypeNameAnalyser.java | 16 ++++++++++++++++ 2 files changed, 16 insertions(+), 15 deletions(-) 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 1f00efc0..0cbf7220 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 @@ -624,21 +624,6 @@ private static boolean shouldGenerateStringBuilderConsumer(TypeName fieldType) { return isOptionalString(fieldType); } - /** - * Checks if the field type is Optional<String>. - * - * @param fieldType the type of the field - * @return true if the type is Optional<String>, false otherwise - */ - private static boolean isOptionalString(TypeName fieldType) { - if (fieldType instanceof TypeNameGeneric fieldTypeGeneric - && isOptional(fieldType) - && fieldTypeGeneric.getInnerTypeArguments().size() == 1) { - return isString(fieldTypeGeneric.getInnerTypeArguments().get(0)); - } - return false; - } - private static Optional findBuilderType( VariableElement param, ProcessingContext context) { TypeMirror typeOfParameter = param.asType(); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/TypeNameAnalyser.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/TypeNameAnalyser.java index 1aa365c6..3d86108c 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/TypeNameAnalyser.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/TypeNameAnalyser.java @@ -25,6 +25,7 @@ import org.apache.commons.lang3.Strings; import org.javahelpers.simple.builders.processor.dtos.TypeName; +import org.javahelpers.simple.builders.processor.dtos.TypeNameGeneric; /** Helperclass to extract insights from TypeName. */ public class TypeNameAnalyser { @@ -98,4 +99,19 @@ public static boolean isString(TypeName typeName) { return Strings.CI.equals(typeName.getPackageName(), "java.lang") && Strings.CI.equals(typeName.getClassName(), "String"); } + + /** + * Checks if the field type is Optional<String>. + * + * @param fieldType the type of the field + * @return true if the type is Optional<String>, false otherwise + */ + public static boolean isOptionalString(TypeName fieldType) { + if (fieldType instanceof TypeNameGeneric fieldTypeGeneric + && isOptional(fieldType) + && fieldTypeGeneric.getInnerTypeArguments().size() == 1) { + return isString(fieldTypeGeneric.getInnerTypeArguments().get(0)); + } + return false; + } }