From 1eb58be04570eddb1dc2389f91b852a31f65f3eb Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sat, 4 Apr 2026 11:21:28 +0200 Subject: [PATCH 01/30] Renaming BuilderDefinitionDto to GenerationTargetClassDto for generalisation --- .../{BuilderDefinitionDto.java => GenerationTargetClassDto.java} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/{BuilderDefinitionDto.java => GenerationTargetClassDto.java} (100%) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderDefinitionDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/GenerationTargetClassDto.java similarity index 100% rename from processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderDefinitionDto.java rename to processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/GenerationTargetClassDto.java From 4c1bf84eb8174ddc104cbf8dc51f3b10be6caf02 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 5 Apr 2026 20:08:00 +0200 Subject: [PATCH 02/30] Fixing expectations because primitive types do not need a null check when being taken via getters --- .../builders/example/BookDtoBuilder.java | 58 ------------- .../example/JacksonIntegrationDtoBuilder.java | 4 - .../example/ProductRecordBuilder.java | 4 - .../processor/BuilderProcessorTest.java | 87 +++++++++++++++++++ 4 files changed, 87 insertions(+), 66 deletions(-) diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java index 94e880ca..1aef439d 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 @@ -10,8 +10,6 @@ import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.function.Consumer; -import java.util.function.Supplier; import org.apache.commons.lang3.builder.ToStringBuilder; import org.javahelpers.simple.builders.core.util.BuilderToStringStyle; import org.javahelpers.simple.builders.core.util.TrackedValue; @@ -116,52 +114,20 @@ public BookDtoBuilder() { public BookDtoBuilder(BookDto instance) { this.author = initialValue(instance.getAuthor()); this.available = initialValue(instance.isAvailable()); - if (this.available.value() == null) { - throw new IllegalArgumentException( - "Cannot initialize builder from instance: field 'available' is marked as non-null but source object has null value"); - } this.category = initialValue(instance.getCategory()); - if (this.category.value() == null) { - throw new IllegalArgumentException( - "Cannot initialize builder from instance: field 'category' is marked as non-null but source object has null value"); - } this.discount = initialValue(instance.getDiscount()); - if (this.discount.value() == null) { - throw new IllegalArgumentException( - "Cannot initialize builder from instance: field 'discount' is marked as non-null but source object has null value"); - } this.edition = initialValue(instance.getEdition()); - if (this.edition.value() == null) { - throw new IllegalArgumentException( - "Cannot initialize builder from instance: field 'edition' is marked as non-null but source object has null value"); - } this.exactPrice = initialValue(instance.getExactPrice()); this.genres = initialValue(instance.getGenres()); this.isbn = initialValue(instance.getIsbn()); this.lastUpdated = initialValue(instance.getLastUpdated()); this.metadata = initialValue(instance.getMetadata()); this.pages = initialValue(instance.getPages()); - if (this.pages.value() == null) { - throw new IllegalArgumentException( - "Cannot initialize builder from instance: field 'pages' is marked as non-null but source object has null value"); - } this.price = initialValue(instance.getPrice()); - if (this.price.value() == null) { - throw new IllegalArgumentException( - "Cannot initialize builder from instance: field 'price' is marked as non-null but source object has null value"); - } this.publishDate = initialValue(instance.getPublishDate()); this.publisher = initialValue(instance.getPublisher()); this.rating = initialValue(instance.getRating()); - if (this.rating.value() == null) { - throw new IllegalArgumentException( - "Cannot initialize builder from instance: field 'rating' is marked as non-null but source object has null value"); - } this.salesCount = initialValue(instance.getSalesCount()); - if (this.salesCount.value() == null) { - throw new IllegalArgumentException( - "Cannot initialize builder from instance: field 'salesCount' is marked as non-null but source object has null value"); - } this.subtitle = initialValue(instance.getSubtitle()); this.tags = initialValue(instance.getTags()); this.title = initialValue(instance.getTitle()); @@ -428,30 +394,6 @@ BookDtoBuilder validateTitle() { * Builds the configured DTO instance. */ public BookDto build() { - if (this.available.isSet() && this.available.value() == null) { - throw new IllegalStateException("Field 'available' is marked as non-null but null value was provided"); - } - if (this.category.isSet() && this.category.value() == null) { - throw new IllegalStateException("Field 'category' is marked as non-null but null value was provided"); - } - if (this.discount.isSet() && this.discount.value() == null) { - throw new IllegalStateException("Field 'discount' is marked as non-null but null value was provided"); - } - if (this.edition.isSet() && this.edition.value() == null) { - throw new IllegalStateException("Field 'edition' is marked as non-null but null value was provided"); - } - if (this.pages.isSet() && this.pages.value() == null) { - throw new IllegalStateException("Field 'pages' is marked as non-null but null value was provided"); - } - if (this.price.isSet() && this.price.value() == null) { - throw new IllegalStateException("Field 'price' is marked as non-null but null value was provided"); - } - if (this.rating.isSet() && this.rating.value() == null) { - throw new IllegalStateException("Field 'rating' is marked as non-null but null value was provided"); - } - if (this.salesCount.isSet() && this.salesCount.value() == null) { - throw new IllegalStateException("Field 'salesCount' is marked as non-null but null value was provided"); - } BookDto result = new BookDto(); this.author.ifSet(result::setAuthor); this.available.ifSet(result::setAvailable); diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/JacksonIntegrationDtoBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/JacksonIntegrationDtoBuilder.java index f430b042..b9aba88d 100644 --- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/JacksonIntegrationDtoBuilder.java +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/JacksonIntegrationDtoBuilder.java @@ -50,10 +50,6 @@ public JacksonIntegrationDtoBuilder() { public JacksonIntegrationDtoBuilder(JacksonIntegrationDto instance) { this.name = initialValue(instance.name()); this.age = initialValue(instance.age()); - if (this.age.value() == null) { - throw new IllegalArgumentException( - "Cannot initialize builder from instance: field 'age' is marked as non-null but source object has null value"); - } } /** 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 fc7d430a..adc17e48 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 @@ -51,10 +51,6 @@ public ProductRecordBuilder() { public ProductRecordBuilder(ProductRecord instance) { this.name = initialValue(instance.name()); this.price = initialValue(instance.price()); - if (this.price.value() == null) { - throw new IllegalArgumentException( - "Cannot initialize builder from instance: field 'price' is marked as non-null but source object has null value"); - } this.category = initialValue(instance.category()); } 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 cbb3f980..a4c5951c 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 @@ -1134,6 +1134,93 @@ void shouldGenerateBothConstructorsWithJavadocs() { "private TrackedValue x = unsetValue();"); } + @Test + void shouldValidateNonNullFieldsInFromInstanceConstructor() { + // Given + String className = "NonNullValidation"; + String builderClassName = className + "Builder"; + + JavaFileObject notNullAnnotation = + ProcessorTestUtils.forSource( + """ + package jakarta.validation.constraints; + import java.lang.annotation.*; + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.FIELD, ElementType.PARAMETER}) + public @interface NotNull { + String message() default ""; + } + """); + + JavaFileObject sourceFile = + ProcessorTestUtils.forSource( + """ + package test; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + import jakarta.validation.constraints.NotNull; + + @SimpleBuilder + public class NonNullValidation { + private String requiredField; + private String optionalField; + private int primitiveField; + + public String getRequiredField() { return requiredField; } + public void setRequiredField(@NotNull String requiredField) { + this.requiredField = requiredField; + } + + public String getOptionalField() { return optionalField; } + public void setOptionalField(String optionalField) { + this.optionalField = optionalField; + } + + public int getPrimitiveField() { return primitiveField; } + public void setPrimitiveField(int primitiveField) { + this.primitiveField = primitiveField; + } + } + """); + + // When + Compilation compilation = compile(notNullAnnotation, sourceFile); + + // Then + String generatedCode = loadGeneratedSource(compilation, builderClassName); + assertGenerationSucceeded(compilation, builderClassName, generatedCode); + + // Verify from-instance constructor validates @NotNull fields but not nullable/primitive fields + ProcessorAsserts.assertContaining( + generatedCode, + """ + public NonNullValidationBuilder(NonNullValidation instance) { + this.optionalField = initialValue(instance.getOptionalField()); + this.primitiveField = initialValue(instance.getPrimitiveField()); + if (instance.getRequiredField() == null) { + throw new IllegalArgumentException( + "Field 'requiredField' is non-null but instance.getRequiredField() returned null"); + } + this.requiredField = initialValue(instance.getRequiredField()); + } + """); + + // Verify build() method has validation for @NotNull field + ProcessorAsserts.assertContaining( + generatedCode, + """ + public NonNullValidation build() { + if (this.requiredField.isSet() && this.requiredField.value() == null) { + throw new IllegalStateException("Field 'requiredField' is marked as non-null but null value was provided"); + } + NonNullValidation result = new NonNullValidation(); + this.optionalField.ifSet(result::setOptionalField); + this.primitiveField.ifSet(result::setPrimitiveField); + this.requiredField.ifSet(result::setRequiredField); + return result; + } + """); + } + @Test void shouldGenerateMethodJavadocsForAllMethodTypes() { // Given From ac1e8e24d73dc4dedf50495796faff52991e0626 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Mon, 6 Apr 2026 19:07:59 +0200 Subject: [PATCH 03/30] 1st state of refactoring to have a GenerationTargetClassDto and a builder independent RoasterCodeGenerator --- .../builders/processor/BuilderProcessor.java | 2 +- .../roaster/RoasterCodeGenerator.java | 316 ++++++++---------- .../builder/ConstructorEnhancer.java | 158 +++++++++ .../builder/CoreMethodsEnhancer.java | 24 +- .../field/AddToCollectionGenerator.java | 8 +- .../field/ArrayBuilderConsumerGenerator.java | 4 + .../field/OptionalHelperGenerator.java | 3 + .../field/StringBuilderConsumerGenerator.java | 6 + .../field/StringFormatHelperGenerator.java | 4 + .../field/VarArgsHelperGenerator.java | 16 +- .../model/core/BuilderDefinitionDto.java | 202 +++++++++++ .../processor/model/core/ClassFieldDto.java | 105 ++++++ .../model/core/GenerationTargetClassDto.java | 203 ++++------- .../model/method/ConstructorDto.java | 153 +++++++++ .../processor/model/method/MethodCodeDto.java | 23 ++ .../processor/model/method/MethodDto.java | 31 ++ .../processor/model/type/NestedTypeDto.java | 13 + ...le.builders.processor.generators.Generator | 1 + 18 files changed, 948 insertions(+), 324 deletions(-) create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ConstructorEnhancer.java create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderDefinitionDto.java create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/ClassFieldDto.java create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/ConstructorDto.java diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java index 5ab749ff..a890e4c0 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java @@ -199,7 +199,7 @@ private void process(Element annotatedElement, BuilderConfiguration config) throws BuilderException { context.initConfigurationForProcessingTarget(config); BuilderDefinitionDto builderDef = extractFromElement(annotatedElement, context); - codeGenerator.generateBuilder(builderDef); + codeGenerator.generateClass(builderDef); // Collect info for Jackson Module if enabled jacksonModuleGenerator.addEntry(builderDef, annotatedElement); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java index 4360b33e..b95d47b0 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java @@ -24,7 +24,6 @@ package org.javahelpers.simple.builders.processor.classgen.roaster; -import static org.javahelpers.simple.builders.processor.classgen.roaster.RoasterMapper.mapBoxedType; import static org.javahelpers.simple.builders.processor.classgen.roaster.RoasterMapper.mapType; import static org.javahelpers.simple.builders.processor.classgen.roaster.RoasterMapper.packageNameOf; import static org.javahelpers.simple.builders.processor.classgen.roaster.RoasterMapper.resolveCodeTemplate; @@ -38,8 +37,6 @@ import java.util.Map; import java.util.Properties; import java.util.Set; -import java.util.function.Consumer; -import java.util.function.Supplier; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; @@ -47,16 +44,18 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.javahelpers.simple.builders.core.util.TrackedValue; -import org.javahelpers.simple.builders.processor.analysis.JavaLangMapper; import org.javahelpers.simple.builders.processor.exceptions.BuilderException; import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto; import org.javahelpers.simple.builders.processor.model.annotation.InterfaceName; import org.javahelpers.simple.builders.processor.model.core.BuilderDefinitionDto; +import org.javahelpers.simple.builders.processor.model.core.ClassFieldDto; import org.javahelpers.simple.builders.processor.model.core.FieldDto; +import org.javahelpers.simple.builders.processor.model.core.GenerationTargetClassDto; import org.javahelpers.simple.builders.processor.model.integration.JacksonModuleDefinitionDto; import org.javahelpers.simple.builders.processor.model.integration.JacksonModuleEntryDto; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocTagDto; +import org.javahelpers.simple.builders.processor.model.method.ConstructorDto; import org.javahelpers.simple.builders.processor.model.method.MethodCodePlaceholder; import org.javahelpers.simple.builders.processor.model.method.MethodCodeTypePlaceholder; import org.javahelpers.simple.builders.processor.model.method.MethodDto; @@ -72,7 +71,6 @@ import org.jboss.forge.roaster.model.source.AnnotationSource; import org.jboss.forge.roaster.model.source.FieldSource; import org.jboss.forge.roaster.model.source.JavaClassSource; -import org.jboss.forge.roaster.model.source.JavaDocSource; import org.jboss.forge.roaster.model.source.JavaInterfaceSource; import org.jboss.forge.roaster.model.source.JavaSource; import org.jboss.forge.roaster.model.source.MethodSource; @@ -105,72 +103,85 @@ public RoasterCodeGenerator(ProcessingEnvironment processingEnv, ProcessingLogge } /** - * Generates a builder class from the given builder definition. + * Generates a class from the given class definition. * - * @param builderDef dto of all information to create the builder + * @param classDef DTO containing all information to create the class * @throws BuilderException if there is an error in source code generation */ - public void generateBuilder(BuilderDefinitionDto builderDef) throws BuilderException { + public void generateClass(GenerationTargetClassDto classDef) throws BuilderException { logger.debugStartOperation( - "Code generation for builder: %s", builderDef.getBuilderTypeName().getClassName()); + "Code generation for class: %s", classDef.getTypeName().getClassName()); - String sourceCode = createBuilderSource(builderDef); - writeBuilderClassToFile(sourceCode, builderDef); + String sourceCode = createClassSource(classDef); + writeClassToFile(sourceCode, classDef); logger.debugEndOperation( - "Successfully generated builder: %s", builderDef.getBuilderTypeName().getClassName()); + "Successfully generated class: %s", classDef.getTypeName().getClassName()); } - private String createBuilderSource(BuilderDefinitionDto builderDef) { - JavaClassSource source = createClassSource(builderDef); - addClassMetadata(source, builderDef); - appendFields(source, builderDef); - appendConstructors(source, builderDef); - appendMethods(source, builderDef); - appendNestedTypes(source, builderDef); - applyClassAnnotations(source, builderDef); + private String createClassSource(GenerationTargetClassDto classDef) { + // Class generation - works for all GenerationTargetClassDto instances + JavaClassSource source = createJavaClassSource(classDef); + addClassMetadata(source, classDef); + appendFields(source, classDef); + appendConstructors(source, classDef); + appendMethods(source, classDef); + appendNestedTypes(source, classDef); + + // Apply annotations first (Roaster will add imports) + applyClassAnnotations(source, classDef); + return renderClassSource(source); } - private void applyClassAnnotations(JavaClassSource source, BuilderDefinitionDto builderDef) { - if (CollectionUtils.isEmpty(builderDef.getClassAnnotations())) { + private void applyClassAnnotations(JavaClassSource source, GenerationTargetClassDto classDef) { + if (CollectionUtils.isEmpty(classDef.getClassAnnotations())) { return; } - // Adding annotations from enhancers - applyAnnotations(source, builderDef.getClassAnnotations()); + // Adding class annotations + applyAnnotations(source, classDef.getClassAnnotations()); logger.debug("Class-level annotations added"); } - private JavaClassSource createClassSource(BuilderDefinitionDto builderDef) { - if (CollectionUtils.isNotEmpty(builderDef.getGenerics())) { - logger.debug("Builder has %d generic type parameter(s)", builderDef.getGenerics().size()); + private JavaClassSource createJavaClassSource(GenerationTargetClassDto classDef) { + if (CollectionUtils.isNotEmpty(classDef.getGenerics())) { + logger.debug("Class has %d generic type parameter(s)", classDef.getGenerics().size()); } JavaClassSource source = Roaster.create(JavaClassSource.class); - String packageName = builderDef.getBuilderTypeName().getPackageName(); + String packageName = classDef.getTypeName().getPackageName(); if (StringUtils.isNotBlank(packageName)) { source.setPackage(packageName); } else { source.setDefaultPackage(); } - source.setName(builderDef.getBuilderTypeName().getClassName()); - addGenericDeclarations(source, builderDef.getGenerics()); + source.setName(classDef.getTypeName().getClassName()); + addGenericDeclarations(source, classDef.getGenerics()); addTrackedValueStaticImports(source); - collectImports(builderDef).stream().sorted().forEach(source::addImport); - logger.debug("Class builder created"); + + // Collect and add imports early (before elements are added) + collectImports(classDef).stream().sorted().forEach(source::addImport); + + logger.debug("JavaClassSource created"); return source; } - private void addClassMetadata(JavaClassSource source, BuilderDefinitionDto builderDef) { - // Add class JavaDoc if provided by enhancer - applyJavadoc(source, builderDef.getClassJavadoc()); + private void addClassMetadata(JavaClassSource source, GenerationTargetClassDto classDef) { + // Add class JavaDoc + applyJavadoc(source, classDef.getClassJavadoc()); - // Set builder class access level - applyVisibility( - source, JavaLangMapper.mapAccessModifier(builderDef.getConfiguration().getBuilderAccess())); + // Set class access level + if (classDef.getClassAccessModifier() != null) { + applyVisibility(source, classDef.getClassAccessModifier()); + } + + // Add superclass if specified + if (classDef.getSuperType() != null) { + source.setSuperType(mapType(classDef.getSuperType())); + } - // Adding interfaces from enhancers - for (InterfaceName interfaceName : builderDef.getInterfaces()) { + // Add interfaces + for (InterfaceName interfaceName : classDef.getInterfaces()) { source.addInterface(RoasterMapper.mapInterfaceToTypeName(interfaceName)); } @@ -182,129 +193,65 @@ private String renderClassSource(JavaClassSource source) { return formatSource(rendered); } - private void appendFields(JavaClassSource source, BuilderDefinitionDto builderDef) { - logger.debugStartOperation( - "Generating %d constructor fields and %d setter fields", - builderDef.getConstructorFieldsForBuilder().size(), - builderDef.getSetterFieldsForBuilder().size()); + private void appendFields(JavaClassSource source, GenerationTargetClassDto classDef) { + logger.debugStartOperation("Generating %d fields", classDef.getClassFields().size()); - for (FieldDto fieldDto : builderDef.getConstructorFieldsForBuilder()) { - appendField(source, fieldDto); - } - for (FieldDto fieldDto : builderDef.getSetterFieldsForBuilder()) { + for (ClassFieldDto fieldDto : classDef.getClassFields()) { appendField(source, fieldDto); } - logger.debugEndOperation("Fields added: %d fields", builderDef.getAllFieldsForBuilder().size()); + logger.debugEndOperation("Fields added: %d fields", classDef.getClassFields().size()); } - private void appendField(JavaClassSource source, FieldDto fieldDto) { - String boxedFieldType = mapBoxedType(fieldDto.getFieldType()); + private void appendField(JavaClassSource source, ClassFieldDto fieldDto) { FieldSource field = source.addField(); - field.setName(fieldDto.getFieldNameInBuilder()); - field.setType(TrackedValue.class.getSimpleName() + "<" + boxedFieldType + ">"); - field.setPrivate(); - field.setLiteralInitializer("unsetValue()"); - applyJavaDocToField(field.getJavaDoc(), fieldDto); - } - - private void applyJavaDocToField(JavaDocSource javaDoc, FieldDto fieldDto) { - JavadocDto trackedValueJavadoc = fieldDto.getJavaDoc(); - if (trackedValueJavadoc != null) { - javaDoc.setText(trackedValueJavadoc.getDescription()); + field.setName(fieldDto.getFieldName()); + field.setType(mapType(fieldDto.getFieldType())); + if (fieldDto.getVisibility() != null) { + applyVisibility(field, fieldDto.getVisibility()); + } + if (fieldDto.getLiteralInitializer() != null) { + field.setLiteralInitializer(fieldDto.getLiteralInitializer()); } + applyJavadoc(field, fieldDto.getJavadoc()); } - private void appendConstructors(JavaClassSource source, BuilderDefinitionDto builderDef) { - Modifier constructorAccessModifier = - JavaLangMapper.mapAccessModifier( - builderDef.getConfiguration().getBuilderConstructorAccess()); - TypeName dtoBaseClass = builderDef.getBuildingTargetTypeName(); + private void appendConstructors(JavaClassSource source, GenerationTargetClassDto classDef) { + logger.debugStartOperation("Generating %d constructors", classDef.getConstructors().size()); - appendEmptyConstructor(source, dtoBaseClass, constructorAccessModifier); - appendConstructorWithInstance( - source, dtoBaseClass, builderDef.getAllFieldsForBuilder(), constructorAccessModifier); - logger.debug("Constructors added"); - } + for (ConstructorDto constructor : classDef.getConstructors()) { + appendConstructor(source, constructor); + } - private void appendEmptyConstructor( - JavaClassSource source, TypeName dtoClass, Modifier accessModifier) { - MethodSource constructor = source.addMethod(); - constructor.setConstructor(true); - applyVisibility(constructor, accessModifier); - constructor.setBody(""); - applyJavadoc( - constructor, - new JavadocDto( - "Empty constructor of builder for {@code %s}.", dtoClass.getFullQualifiedName())); + logger.debugEndOperation("Constructors added: %d", classDef.getConstructors().size()); } - private void appendConstructorWithInstance( - JavaClassSource source, - TypeName dtoBaseClass, - List fields, - Modifier accessModifier) { - MethodSource constructor = source.addMethod(); - constructor.setConstructor(true); - applyVisibility(constructor, accessModifier); - constructor.addParameter(mapType(dtoBaseClass), "instance"); - constructor.setBody(buildConstructorBody(fields)); - applyJavadoc( - constructor, - new JavadocDto( - "Initialisation of builder for {@code %s} by a instance.", - dtoBaseClass.getFullQualifiedName()) - .addParam("instance", "object instance for initialisiation")); - } - - private String buildConstructorBody(List fields) { - StringBuilder body = new StringBuilder(); - for (FieldDto field : fields) { - field - .getGetterName() - .ifPresent(getter -> addFieldInitializationWithValidation(body, field, getter)); - } - return body.toString(); - } - - private void addFieldInitializationWithValidation( - StringBuilder body, FieldDto field, String getter) { - String fieldInBuilder = field.getFieldNameInBuilder(); - - String initialisationCode = - """ - this.%s = initialValue(instance.%s()); - """ - .formatted(fieldInBuilder, getter); - - String validationCode = ""; - if (field.isNonNullable()) { - validationCode = - """ - - if (this.%s.value() == null) { - throw new IllegalArgumentException("Cannot initialize builder from instance: field '%s' is marked as non-null but source object has null value"); - } - """ - .formatted(fieldInBuilder, fieldInBuilder); + private void appendConstructor(JavaClassSource source, ConstructorDto constructor) { + MethodSource method = source.addMethod(); + method.setConstructor(true); + if (constructor.getVisibility() != null) { + applyVisibility(method, constructor.getVisibility()); + } + for (MethodParameterDto param : constructor.getParameters()) { + method.addParameter(mapType(param.getParameterType()), param.getParameterName()); + } + applyJavadoc(method, constructor.getJavadoc()); + // Use getMethodCodeDto for proper template resolution + // Note: empty string is a valid body (for empty constructors) + if (constructor.getMethodCodeDto() != null + && constructor.getMethodCodeDto().getCodeFormat() != null) { + method.setBody(resolveCodeTemplate(constructor.getMethodCodeDto())); } - - body.append(initialisationCode).append(validationCode); } - private void appendMethods(JavaClassSource source, BuilderDefinitionDto builderDef) { - Map allMethods = collectAllMethods(builderDef); - logger.debugStartOperation("Adding Methods for %d candidates", allMethods.size()); + private void appendMethods(JavaClassSource source, GenerationTargetClassDto classDef) { + logger.debugStartOperation("Generating %d methods", classDef.getMethods().size()); - List resolvedMethods = resolveMethodConflicts(allMethods); - logger.debug("Resolved %d methods after conflict resolution", resolvedMethods.size()); - - int generatedCnt = 0; - for (MethodDto methodDto : resolvedMethods) { + for (MethodDto methodDto : classDef.getMethods()) { appendMethod(source, methodDto, false, false); - generatedCnt++; } - logger.debugEndOperation("%d Methods added", generatedCnt); + + logger.debugEndOperation("Methods added: %d", classDef.getMethods().size()); } private Map collectAllMethods(BuilderDefinitionDto builderDef) { @@ -399,12 +346,12 @@ private void appendMethod( method.setBody(body); } - private void appendNestedTypes(JavaClassSource source, BuilderDefinitionDto builderDef) { - if (CollectionUtils.isEmpty(builderDef.getNestedTypes())) { + private void appendNestedTypes(JavaClassSource source, GenerationTargetClassDto classDef) { + if (CollectionUtils.isEmpty(classDef.getNestedTypes())) { return; } - logger.debugStartOperation("Generating %d nested type(s)", builderDef.getNestedTypes().size()); - for (NestedTypeDto nestedType : builderDef.getNestedTypes()) { + logger.debugStartOperation("Generating %d nested type(s)", classDef.getNestedTypes().size()); + for (NestedTypeDto nestedType : classDef.getNestedTypes()) { appendNestedType(source, nestedType); logger.debug("Generated nested type: %s", nestedType.getTypeName()); } @@ -591,35 +538,65 @@ private void addTrackedValueStaticImports(JavaClassSource source) { source.addImport(TrackedValue.class.getName() + ".unsetValue").setStatic(true); } - private Set collectImports(BuilderDefinitionDto builderDef) { + private Set collectImports(GenerationTargetClassDto classDef) { Set imports = new LinkedHashSet<>(); - String currentPackage = builderDef.getBuilderTypeName().getPackageName(); - - addImportIfNeeded(imports, currentPackage, TrackedValue.class.getName()); - addImportIfNeeded(imports, currentPackage, Consumer.class.getName()); - addImportIfNeeded(imports, currentPackage, Supplier.class.getName()); - addTypeImports(imports, currentPackage, builderDef.getBuildingTargetTypeName()); - addTypeImports(imports, currentPackage, builderDef.getBuilderTypeName()); - builderDef + String currentPackage = classDef.getTypeName().getPackageName(); + + // Add superclass imports + if (classDef.getSuperType() != null) { + addTypeImports(imports, currentPackage, classDef.getSuperType()); + } + + // Add class type and generics + addTypeImports(imports, currentPackage, classDef.getTypeName()); + classDef .getGenerics() .forEach( generic -> generic .getUpperBounds() .forEach(type -> addTypeImports(imports, currentPackage, type))); - builderDef + + // Add interface imports + classDef .getInterfaces() .forEach(interfaceName -> addInterfaceImports(imports, currentPackage, interfaceName)); - builderDef + + // Add class annotation imports + classDef .getClassAnnotations() .forEach(annotation -> addAnnotationImports(imports, currentPackage, annotation)); - builderDef - .getAllFieldsForBuilder() - .forEach(field -> addFieldImports(imports, currentPackage, field)); - builderDef - .getCoreMethods() - .forEach(method -> addMethodImports(imports, currentPackage, method)); - builderDef + + // Add field imports from ClassFieldDto + classDef + .getClassFields() + .forEach( + field -> { + addTypeImports(imports, currentPackage, field.getFieldType()); + field + .getFieldTypeImports() + .forEach(typeName -> addTypeImports(imports, currentPackage, typeName)); + }); + + // Add method imports + classDef.getMethods().forEach(method -> addMethodImports(imports, currentPackage, method)); + + // Add constructor imports + classDef + .getConstructors() + .forEach( + constructor -> { + constructor + .getParameters() + .forEach( + param -> addTypeImports(imports, currentPackage, param.getParameterType())); + constructor + .getCodeBlockImports() + .forEach(typeName -> addTypeImports(imports, currentPackage, typeName)); + }); + + // Add nested type imports + classDef .getNestedTypes() .forEach( nestedType -> @@ -783,14 +760,13 @@ private Properties loadFormatterProperties() { } } - private void writeBuilderClassToFile(String sourceCode, BuilderDefinitionDto builderDef) + private void writeClassToFile(String sourceCode, GenerationTargetClassDto classDef) throws BuilderException { logger.debug( - "Writing builder class to file: %s.%s", - builderDef.getBuilderTypeName().getPackageName(), - builderDef.getBuilderTypeName().getClassName()); + "Writing class to file: %s.%s", + classDef.getTypeName().getPackageName(), classDef.getTypeName().getClassName()); - String qualifiedName = builderDef.getBuilderTypeName().getFullQualifiedName(); + String qualifiedName = classDef.getTypeName().getFullQualifiedName(); if (builderClassAlreadyExists(qualifiedName)) { throw new BuilderException( null, diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ConstructorEnhancer.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ConstructorEnhancer.java new file mode 100644 index 00000000..89a7040f --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ConstructorEnhancer.java @@ -0,0 +1,158 @@ +/* + * MIT License + * + * Copyright (c) 2026 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.processor.generators.builder; + +import java.util.concurrent.atomic.AtomicBoolean; +import org.javahelpers.simple.builders.processor.analysis.JavaLangMapper; +import org.javahelpers.simple.builders.processor.generators.BuilderEnhancer; +import org.javahelpers.simple.builders.processor.model.core.BuilderDefinitionDto; +import org.javahelpers.simple.builders.processor.model.core.FieldDto; +import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; +import org.javahelpers.simple.builders.processor.model.method.ConstructorDto; +import org.javahelpers.simple.builders.processor.model.method.MethodParameterDto; +import org.javahelpers.simple.builders.processor.model.type.TypeName; +import org.javahelpers.simple.builders.processor.model.type.TypeNamePrimitive; +import org.javahelpers.simple.builders.processor.processing.ProcessingContext; + +/** + * Enhancer that adds builder constructors. + * + *

This enhancer generates the two builder constructors: + * + *

    + *
  • Empty constructor - for creating a new builder from scratch + *
  • From-instance constructor - for creating a builder initialized from an existing DTO + * instance + *
+ * + *

Priority: 95 (runs after CoreMethodsEnhancer at 100) + */ +public class ConstructorEnhancer implements BuilderEnhancer { + + private static final int PRIORITY = 95; + + @Override + public int getPriority() { + return PRIORITY; + } + + @Override + public boolean appliesTo( + BuilderDefinitionDto builderDto, TypeName dtoType, ProcessingContext context) { + return true; // Constructors are always needed + } + + @Override + public void enhanceBuilder(BuilderDefinitionDto builderDto, ProcessingContext context) { + // Add empty constructor + ConstructorDto emptyConstructor = createEmptyConstructor(builderDto); + builderDto.getConstructors().add(emptyConstructor); + + // Add from-instance constructor + ConstructorDto fromInstanceConstructor = createFromInstanceConstructor(builderDto); + builderDto.getConstructors().add(fromInstanceConstructor); + } + + /** Creates the empty constructor. */ + protected ConstructorDto createEmptyConstructor(BuilderDefinitionDto builderDto) { + ConstructorDto constructor = new ConstructorDto(); + constructor.setVisibility( + JavaLangMapper.mapAccessModifier( + builderDto.getConfiguration().getBuilderConstructorAccess())); + constructor.setBody(""); // Empty body + + String targetFullName = builderDto.getBuildingTargetTypeName().getFullQualifiedName(); + constructor.setJavadoc( + new JavadocDto("Empty constructor of builder for {@code %s}.", targetFullName)); + + return constructor; + } + + /** Creates the from-instance constructor. */ + protected ConstructorDto createFromInstanceConstructor(BuilderDefinitionDto builderDto) { + ConstructorDto constructor = new ConstructorDto(); + constructor.setVisibility( + JavaLangMapper.mapAccessModifier( + builderDto.getConfiguration().getBuilderConstructorAccess())); + + // Add parameter for the DTO instance + MethodParameterDto instanceParam = new MethodParameterDto(); + instanceParam.setParameterName("instance"); + instanceParam.setParameterTypeName(builderDto.getBuildingTargetTypeName()); + constructor.addParameter(instanceParam); + + // Build constructor body using text blocks + // The TrackedValue.* prefixes will be replaced by RoasterCodeGenerator + StringBuilder bodyCode = new StringBuilder(); + AtomicBoolean hasNonNullChecks = new AtomicBoolean(false); + + // Initialize fields from instance + for (FieldDto field : builderDto.getAllFieldsForBuilder()) { + String fieldName = field.getFieldNameInBuilder(); + + // getGetterName() returns Optional + field + .getGetterName() + .ifPresent( + getterName -> { + // Field has a getter - use it + if (field.isNonNullable() && !(field.getFieldType() instanceof TypeNamePrimitive)) { + // Non-nullable non-primitive field - validate not null + // Skip primitives as they can't be compared to null (compilation error) + bodyCode.append( + """ + if (instance.%s() == null) { + throw new IllegalArgumentException("Field '%s' is non-null but instance.%s() returned null"); + } + """ + .formatted(getterName, fieldName, getterName)); + hasNonNullChecks.set(true); + } + bodyCode.append( + """ + this.%s = TrackedValue.initialValue(instance.%s()); + """ + .formatted(fieldName, getterName)); + }); + // If no getter available - cannot initialize this field + // Leave it unset (will use unsetValue() from field initializer) + } + + // Set the body as string (will be processed by RoasterCodeGenerator) + constructor.setBody(bodyCode.toString()); + + // Add IllegalArgumentException import only if we have non-null checks + if (hasNonNullChecks.get()) { + constructor.addCodeBlockImport(new TypeName("java.lang", "IllegalArgumentException")); + } + + String targetFullName = builderDto.getBuildingTargetTypeName().getFullQualifiedName(); + constructor.setJavadoc( + new JavadocDto("Initialisation of builder for {@code %s} by a instance.", targetFullName) + .addParam("instance", "object instance for initialisiation")); + + return constructor; + } +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java index c7aeef7b..ff05c68f 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java @@ -36,6 +36,7 @@ import org.javahelpers.simple.builders.processor.model.method.MethodDto; import org.javahelpers.simple.builders.processor.model.type.GenericParameterDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; +import org.javahelpers.simple.builders.processor.model.type.TypeNamePrimitive; import org.javahelpers.simple.builders.processor.processing.ProcessingContext; /** @@ -154,7 +155,9 @@ protected MethodDto createBuildMethod(BuilderDefinitionDto builderDto) { // Add validation for non-nullable setter fields for (var field : builderDto.getSetterFieldsForBuilder()) { - if (field.isNonNullable()) { + if (field.isNonNullable() && !(field.getFieldType() instanceof TypeNamePrimitive)) { + // Non-nullable non-primitive field - validate not null + // Skip primitives as they can't be null (but they could be null if boxed) code.append("if (this.") .append(field.getFieldNameInBuilder()) .append(".isSet() && this.") @@ -194,6 +197,9 @@ protected MethodDto createBuildMethod(BuilderDefinitionDto builderDto) { method.addArgument("dtoBaseType", builderDto.getBuildingTargetTypeName()); method.addArgument("buildResultType", returnType); + // Add imports for types used in code block + method.addCodeBlockImport(new TypeName("java.lang", "IllegalStateException")); + method.setJavadoc(new JavadocDto("Builds the configured DTO instance.")); return method; @@ -250,11 +256,17 @@ protected MethodDto createToStringMethod(BuilderDefinitionDto builderDto) { + "\n .toString();"); // Add template arguments for code generation - method.addArgument( - "toStringBuilder", new TypeName("org.apache.commons.lang3.builder", "ToStringBuilder")); - method.addArgument( - "toStringStyle", - new TypeName("org.javahelpers.simple.builders.core.util", "BuilderToStringStyle")); + TypeName toStringBuilderType = + new TypeName("org.apache.commons.lang3.builder", "ToStringBuilder"); + TypeName toStringStyleType = + new TypeName("org.javahelpers.simple.builders.core.util", "BuilderToStringStyle"); + + method.addArgument("toStringBuilder", toStringBuilderType); + method.addArgument("toStringStyle", toStringStyleType); + + // Add imports for types used in code block + method.addCodeBlockImport(toStringBuilderType); + method.addCodeBlockImport(toStringStyleType); method.setJavadoc( new JavadocDto( diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/AddToCollectionGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/AddToCollectionGenerator.java index 3d91e0d9..ba859da4 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/AddToCollectionGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/AddToCollectionGenerator.java @@ -172,11 +172,17 @@ private MethodDto createAddToCollectionMethod( this.$fieldName:N = $builderFieldWrapper:T.changedValue(newCollection); return this; """); + TypeName collectionImplType = new TypeName("java.util", collectionImpl); + methodDto.addArgument("collectionVarType", collectionVarType); - methodDto.addArgument("collectionImpl", new TypeName("java.util", collectionImpl)); + methodDto.addArgument("collectionImpl", collectionImplType); methodDto.addArgument("fieldName", fieldNameInBuilder); methodDto.addArgument("elementType", elementType); methodDto.addArgument("builderFieldWrapper", TRACKED_VALUE_TYPE); + + // Add imports for types used in code block + methodDto.addCodeBlockImport(collectionImplType); + methodDto.setPriority(MethodDto.PRIORITY_MEDIUM); methodDto.setJavadoc( diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ArrayBuilderConsumerGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ArrayBuilderConsumerGenerator.java index 02f59310..5339720a 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ArrayBuilderConsumerGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ArrayBuilderConsumerGenerator.java @@ -140,6 +140,10 @@ private MethodDto createFieldConsumerWithArrayBuilder( this.$fieldName:N = $builderFieldWrapper:T.changedValue(builder.build().toArray(new $elementType:T[0])); return this; """); + + // Add code block import for java.util.List.of + methodDto.getMethodCodeDto().addCodeBlockImport(new TypeName("java.util", "List")); + methodDto.addArgument("fieldName", fieldNameInBuilder); methodDto.addArgument("dtoMethodParam", parameter.getParameterName()); methodDto.addArgument("helperType", builderTypeGeneric); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/OptionalHelperGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/OptionalHelperGenerator.java index 499cd4df..9d8aa3a3 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/OptionalHelperGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/OptionalHelperGenerator.java @@ -105,6 +105,9 @@ public List generateMethods( MethodGeneratorUtil.createBuilderMethodForFieldWithTransform( field, "Optional.ofNullable(%s)", innerType, builderType, context); + // Add code block import for Optional.ofNullable + method.getMethodCodeDto().addCodeBlockImport(new TypeName("java.util", "Optional")); + return Collections.singletonList(method); } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringBuilderConsumerGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringBuilderConsumerGenerator.java index 7735031f..c27e3bf4 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringBuilderConsumerGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringBuilderConsumerGenerator.java @@ -118,6 +118,12 @@ public List generateMethods( transform, builderType, context); + + // Add code block import for Optional.of when used + if (isOptionalString(field.getFieldType())) { + method.getMethodCodeDto().addCodeBlockImport(new TypeName("java.util", "Optional")); + } + return List.of(method); } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringFormatHelperGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringFormatHelperGenerator.java index b6182c0b..eabc4995 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringFormatHelperGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringFormatHelperGenerator.java @@ -139,6 +139,10 @@ public List generateMethods( field.getParameterAnnotations(), builderType, context); + + // Add code block imports for Optional.of and String.format + method.getMethodCodeDto().addCodeBlockImport(new TypeName("java.util", "Optional")); + methods.add(method); } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/VarArgsHelperGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/VarArgsHelperGenerator.java index 0c82b18f..1fa96c3f 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/VarArgsHelperGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/VarArgsHelperGenerator.java @@ -151,7 +151,19 @@ private MethodDto createFieldSetterByVarArgs( } String transform = MethodGeneratorUtil.wrapConcreteCollectionType(fieldType, baseExpression); - return MethodGeneratorUtil.createBuilderMethodForFieldWithTransform( - field, transform, parameterType, builderType, context); + MethodDto method = + MethodGeneratorUtil.createBuilderMethodForFieldWithTransform( + field, transform, parameterType, builderType, context); + + // Add code block imports for collection factory methods + if (fieldType instanceof TypeNameList) { + method.getMethodCodeDto().addCodeBlockImport(new TypeName("java.util", "List")); + } else if (fieldType instanceof TypeNameSet) { + method.getMethodCodeDto().addCodeBlockImport(new TypeName("java.util", "Set")); + } else if (fieldType instanceof TypeNameMap) { + method.getMethodCodeDto().addCodeBlockImport(new TypeName("java.util", "Map")); + } + + return method; } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderDefinitionDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderDefinitionDto.java new file mode 100644 index 00000000..c9767856 --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderDefinitionDto.java @@ -0,0 +1,202 @@ +/* + * MIT License + * + * Copyright (c) 2026 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.processor.model.core; + +import java.util.LinkedList; +import java.util.List; +import org.javahelpers.simple.builders.processor.model.method.MethodDto; +import org.javahelpers.simple.builders.processor.model.type.TypeName; + +/** + * Builder-specific extension of GenerationTargetClassDto. + * + *

This class holds all information for generating a builder, including builder-specific fields + * and methods that are not part of the generic class generation. + */ +public class BuilderDefinitionDto extends GenerationTargetClassDto { + /** Target type of result of building by builder. Containing package and name of DTO. */ + private TypeName buildingTargetTypeName; + + /** + * Fields in constructor. Containing all fields which need to be given for calling the + * constructor. Ordering in LinkedList is matching ordering of parameters in constructor call. + * Instances of fields are added in LinkedList of all fields too. + */ + private final List fieldsInConstructor = new LinkedList<>(); + + /** + * List of all fields of target DTO which are supported by builder. A field could be supported by + * multiple functions in builder. + */ + private final List fields = new LinkedList<>(); + + /** + * Core builder methods (build, create, conditional, toString, etc.) to be generated. These are + * added by BuilderEnhancers and have ordering for generation sequence. + */ + private final List coreMethods = new LinkedList<>(); + + /** Configuration for builder generation. */ + private BuilderConfiguration configuration; + + /** + * Getting type of builder. Delegates to base class typeName. + * + * @return package and name of builder. + */ + public TypeName getBuilderTypeName() { + return getTypeName(); + } + + /** + * Setting type of builder. Delegates to base class typeName. + * + * @param builderClassName type of builder + */ + public void setBuilderTypeName(TypeName builderClassName) { + setTypeName(builderClassName); + } + + /** + * Getting target type of result of building by builder. + * + * @return package and name of type of building result. + */ + public TypeName getBuildingTargetTypeName() { + return buildingTargetTypeName; + } + + /** + * Setting target type of result of building by builder. + * + * @param buildingTargetTypeName package and name of type of building result + */ + public void setBuildingTargetTypeName(TypeName buildingTargetTypeName) { + this.buildingTargetTypeName = buildingTargetTypeName; + } + + /** + * Adding a field-definition to builder definition. + * + * @param field to be added + */ + public void addField(FieldDto field) { + fields.add(field); + } + + /** + * Adds a field definition that will be provided via the DTO constructor. + * + * @param field the field to add to the constructor parameters + */ + public void addFieldInConstructor(FieldDto field) { + fieldsInConstructor.add(field); + } + + /** + * Adds multiple field definitions to be provided via the DTO constructor. + * + * @param fields the list of fields to add to the constructor parameters + */ + public void addAllFieldsInConstructor(List fields) { + fieldsInConstructor.addAll(fields); + } + + /** + * Adds multiple field definitions to the builder. + * + * @param fields the list of fields to add + */ + public void addAllFields(List fields) { + this.fields.addAll(fields); + } + + /** + * Getting all defined fields in builder definition. + * + * @return list of fields + */ + public List getSetterFieldsForBuilder() { + return fields; + } + + /** + * Returns the ordered list of fields to be passed to the DTO constructor. + * + * @return the ordered list of constructor parameter fields + */ + public List getConstructorFieldsForBuilder() { + return fieldsInConstructor; + } + + /** + * Returns all fields known to the builder in a single list. Constructor parameters appear first + * (in constructor order), followed by setter-derived fields. + * + * @return all fields in the builder (constructor fields first, then setter fields) + */ + public List getAllFieldsForBuilder() { + LinkedList all = new LinkedList<>(); + all.addAll(fieldsInConstructor); + all.addAll(fields); + return all; + } + + /** + * Returns the list of core builder methods. + * + * @return the list of core methods + */ + public List getCoreMethods() { + return coreMethods; + } + + /** + * Adds a core method to be generated in the builder. + * + * @param method the core method to add + */ + public void addCoreMethod(MethodDto method) { + this.coreMethods.add(method); + } + + /** + * Returns the configuration for builder generation. + * + * @return the builder configuration + */ + public BuilderConfiguration getConfiguration() { + return configuration; + } + + /** + * Sets the builder configuration. + * + * @param configuration the builder configuration + */ + public void setConfiguration(BuilderConfiguration configuration) { + this.configuration = configuration; + } +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/ClassFieldDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/ClassFieldDto.java new file mode 100644 index 00000000..03b8f440 --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/ClassFieldDto.java @@ -0,0 +1,105 @@ +/* + * MIT License + * + * Copyright (c) 2026 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.processor.model.core; + +import java.util.LinkedHashSet; +import java.util.Set; +import javax.lang.model.element.Modifier; +import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; +import org.javahelpers.simple.builders.processor.model.type.TypeName; + +/** + * Represents a field declaration in the generated class. + * + *

This DTO contains only rendering information for a field. It is created from {@code FieldDto} + * during finalization in {@code BuilderDefinitionCreator}. + */ +public class ClassFieldDto { + /** Field name in generated class. */ + private String fieldName; + + /** Field type (e.g., TrackedValue represented as TypeName). */ + private TypeName fieldType; + + /** Field visibility (typically PRIVATE). */ + private Modifier visibility; + + /** Literal initializer (e.g., "unsetValue()"). */ + private String literalInitializer; + + /** Field javadoc. */ + private JavadocDto javadoc; + + /** Types to import for this field (e.g., TrackedValue, the boxed inner type). */ + private final Set fieldTypeImports = new LinkedHashSet<>(); + + public String getFieldName() { + return fieldName; + } + + public void setFieldName(String fieldName) { + this.fieldName = fieldName; + } + + public TypeName getFieldType() { + return fieldType; + } + + public void setFieldType(TypeName fieldType) { + this.fieldType = fieldType; + } + + public Modifier getVisibility() { + return visibility; + } + + public void setVisibility(Modifier visibility) { + this.visibility = visibility; + } + + public String getLiteralInitializer() { + return literalInitializer; + } + + public void setLiteralInitializer(String literalInitializer) { + this.literalInitializer = literalInitializer; + } + + public JavadocDto getJavadoc() { + return javadoc; + } + + public void setJavadoc(JavadocDto javadoc) { + this.javadoc = javadoc; + } + + public Set getFieldTypeImports() { + return fieldTypeImports; + } + + public void addFieldTypeImport(TypeName typeName) { + this.fieldTypeImports.add(typeName); + } +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/GenerationTargetClassDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/GenerationTargetClassDto.java index e19df4da..deafb828 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/GenerationTargetClassDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/GenerationTargetClassDto.java @@ -28,37 +28,46 @@ import java.util.LinkedList; import java.util.List; import java.util.Set; +import javax.lang.model.element.Modifier; import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto; import org.javahelpers.simple.builders.processor.model.annotation.InterfaceName; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; +import org.javahelpers.simple.builders.processor.model.method.ConstructorDto; import org.javahelpers.simple.builders.processor.model.method.MethodDto; import org.javahelpers.simple.builders.processor.model.type.GenericParameterDto; import org.javahelpers.simple.builders.processor.model.type.NestedTypeDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; -/** BuilderDefinitionDto holds all information for generating a builder. */ -public class BuilderDefinitionDto { - /** Target type of result of building by builder. Containing package and name of DTO. */ - private TypeName buildingTargetTypeName; +/** + * Generic representation of a class to generate. + * + *

This base class contains all information needed by the code generator to render a class, + * independent of whether it's a builder, Jackson module, or other generated class type. + */ +public class GenerationTargetClassDto { + /** Generated class FQN. */ + private TypeName typeName; - /** Type of builder. Containing package and name of DTO. */ - private TypeName builderTypeName; + /** Class visibility. */ + private Modifier classAccessModifier; - /** - * Fields in constructor. Containing all fields which need to be given for calling the - * constructor. Ordering in LinkedList is matching orderung of parameters in constructor call. - * Instances of fields are added in LinkedList of all fields too. - */ - private final List fieldsInConstructor = new LinkedList<>(); + /** Superclass (e.g., SimpleModule), null if none. */ + private TypeName superType; + + /** Field declarations. */ + private final List classFields = new LinkedList<>(); + + /** Constructor definitions. */ + private final List constructors = new LinkedList<>(); + + /** All methods (unresolved — conflict resolution in generator). */ + private final List methods = new LinkedList<>(); /** Generic parameters declared on the target DTO (e.g., {@code }). */ private final List generics = new LinkedList<>(); - /** - * List of all fields of target DTO which are supported by builder. A field could be supported by - * multiple functions in builder. - */ - private final List fields = new LinkedList<>(); + /** Static import FQNs (e.g., TrackedValue.changedValue). */ + private final Set staticImports = new LinkedHashSet<>(); /** * Nested types (interfaces or classes) to be generated inside the builder, such as the "With" @@ -66,12 +75,6 @@ public class BuilderDefinitionDto { */ private final List nestedTypes = new LinkedList<>(); - /** - * Core builder methods (build, create, conditional, toString, etc.) to be generated. These are - * added by BuilderEnhancers and have ordering for generation sequence. - */ - private final List coreMethods = new LinkedList<>(); - /** * Class-level annotations to be added to the generated builder class. These are added by * BuilderEnhancers and include annotations like @Generated, @BuilderImplementation, etc. @@ -91,112 +94,60 @@ public class BuilderDefinitionDto { /** Class-level JavaDoc for the generated builder class. */ private JavadocDto classJavadoc; - /** Configuration for builder generation. */ - private BuilderConfiguration configuration; + public TypeName getTypeName() { + return typeName; + } - /** - * Getting type of builder. - * - * @return {@code org.javahelpers.simple.builders.internal.dtos.Typename} package and name of - * builder. - */ - public TypeName getBuilderTypeName() { - return builderTypeName; + public void setTypeName(TypeName typeName) { + this.typeName = typeName; } - /** - * Setting type of builder. - * - * @param builderClassName type of builder - */ - public void setBuilderTypeName(TypeName builderClassName) { - this.builderTypeName = builderClassName; + public Modifier getClassAccessModifier() { + return classAccessModifier; } - /** - * Getting target type of result of building by builder. - * - * @return {@code org.javahelpers.simple.builders.internal.dtos.Typename} package and name of type - * of building result. - */ - public TypeName getBuildingTargetTypeName() { - return buildingTargetTypeName; + public void setClassAccessModifier(Modifier classAccessModifier) { + this.classAccessModifier = classAccessModifier; } - /** - * Setting target type of result of building by builder. - * - * @param buildingTargetTypeName package and name of type of building result - */ - public void setBuildingTargetTypeName(TypeName buildingTargetTypeName) { - this.buildingTargetTypeName = buildingTargetTypeName; + public TypeName getSuperType() { + return superType; } - /** - * Adding a field-definition to builder defintion. - * - * @param field {@code org.javahelpers.simple.builders.internal.dtos.FieldDto} to be added - */ - public void addField(FieldDto field) { - fields.add(field); + public void setSuperType(TypeName superType) { + this.superType = superType; } - /** - * Adds a field definition that will be provided via the DTO constructor. - * - * @param field the field to add to the constructor parameters - */ - public void addFieldInConstructor(FieldDto field) { - fieldsInConstructor.add(field); + public List getClassFields() { + return classFields; } - /** - * Adds multiple field definitions to be provided via the DTO constructor. - * - * @param fields the list of fields to add to the constructor parameters - */ - public void addAllFieldsInConstructor(List fields) { - fieldsInConstructor.addAll(fields); + public void addClassField(ClassFieldDto classField) { + this.classFields.add(classField); } - /** - * Adds multiple field definitions to the builder. - * - * @param fields the list of fields to add - */ - public void addAllFields(List fields) { - this.fields.addAll(fields); + public List getConstructors() { + return constructors; } - /** - * Getting all definied fields in builder definition. - * - * @return list of fields with type {@code org.javahelpers.simple.builders.internal.dtos.FieldDto} - */ - public List getSetterFieldsForBuilder() { - return fields; + public void addConstructor(ConstructorDto constructor) { + this.constructors.add(constructor); } - /** - * Returns the ordered list of fields to be passed to the DTO constructor. - * - * @return the ordered list of constructor parameter fields - */ - public List getConstructorFieldsForBuilder() { - return fieldsInConstructor; + public List getMethods() { + return methods; } - /** - * Returns all fields known to the builder in a single list. Constructor parameters appear first - * (in constructor order), followed by setter-derived fields. - * - * @return all fields in the builder (constructor fields first, then setter fields) - */ - public List getAllFieldsForBuilder() { - LinkedList all = new LinkedList<>(); - all.addAll(fieldsInConstructor); - all.addAll(fields); - return all; + public void addMethod(MethodDto method) { + this.methods.add(method); + } + + public Set getStaticImports() { + return staticImports; + } + + public void addStaticImport(String staticImport) { + this.staticImports.add(staticImport); } /** @@ -235,24 +186,6 @@ public void addNestedType(NestedTypeDto nestedType) { this.nestedTypes.add(nestedType); } - /** - * Returns the list of core builder methods. - * - * @return the list of core methods - */ - public List getCoreMethods() { - return coreMethods; - } - - /** - * Adds a core method to be generated in the builder. - * - * @param method the core method to add - */ - public void addCoreMethod(MethodDto method) { - this.coreMethods.add(method); - } - /** * Returns the set of class-level annotations. * @@ -289,24 +222,6 @@ public void addInterface(InterfaceName interfaceType) { this.interfaces.add(interfaceType); } - /** - * Returns the configuration for builder generation. - * - * @return the builder configuration - */ - public BuilderConfiguration getConfiguration() { - return configuration; - } - - /** - * Sets the builder configuration. - * - * @param configuration the builder configuration - */ - public void setConfiguration(BuilderConfiguration configuration) { - this.configuration = configuration; - } - /** * Returns the class-level JavaDoc for the generated builder. * diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/ConstructorDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/ConstructorDto.java new file mode 100644 index 00000000..842986b9 --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/ConstructorDto.java @@ -0,0 +1,153 @@ +/* + * MIT License + * + * Copyright (c) 2026 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.processor.model.method; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import javax.lang.model.element.Modifier; +import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto; +import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; +import org.javahelpers.simple.builders.processor.model.type.TypeName; + +/** + * Dedicated constructor representation (separate from MethodDto). + * + *

This DTO contains all information needed to render a constructor in the generated class. + */ +public class ConstructorDto { + /** Constructor parameters. */ + private final LinkedList parameters = new LinkedList<>(); + + /** Constructor body code with type placeholders. */ + private MethodCodeDto methodCodeDto = new MethodCodeDto(); + + /** Constructor visibility. */ + private Modifier visibility; + + /** Constructor javadoc. */ + private JavadocDto javadoc; + + /** Constructor annotations. */ + private final List annotations = new ArrayList<>(); + + public List getParameters() { + return parameters; + } + + public void addParameter(MethodParameterDto parameter) { + this.parameters.add(parameter); + } + + /** + * Gets the constructor body as MethodCodeDto. + * + * @return the method code DTO containing body and type placeholders + */ + public MethodCodeDto getMethodCodeDto() { + return methodCodeDto; + } + + /** + * Sets the constructor body using MethodCodeDto. + * + * @param methodCodeDto the method code DTO + */ + public void setMethodCodeDto(MethodCodeDto methodCodeDto) { + this.methodCodeDto = methodCodeDto; + } + + /** + * Gets the constructor body as a plain string (for backward compatibility). + * + * @return the constructor body code + * @deprecated Use {@link #getMethodCodeDto()} instead for proper type handling + */ + @Deprecated + public String getBody() { + return methodCodeDto != null ? methodCodeDto.getCodeFormat() : null; + } + + /** + * Sets the constructor body as a plain string (for backward compatibility). + * + * @param body the constructor body code + * @deprecated Use {@link #setMethodCodeDto(MethodCodeDto)} instead for proper type handling + */ + @Deprecated + public void setBody(String body) { + if (this.methodCodeDto == null) { + this.methodCodeDto = new MethodCodeDto(); + } + this.methodCodeDto.setCodeFormat(body); + } + + public Modifier getVisibility() { + return visibility; + } + + public void setVisibility(Modifier visibility) { + this.visibility = visibility; + } + + public JavadocDto getJavadoc() { + return javadoc; + } + + public void setJavadoc(JavadocDto javadoc) { + this.javadoc = javadoc; + } + + public List getAnnotations() { + return annotations; + } + + public void addAnnotation(AnnotationDto annotation) { + this.annotations.add(annotation); + } + + /** + * Gets code block imports from the method code DTO. + * + * @return set of types used in code blocks + */ + public Set getCodeBlockImports() { + return methodCodeDto != null ? methodCodeDto.getCodeBlockImports() : new LinkedHashSet<>(); + } + + /** + * Adds a code block import to the method code DTO. + * + * @param typeName the type to import + */ + public void addCodeBlockImport(TypeName typeName) { + if (this.methodCodeDto == null) { + this.methodCodeDto = new MethodCodeDto(); + } + this.methodCodeDto.addCodeBlockImport(typeName); + } +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodCodeDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodCodeDto.java index ca29e2a5..3eda5f4b 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodCodeDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodCodeDto.java @@ -25,7 +25,9 @@ package org.javahelpers.simple.builders.processor.model.method; import java.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; import org.javahelpers.simple.builders.processor.model.type.TypeName; /** DTO for holding information of code implementation. */ @@ -36,6 +38,9 @@ public class MethodCodeDto { /** List of placeholders in CodeFormat. Containing dynamic values too. */ private final List> codeArguments = new ArrayList<>(); + /** Types used in the code body that aren't covered by arguments. */ + private final Set codeBlockImports = new LinkedHashSet<>(); + /** * Setting format of code. * @@ -83,4 +88,22 @@ public String getCodeFormat() { public List> getCodeArguments() { return codeArguments; } + + /** + * Getter for code block imports. + * + * @return types used in code body needing import + */ + public Set getCodeBlockImports() { + return codeBlockImports; + } + + /** + * Adding an import for a type used in the code block. + * + * @param typeName type to import + */ + public void addCodeBlockImport(TypeName typeName) { + this.codeBlockImports.add(typeName); + } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodDto.java index 4d8e5662..7233b94d 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodDto.java @@ -55,6 +55,9 @@ public class MethodDto { /** Ordering for method generation. Lower values appear first in generated class. */ private int ordering = 1000; // Default high value for field-generated methods + /** Origin description for logging (e.g., "field 'name'", "core method 'build'"). */ + private String sourceDescription; + /** Name of method. */ private String methodName; @@ -145,6 +148,24 @@ public int getOrdering() { return ordering; } + /** + * Gets the source description for logging. + * + * @return the source description (e.g., "field 'name'", "core method 'build'") + */ + public String getSourceDescription() { + return sourceDescription; + } + + /** + * Sets the source description for logging. + * + * @param sourceDescription the source description + */ + public void setSourceDescription(String sourceDescription) { + this.sourceDescription = sourceDescription; + } + /** * Setting the inner implementation of a method. Supports placeholders which has to be set by * addArgument. @@ -358,6 +379,16 @@ public void addAnnotation(AnnotationDto annotation) { this.annotations.add(annotation); } + /** + * Adds an import for a type used in the method's code block. This is a convenience method that + * delegates to the underlying MethodCodeDto. + * + * @param typeName the type to import + */ + public void addCodeBlockImport(TypeName typeName) { + this.methodCodeDto.addCodeBlockImport(typeName); + } + /** * Comparator for sorting MethodDto instances with sophisticated ordering rules. * diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/NestedTypeDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/NestedTypeDto.java index 3a82163c..61ebea84 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/NestedTypeDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/NestedTypeDto.java @@ -24,8 +24,10 @@ package org.javahelpers.simple.builders.processor.model.type; +import java.util.ArrayList; import java.util.LinkedList; import java.util.List; +import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; import org.javahelpers.simple.builders.processor.model.method.MethodDto; @@ -51,6 +53,9 @@ public class NestedTypeDto { /** Javadoc comment for this nested type. */ private JavadocDto javadoc; + /** Type-level annotations for this nested type. */ + private final List annotations = new ArrayList<>(); + public enum NestedTypeKind { INTERFACE, CLASS @@ -95,4 +100,12 @@ public JavadocDto getJavadoc() { public void setJavadoc(JavadocDto javadoc) { this.javadoc = javadoc; } + + public List getAnnotations() { + return annotations; + } + + public void addAnnotation(AnnotationDto annotation) { + this.annotations.add(annotation); + } } diff --git a/processor/src/main/resources/META-INF/services/org.javahelpers.simple.builders.processor.generators.Generator b/processor/src/main/resources/META-INF/services/org.javahelpers.simple.builders.processor.generators.Generator index e859fec3..b72cbd9f 100644 --- a/processor/src/main/resources/META-INF/services/org.javahelpers.simple.builders.processor.generators.Generator +++ b/processor/src/main/resources/META-INF/services/org.javahelpers.simple.builders.processor.generators.Generator @@ -25,5 +25,6 @@ org.javahelpers.simple.builders.processor.generators.builder.JacksonAnnotationEn org.javahelpers.simple.builders.processor.generators.builder.InterfaceEnhancer org.javahelpers.simple.builders.processor.generators.builder.ClassJavaDocEnhancer org.javahelpers.simple.builders.processor.generators.builder.CoreMethodsEnhancer +org.javahelpers.simple.builders.processor.generators.builder.ConstructorEnhancer org.javahelpers.simple.builders.processor.generators.builder.WithInterfaceEnhancer org.javahelpers.simple.builders.processor.generators.builder.ConditionalEnhancer From 1fd07e0af0e2a26f750734bb044579f95eeeb58d Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Mon, 6 Apr 2026 22:50:13 +0200 Subject: [PATCH 04/30] Fixing failing tests --- .../builders/example/BookDtoBuilder.java | 24 ++++ .../roaster/RoasterCodeGenerator.java | 87 ++++--------- .../builder/CoreMethodsEnhancer.java | 8 +- .../field/ArrayBuilderConsumerGenerator.java | 4 +- .../generators/util/MethodGeneratorUtil.java | 8 +- .../processing/BuilderDefinitionCreator.java | 118 ++++++++++++++++++ .../BuilderConfigurationReaderTest.java | 2 - .../processor/BuilderProcessorTest.java | 31 +++-- .../ComprehensiveFeatureIntegrationTest.java | 20 ++- 9 files changed, 206 insertions(+), 96 deletions(-) diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java index 1aef439d..0461b5df 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 @@ -394,6 +394,30 @@ BookDtoBuilder validateTitle() { * Builds the configured DTO instance. */ public BookDto build() { + if (this.available.isSet() && this.available.value() == null) { + throw new IllegalStateException("Field 'available' is marked as non-null but null value was provided"); + } + if (this.category.isSet() && this.category.value() == null) { + throw new IllegalStateException("Field 'category' is marked as non-null but null value was provided"); + } + if (this.discount.isSet() && this.discount.value() == null) { + throw new IllegalStateException("Field 'discount' is marked as non-null but null value was provided"); + } + if (this.edition.isSet() && this.edition.value() == null) { + throw new IllegalStateException("Field 'edition' is marked as non-null but null value was provided"); + } + if (this.pages.isSet() && this.pages.value() == null) { + throw new IllegalStateException("Field 'pages' is marked as non-null but null value was provided"); + } + if (this.price.isSet() && this.price.value() == null) { + throw new IllegalStateException("Field 'price' is marked as non-null but null value was provided"); + } + if (this.rating.isSet() && this.rating.value() == null) { + throw new IllegalStateException("Field 'rating' is marked as non-null but null value was provided"); + } + if (this.salesCount.isSet() && this.salesCount.value() == null) { + throw new IllegalStateException("Field 'salesCount' is marked as non-null but null value was provided"); + } BookDto result = new BookDto(); this.author.ifSet(result::setAuthor); this.available.ifSet(result::setAvailable); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java index b95d47b0..35079193 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java @@ -31,7 +31,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.Writer; -import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -47,9 +46,7 @@ import org.javahelpers.simple.builders.processor.exceptions.BuilderException; import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto; import org.javahelpers.simple.builders.processor.model.annotation.InterfaceName; -import org.javahelpers.simple.builders.processor.model.core.BuilderDefinitionDto; import org.javahelpers.simple.builders.processor.model.core.ClassFieldDto; -import org.javahelpers.simple.builders.processor.model.core.FieldDto; import org.javahelpers.simple.builders.processor.model.core.GenerationTargetClassDto; import org.javahelpers.simple.builders.processor.model.integration.JacksonModuleDefinitionDto; import org.javahelpers.simple.builders.processor.model.integration.JacksonModuleEntryDto; @@ -158,10 +155,10 @@ private JavaClassSource createJavaClassSource(GenerationTargetClassDto classDef) source.setName(classDef.getTypeName().getClassName()); addGenericDeclarations(source, classDef.getGenerics()); addTrackedValueStaticImports(source); - + // Collect and add imports early (before elements are added) collectImports(classDef).stream().sorted().forEach(source::addImport); - + logger.debug("JavaClassSource created"); return source; } @@ -171,9 +168,7 @@ private void addClassMetadata(JavaClassSource source, GenerationTargetClassDto c applyJavadoc(source, classDef.getClassJavadoc()); // Set class access level - if (classDef.getClassAccessModifier() != null) { - applyVisibility(source, classDef.getClassAccessModifier()); - } + applyVisibility(source, classDef.getClassAccessModifier()); // Add superclass if specified if (classDef.getSuperType() != null) { @@ -245,48 +240,28 @@ private void appendConstructor(JavaClassSource source, ConstructorDto constructo } private void appendMethods(JavaClassSource source, GenerationTargetClassDto classDef) { - logger.debugStartOperation("Generating %d methods", classDef.getMethods().size()); + logger.debugStartOperation("Generating %d method candidates", classDef.getMethods().size()); - for (MethodDto methodDto : classDef.getMethods()) { - appendMethod(source, methodDto, false, false); - } + // Resolve method conflicts by signature and priority + List resolvedMethods = resolveMethodConflicts(classDef.getMethods()); + logger.debug("Resolved to %d methods after conflict resolution", resolvedMethods.size()); - logger.debugEndOperation("Methods added: %d", classDef.getMethods().size()); - } - - private Map collectAllMethods(BuilderDefinitionDto builderDef) { - Map allMethods = new HashMap<>(); - - for (FieldDto fieldDto : builderDef.getConstructorFieldsForBuilder()) { - for (MethodDto method : fieldDto.getMethods()) { - allMethods.put(method, fieldDto); - } - } - for (FieldDto fieldDto : builderDef.getSetterFieldsForBuilder()) { - for (MethodDto method : fieldDto.getMethods()) { - allMethods.put(method, fieldDto); - } - } - for (MethodDto coreMethod : builderDef.getCoreMethods()) { - allMethods.put(coreMethod, null); + for (MethodDto methodDto : resolvedMethods) { + appendMethod(source, methodDto, false, false); } - return allMethods; + logger.debugEndOperation("Methods added: %d", resolvedMethods.size()); } - private List resolveMethodConflicts(Map methodToField) { + private List resolveMethodConflicts(List methods) { MethodDto.MethodComparator comparator = new MethodDto.MethodComparator(); - List> sortedEntries = - methodToField.entrySet().stream() - .sorted((e1, e2) -> comparator.compare(e1.getKey(), e2.getKey())) - .toList(); + // Sort methods by comparator for consistent ordering + List sortedMethods = methods.stream().sorted(comparator).toList(); Map signatureToMethod = new java.util.LinkedHashMap<>(); - for (Map.Entry entry : sortedEntries) { - MethodDto method = entry.getKey(); - FieldDto field = entry.getValue(); + for (MethodDto method : sortedMethods) { String signature = method.getSignatureKey(); MethodDto existing = signatureToMethod.get(signature); @@ -294,8 +269,13 @@ private List resolveMethodConflicts(Map methodTo signatureToMethod.put(signature, method); } else { String existingSource = - createSourceDescriptionForLogging(existing, methodToField.get(existing)); - String newSource = createSourceDescriptionForLogging(method, field); + existing.getSourceDescription() != null + ? existing.getSourceDescription() + : "unknown source"; + String newSource = + method.getSourceDescription() != null + ? method.getSourceDescription() + : "unknown source"; if (method.getPriority() > existing.getPriority()) { signatureToMethod.put(signature, method); @@ -314,21 +294,8 @@ private List resolveMethodConflicts(Map methodTo } } - return new java.util.ArrayList<>(signatureToMethod.values()); - } - - /** - * Creates a description string for method source identification. - * - * @param method method to describe - * @param field associated field (may be null) - * @return description string for logging/debugging - */ - public static String createSourceDescriptionForLogging(MethodDto method, FieldDto field) { - if (field == null) { - return "core method '" + method.getMethodName() + "'"; - } - return "field '" + field.getFieldNameInBuilder() + "'"; + // Sort the final result for reproducible output + return signatureToMethod.values().stream().sorted(comparator).toList(); } private void appendMethod( @@ -607,14 +574,6 @@ private Set collectImports(GenerationTargetClassDto classDef) { return imports; } - private void addFieldImports(Set imports, String currentPackage, FieldDto field) { - addTypeImports(imports, currentPackage, field.getFieldType()); - field - .getParameterAnnotations() - .forEach(annotation -> addAnnotationImports(imports, currentPackage, annotation)); - field.getMethods().forEach(method -> addMethodImports(imports, currentPackage, method)); - } - private void addMethodImports(Set imports, String currentPackage, MethodDto method) { if (method.getReturnType() != null) { addTypeImports(imports, currentPackage, method.getReturnType()); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java index ff05c68f..4052febf 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java @@ -36,7 +36,6 @@ import org.javahelpers.simple.builders.processor.model.method.MethodDto; import org.javahelpers.simple.builders.processor.model.type.GenericParameterDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; -import org.javahelpers.simple.builders.processor.model.type.TypeNamePrimitive; import org.javahelpers.simple.builders.processor.processing.ProcessingContext; /** @@ -154,10 +153,11 @@ protected MethodDto createBuildMethod(BuilderDefinitionDto builderDto) { } // Add validation for non-nullable setter fields + // Note: Primitives are stored as boxed types in TrackedValue, etc. + // They can be null via Supplier methods: builder.pages(() -> null) + // So we need null checks for ALL non-nullable fields, including primitives for (var field : builderDto.getSetterFieldsForBuilder()) { - if (field.isNonNullable() && !(field.getFieldType() instanceof TypeNamePrimitive)) { - // Non-nullable non-primitive field - validate not null - // Skip primitives as they can't be null (but they could be null if boxed) + if (field.isNonNullable()) { code.append("if (this.") .append(field.getFieldNameInBuilder()) .append(".isSet() && this.") diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ArrayBuilderConsumerGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ArrayBuilderConsumerGenerator.java index 5339720a..ffd83cfe 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ArrayBuilderConsumerGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ArrayBuilderConsumerGenerator.java @@ -135,7 +135,9 @@ private MethodDto createFieldConsumerWithArrayBuilder( setMethodAccessModifier(methodDto, getMethodAccessModifier(context)); methodDto.setCode( """ - $helperType:T builder = this.$fieldName:N.isSet() ? new $helperType:T(java.util.List.of(this.$fieldName:N.value())) : new $helperType:T(); + $helperType:T builder = this.$fieldName:N.isSet() + ? new $helperType:T(java.util.List.of(this.$fieldName:N.value())) + : new $helperType:T(); $dtoMethodParam:N.accept(builder); this.$fieldName:N = $builderFieldWrapper:T.changedValue(builder.build().toArray(new $elementType:T[0])); return this; diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java index e58984b8..0ea3f573 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java @@ -230,7 +230,9 @@ public static MethodDto createFieldConsumerWithBuilder( methodDto.setCode( """ - $helperType:T builder = this.$fieldName:N.isSet() ? new $helperType:T(%s) : new $helperType:T(%s); + $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($buildExpression:N); return this; @@ -363,7 +365,9 @@ public static MethodDto createSimpleFieldConsumer( methodDto.setCode( """ - $helperType:T consumer = this.$fieldName:N.isSet() ? this.$fieldName:N.value() : new $helperType:T(); + $helperType:T consumer = this.$fieldName:N.isSet() + ? this.$fieldName:N.value() + : new $helperType:T(); $dtoMethodParam:N.accept(consumer); this.$fieldName:N = $builderFieldWrapper:T.changedValue(consumer); return this; diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java index fb2e512d..e72d1e00 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java @@ -47,11 +47,14 @@ import org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil; import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto; import org.javahelpers.simple.builders.processor.model.core.BuilderDefinitionDto; +import org.javahelpers.simple.builders.processor.model.core.ClassFieldDto; import org.javahelpers.simple.builders.processor.model.core.FieldDto; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; import org.javahelpers.simple.builders.processor.model.method.MethodDto; import org.javahelpers.simple.builders.processor.model.method.MethodParameterDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; +import org.javahelpers.simple.builders.processor.model.type.TypeNameGeneric; +import org.javahelpers.simple.builders.processor.model.type.TypeNamePrimitive; /** Class for creating a specific BuilderDefinitionDto for an annotated DTO class. */ public class BuilderDefinitionCreator { @@ -92,6 +95,9 @@ public static BuilderDefinitionDto extractFromElement( // Apply builder enhancers (including With interface generation) context.getGeneratorRegistry().enhanceBuilder(result, result.getBuildingTargetTypeName()); + // Finalize the definition - convert to generic class representation + finalizeDefinition(result, context); + context.debug("Builder will be generated as: %s", result.getBuilderTypeName().getClassName()); context.debugEndOperation( @@ -100,6 +106,118 @@ public static BuilderDefinitionDto extractFromElement( return result; } + /** + * Finalizes the builder definition by converting builder-specific data into generic class + * generation data. + * + *

This step: + * + *

    + *
  • Converts FieldDto instances to ClassFieldDto instances + *
  • Collects all methods from fields and core methods + *
  • Sets source descriptions on methods for conflict resolution logging + *
  • Sets class access modifier + *
  • Sets static imports for TrackedValue + *
+ * + * @param builderDto the builder definition to finalize + * @param context the processing context + */ + private static void finalizeDefinition( + BuilderDefinitionDto builderDto, ProcessingContext context) { + context.debugStartOperation("Finalizing builder definition"); + + // 1. Convert FieldDto → ClassFieldDto + for (FieldDto field : builderDto.getAllFieldsForBuilder()) { + ClassFieldDto classField = convertToClassField(field); + builderDto.addClassField(classField); + } + + // 2. Collect methods from fields and core methods, set source descriptions + for (FieldDto field : builderDto.getConstructorFieldsForBuilder()) { + for (MethodDto method : field.getMethods()) { + method.setSourceDescription("field '" + field.getFieldNameInBuilder() + "'"); + builderDto.addMethod(method); + } + } + + for (FieldDto field : builderDto.getSetterFieldsForBuilder()) { + for (MethodDto method : field.getMethods()) { + method.setSourceDescription("field '" + field.getFieldNameInBuilder() + "'"); + builderDto.addMethod(method); + } + } + + for (MethodDto method : builderDto.getCoreMethods()) { + method.setSourceDescription("core method '" + method.getMethodName() + "'"); + builderDto.addMethod(method); + } + + // 3. Set class access modifier + builderDto.setClassAccessModifier( + JavaLangMapper.mapAccessModifier(builderDto.getConfiguration().getBuilderAccess())); + + // 4. Set static imports for TrackedValue + builderDto.addStaticImport( + "org.javahelpers.simple.builders.core.util.TrackedValue.changedValue"); + builderDto.addStaticImport( + "org.javahelpers.simple.builders.core.util.TrackedValue.initialValue"); + builderDto.addStaticImport("org.javahelpers.simple.builders.core.util.TrackedValue.unsetValue"); + + context.debugEndOperation( + "Finalized: %d class fields, %d methods, %d constructors", + builderDto.getClassFields().size(), + builderDto.getMethods().size(), + builderDto.getConstructors().size()); + } + + /** + * Converts a FieldDto to a ClassFieldDto for rendering. + * + * @param field the field DTO to convert + * @return the class field DTO + */ + private static ClassFieldDto convertToClassField(FieldDto field) { + ClassFieldDto classField = new ClassFieldDto(); + classField.setFieldName(field.getFieldNameInBuilder()); + + // Build the field type: TrackedValue + TypeName trackedValueType = + new TypeName("org.javahelpers.simple.builders.core.util", "TrackedValue"); + // Box primitive types (int -> Integer, etc.) + TypeName boxedType; + if (field.getFieldType() instanceof TypeNamePrimitive primitive) { + boxedType = + switch (primitive.getType()) { + case BOOLEAN -> new TypeName("java.lang", "Boolean"); + case BYTE -> new TypeName("java.lang", "Byte"); + case SHORT -> new TypeName("java.lang", "Short"); + case INT -> new TypeName("java.lang", "Integer"); + case LONG -> new TypeName("java.lang", "Long"); + case CHAR -> new TypeName("java.lang", "Character"); + case FLOAT -> new TypeName("java.lang", "Float"); + case DOUBLE -> new TypeName("java.lang", "Double"); + case VOID -> new TypeName("java.lang", "Void"); + }; + } else { + boxedType = field.getFieldType(); + } + TypeName wrappedFieldType = new TypeNameGeneric(trackedValueType, List.of(boxedType)); + classField.setFieldType(wrappedFieldType); + + classField.setVisibility(Modifier.PRIVATE); + classField.setLiteralInitializer("unsetValue()"); + classField.setJavadoc(field.getJavaDoc()); + + // Add field type imports + classField.addFieldTypeImport( + new TypeName("org.javahelpers.simple.builders.core.util", "TrackedValue")); + // Add the boxed field type for import (derived from original field type) + classField.addFieldTypeImport(boxedType); + + return classField; + } + /** Initializes the builder definition with package, class name, and generics. */ private static BuilderDefinitionDto initializeBuilderDefinition( TypeElement annotatedType, ProcessingContext context) { 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 9deb48e6..f88df074 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 @@ -463,8 +463,6 @@ public class PersonDto { import static org.javahelpers.simple.builders.core.util.TrackedValue.unsetValue; import java.util.List; - import java.util.function.Consumer; - import java.util.function.Supplier; import org.apache.commons.lang3.builder.ToStringBuilder; import org.javahelpers.simple.builders.core.interfaces.IBuilderBase; import org.javahelpers.simple.builders.core.util.BuilderToStringStyle; 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 a4c5951c..2cad03f8 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 @@ -92,8 +92,8 @@ void shouldLogDebugMessagesWhenVerboseModeEnabled() { "[DEBUG] Starting BuilderProcessor...", "[DEBUG] Loaded global configuration from compiler arguments: BuilderConfiguration[]", "[DEBUG] Initializing generator registry", - "[DEBUG] ├─ Loaded 14 method generators and 8 builder enhancers total", - "[DEBUG] └─ Initialized GeneratorRegistry with 14 method generators and 8 builder", + "[DEBUG] ├─ Loaded 14 method generators and 9 builder enhancers total", + "[DEBUG] └─ Initialized GeneratorRegistry with 14 method generators and 9 builder", "simple-builders: PROCESSING ROUND START", "[DEBUG] simple-builders: Processing round started. Found 1 annotated elements.", "[DEBUG] Processing element: VerboseTest", @@ -120,23 +120,27 @@ void shouldLogDebugMessagesWhenVerboseModeEnabled() { "[DEBUG] │ │ ├─ Applying: WithInterfaceEnhancer (priority: 95)", "[DEBUG] │ │ ├─ Applying: InterfaceEnhancer (priority: 90)", "[DEBUG] │ │ ├─ Applying: ConditionalEnhancer (priority: 80)", - "[DEBUG] │ │ └─ Applied 7 builder enhancers", + "[DEBUG] │ │ └─ Applied 8 builder enhancers", + "[DEBUG] │ ├─ Finalizing builder definition", + "[DEBUG] │ │ └─ Finalized: 1 class fields, 9 methods, 2 constructors", + "[DEBUG] │ ├─ Builder will be generated as: VerboseTestBuilder", "[DEBUG] │ └─ Builder definition extracted: VerboseTestBuilder", - "[DEBUG] ├─ Code generation for builder: VerboseTestBuilder", - "[DEBUG] │ ├─ Class builder created", + "[DEBUG] ├─ Code generation for class: VerboseTestBuilder", + "[DEBUG] │ ├─ JavaClassSource created", "[DEBUG] │ ├─ Class metadata added", - "[DEBUG] │ ├─ Generating 0 constructor fields and 1 setter fields", + "[DEBUG] │ ├─ Generating 1 fields", "[DEBUG] │ │ └─ Fields added: 1 fields", - "[DEBUG] │ ├─ Adding Methods for 9 candidates", - "[DEBUG] │ │ ├─ Resolved 9 methods after conflict resolution", - "[DEBUG] │ │ └─ 9 Methods added", - "[DEBUG] │ ├─ Constructors added", + "[DEBUG] │ ├─ Generating 2 constructors", + "[DEBUG] │ │ └─ Constructors added: 2", + "[DEBUG] │ ├─ Generating 9 method candidates", + "[DEBUG] │ │ ├─ Resolved to 9 methods after conflict resolution", + "[DEBUG] │ │ └─ Methods added: 9", "[DEBUG] │ ├─ Generating 1 nested type(s)", "[DEBUG] │ │ ├─ Generated nested type: With", "[DEBUG] │ │ └─ Nested types added", "[DEBUG] │ ├─ Class-level annotations added", - "[DEBUG] │ ├─ Writing builder class to file: test.VerboseTestBuilder", - "[DEBUG] │ └─ Successfully generated builder: VerboseTestBuilder", + "[DEBUG] │ ├─ Writing class to file: test.VerboseTestBuilder", + "[DEBUG] │ └─ Successfully generated class: VerboseTestBuilder", "[DEBUG] ├─ Jackson module entry added", "[DEBUG] └─ Generated builder with 1 fields and 5 methods for VerboseTestBuilder", "simple-builders: Successfully generated 1 builder(s) in this processing round", @@ -1209,6 +1213,9 @@ public NonNullValidationBuilder(NonNullValidation instance) { generatedCode, """ public NonNullValidation build() { + if (this.primitiveField.isSet() && this.primitiveField.value() == null) { + throw new IllegalStateException("Field 'primitiveField' is marked as non-null but null value was provided"); + } if (this.requiredField.isSet() && this.requiredField.value() == null) { throw new IllegalStateException("Field 'requiredField' is marked as non-null but null value was provided"); } diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/ComprehensiveFeatureIntegrationTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/ComprehensiveFeatureIntegrationTest.java index 86a4fe7b..5d4ccd8b 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/ComprehensiveFeatureIntegrationTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/ComprehensiveFeatureIntegrationTest.java @@ -180,6 +180,7 @@ public PersonDto(String name, int age, Optional email, @Generated("Generated by org.javahelpers.simple.builders.processor.BuilderProcessor") @BuilderImplementation(forClass = PersonDto.class) public class PersonDtoBuilder implements IBuilderBase { + /** * Tracked value for name: name. */ @@ -239,10 +240,6 @@ public PersonDtoBuilder() { public PersonDtoBuilder(PersonDto instance) { this.name = initialValue(instance.getName()); this.age = initialValue(instance.getAge()); - if (this.age.value() == null) { - throw new IllegalArgumentException( - "Cannot initialize builder from instance: field 'age' is marked as non-null but source object has null value"); - } this.email = initialValue(instance.getEmail()); this.nicknames = initialValue(instance.getNicknames()); this.tags = initialValue(instance.getTags()); @@ -623,8 +620,7 @@ public PersonDtoBuilder phoneNumbers(LinkedList phoneNumbers) { * @param phoneNumbersBuilderConsumer consumer providing an instance of a builder for phoneNumbers * @return current instance of builder */ - public PersonDtoBuilder phoneNumbers( - Consumer> phoneNumbersBuilderConsumer) { + public PersonDtoBuilder phoneNumbers(Consumer> phoneNumbersBuilderConsumer) { ArrayListBuilder builder = this.phoneNumbers.isSet() ? new ArrayListBuilder(this.phoneNumbers.value()) : new ArrayListBuilder(); @@ -675,8 +671,9 @@ public PersonDtoBuilder previousAddresses(List previousAddresses) { public PersonDtoBuilder previousAddresses( Consumer> previousAddressesBuilderConsumer) { ArrayListBuilderWithElementBuilders builder = this.previousAddresses.isSet() - ? new ArrayListBuilderWithElementBuilders(this.previousAddresses.value(), AddressDtoBuilder::create) - : new ArrayListBuilderWithElementBuilders(AddressDtoBuilder::create); + ? new ArrayListBuilderWithElementBuilders(this.previousAddresses.value(), + AddressDtoBuilder::create) + : new ArrayListBuilderWithElementBuilders(AddressDtoBuilder::create); previousAddressesBuilderConsumer.accept(builder); this.previousAddresses = changedValue(builder.build()); return this; @@ -760,11 +757,12 @@ public PersonDtoBuilder conditional(BooleanSupplier condition, Consumer trueCase, Consumer falseCase) { + public PersonDtoBuilder conditional(BooleanSupplier condition, Consumer trueCase, + Consumer falseCase) { if (condition.getAsBoolean()) { - trueCase.accept(this); + trueCase.accept(this); } else if (falseCase != null) { - falseCase.accept(this); + falseCase.accept(this); } return this; } From 9c3741619c6ff315d978b1c36d9f96530ffe787c Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Mon, 6 Apr 2026 23:35:31 +0200 Subject: [PATCH 05/30] Switching from javax Modifier to AccessModifier --- .../classgen/roaster/RoasterCodeGenerator.java | 9 +++++---- .../generators/builder/ConditionalEnhancer.java | 6 +++--- .../generators/builder/ConstructorEnhancer.java | 9 ++------- .../generators/builder/CoreMethodsEnhancer.java | 8 ++++---- .../generators/util/MethodGeneratorUtil.java | 12 ++++++------ .../processor/model/core/ClassFieldDto.java | 8 ++++---- .../model/core/GenerationTargetClassDto.java | 8 ++++---- .../processor/model/method/ConstructorDto.java | 8 ++++---- .../builders/processor/model/method/MethodDto.java | 13 ++++++------- .../processing/BuilderDefinitionCreator.java | 6 +++--- 10 files changed, 41 insertions(+), 46 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java index 35079193..aec144a2 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java @@ -37,11 +37,11 @@ import java.util.Properties; import java.util.Set; import javax.annotation.processing.ProcessingEnvironment; -import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; import javax.tools.JavaFileObject; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; +import org.javahelpers.simple.builders.core.enums.AccessModifier; import org.javahelpers.simple.builders.core.util.TrackedValue; import org.javahelpers.simple.builders.processor.exceptions.BuilderException; import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto; @@ -358,16 +358,17 @@ private void appendNestedMethod(JavaSource source, MethodDto methodDto, boole } private void applyVisibility( - org.jboss.forge.roaster.model.source.VisibilityScopedSource source, Modifier modifier) { + org.jboss.forge.roaster.model.source.VisibilityScopedSource source, + AccessModifier modifier) { if (modifier == null) { source.setPackagePrivate(); return; } switch (modifier) { case PUBLIC -> source.setPublic(); - case PROTECTED -> source.setProtected(); case PRIVATE -> source.setPrivate(); - default -> source.setPackagePrivate(); + case PACKAGE_PRIVATE -> source.setPackagePrivate(); + case DEFAULT -> source.setPackagePrivate(); // DEFAULT maps to PACKAGE_PRIVATE } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ConditionalEnhancer.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ConditionalEnhancer.java index 59267d1f..e8d65245 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ConditionalEnhancer.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ConditionalEnhancer.java @@ -24,7 +24,7 @@ package org.javahelpers.simple.builders.processor.generators.builder; import java.util.function.BooleanSupplier; -import javax.lang.model.element.Modifier; +import org.javahelpers.simple.builders.core.enums.AccessModifier; import org.javahelpers.simple.builders.processor.analysis.JavaLangMapper; import org.javahelpers.simple.builders.processor.generators.BuilderEnhancer; import org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil; @@ -119,7 +119,7 @@ private MethodDto createConditionalMethod(BuilderDefinitionDto builderDto) { MethodDto method = new MethodDto("conditional", builderDto.getBuilderTypeName()); method.setOrdering(ORDERING_CONDITIONAL); method.setPriority(MethodDto.PRIORITY_HIGHEST); - method.setModifier(Modifier.PUBLIC); + method.setModifier(AccessModifier.PUBLIC); // Add parameters addConditionalPositiveNegativeParameters(method, builderDto.getBuilderTypeName()); @@ -151,7 +151,7 @@ private MethodDto createConditionalPositiveOnlyMethod(BuilderDefinitionDto build MethodDto method = new MethodDto("conditional", builderDto.getBuilderTypeName()); method.setOrdering(ORDERING_CONDITIONAL_POSITIVE_ONLY); method.setPriority(MethodDto.PRIORITY_HIGHEST); - method.setModifier(Modifier.PUBLIC); + method.setModifier(AccessModifier.PUBLIC); // Add parameters addConditionalPositiveOnlyParameters(method, builderDto.getBuilderTypeName()); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ConstructorEnhancer.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ConstructorEnhancer.java index 89a7040f..29255ac0 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ConstructorEnhancer.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ConstructorEnhancer.java @@ -25,7 +25,6 @@ package org.javahelpers.simple.builders.processor.generators.builder; import java.util.concurrent.atomic.AtomicBoolean; -import org.javahelpers.simple.builders.processor.analysis.JavaLangMapper; import org.javahelpers.simple.builders.processor.generators.BuilderEnhancer; import org.javahelpers.simple.builders.processor.model.core.BuilderDefinitionDto; import org.javahelpers.simple.builders.processor.model.core.FieldDto; @@ -78,9 +77,7 @@ public void enhanceBuilder(BuilderDefinitionDto builderDto, ProcessingContext co /** Creates the empty constructor. */ protected ConstructorDto createEmptyConstructor(BuilderDefinitionDto builderDto) { ConstructorDto constructor = new ConstructorDto(); - constructor.setVisibility( - JavaLangMapper.mapAccessModifier( - builderDto.getConfiguration().getBuilderConstructorAccess())); + constructor.setVisibility(builderDto.getConfiguration().getBuilderConstructorAccess()); constructor.setBody(""); // Empty body String targetFullName = builderDto.getBuildingTargetTypeName().getFullQualifiedName(); @@ -93,9 +90,7 @@ protected ConstructorDto createEmptyConstructor(BuilderDefinitionDto builderDto) /** Creates the from-instance constructor. */ protected ConstructorDto createFromInstanceConstructor(BuilderDefinitionDto builderDto) { ConstructorDto constructor = new ConstructorDto(); - constructor.setVisibility( - JavaLangMapper.mapAccessModifier( - builderDto.getConfiguration().getBuilderConstructorAccess())); + constructor.setVisibility(builderDto.getConfiguration().getBuilderConstructorAccess()); // Add parameter for the DTO instance MethodParameterDto instanceParam = new MethodParameterDto(); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java index 4052febf..066f5224 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java @@ -25,7 +25,7 @@ import java.util.ArrayList; import java.util.List; -import javax.lang.model.element.Modifier; +import org.javahelpers.simple.builders.core.enums.AccessModifier; import org.javahelpers.simple.builders.processor.analysis.JavaLangMapper; import org.javahelpers.simple.builders.processor.generators.BuilderEnhancer; import org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil; @@ -120,7 +120,7 @@ protected MethodDto createBuildMethod(BuilderDefinitionDto builderDto) { MethodDto method = new MethodDto("build", returnType); method.setOrdering(ORDERING_BUILD); method.setPriority(MethodDto.PRIORITY_HIGHEST); - method.setModifier(Modifier.PUBLIC); + method.setModifier(AccessModifier.PUBLIC); // Add @Override annotation only if implementing IBuilderBase interface if (builderDto.getConfiguration().shouldImplementBuilderBase()) { @@ -213,7 +213,7 @@ protected MethodDto createStaticCreateMethod(BuilderDefinitionDto builderDto) { MethodDto method = new MethodDto("create", returnType); method.setOrdering(ORDERING_CREATE); method.setPriority(MethodDto.PRIORITY_HIGHEST); - method.setModifier(Modifier.PUBLIC); + method.setModifier(AccessModifier.PUBLIC); method.setStatic(true); // Use appropriate code template based on whether we have generics @@ -244,7 +244,7 @@ protected MethodDto createToStringMethod(BuilderDefinitionDto builderDto) { MethodDto method = new MethodDto("toString", new TypeName("java.lang", "String")); method.setOrdering(ORDERING_TO_STRING); method.setPriority(MethodDto.PRIORITY_HIGHEST); - method.setModifier(Modifier.PUBLIC); + method.setModifier(AccessModifier.PUBLIC); AnnotationDto overrideAnnotation = new AnnotationDto(); overrideAnnotation.setAnnotationType(JavaLangMapper.map2TypeName(Override.class)); method.addAnnotation(overrideAnnotation); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java index 0ea3f573..256cbdcd 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java @@ -27,8 +27,8 @@ import java.util.List; import java.util.Map; import java.util.function.Consumer; -import javax.lang.model.element.Modifier; import org.apache.commons.lang3.StringUtils; +import org.javahelpers.simple.builders.core.enums.AccessModifier; import org.javahelpers.simple.builders.processor.analysis.JavaLangMapper; import org.javahelpers.simple.builders.processor.model.core.FieldDto; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; @@ -90,13 +90,13 @@ public static String generateBuilderMethodName(String fieldName, ProcessingConte } /** - * Gets the method access modifier from the builder configuration. + * Gets the access modifier for methods from the processing context. * * @param context the processing context - * @return the Modifier for method access, or null for package-private + * @return the AccessModifier for method access, or null for package-private */ - public static Modifier getMethodAccessModifier(ProcessingContext context) { - return JavaLangMapper.mapAccessModifier(context.getConfiguration().getMethodAccess()); + public static AccessModifier getMethodAccessModifier(ProcessingContext context) { + return context.getConfiguration().getMethodAccess(); } /** @@ -105,7 +105,7 @@ public static Modifier getMethodAccessModifier(ProcessingContext context) { * @param method the MethodDto to update * @param modifier the access modifier to set, or null for package-private */ - public static void setMethodAccessModifier(MethodDto method, Modifier modifier) { + public static void setMethodAccessModifier(MethodDto method, AccessModifier modifier) { if (modifier != null) { method.setModifier(modifier); } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/ClassFieldDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/ClassFieldDto.java index 03b8f440..a19361b4 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/ClassFieldDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/ClassFieldDto.java @@ -26,7 +26,7 @@ import java.util.LinkedHashSet; import java.util.Set; -import javax.lang.model.element.Modifier; +import org.javahelpers.simple.builders.core.enums.AccessModifier; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; @@ -44,7 +44,7 @@ public class ClassFieldDto { private TypeName fieldType; /** Field visibility (typically PRIVATE). */ - private Modifier visibility; + private AccessModifier visibility; /** Literal initializer (e.g., "unsetValue()"). */ private String literalInitializer; @@ -71,11 +71,11 @@ public void setFieldType(TypeName fieldType) { this.fieldType = fieldType; } - public Modifier getVisibility() { + public AccessModifier getVisibility() { return visibility; } - public void setVisibility(Modifier visibility) { + public void setVisibility(AccessModifier visibility) { this.visibility = visibility; } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/GenerationTargetClassDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/GenerationTargetClassDto.java index deafb828..970fcd0f 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/GenerationTargetClassDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/GenerationTargetClassDto.java @@ -28,7 +28,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Set; -import javax.lang.model.element.Modifier; +import org.javahelpers.simple.builders.core.enums.AccessModifier; import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto; import org.javahelpers.simple.builders.processor.model.annotation.InterfaceName; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; @@ -49,7 +49,7 @@ public class GenerationTargetClassDto { private TypeName typeName; /** Class visibility. */ - private Modifier classAccessModifier; + private AccessModifier classAccessModifier; /** Superclass (e.g., SimpleModule), null if none. */ private TypeName superType; @@ -102,11 +102,11 @@ public void setTypeName(TypeName typeName) { this.typeName = typeName; } - public Modifier getClassAccessModifier() { + public AccessModifier getClassAccessModifier() { return classAccessModifier; } - public void setClassAccessModifier(Modifier classAccessModifier) { + public void setClassAccessModifier(AccessModifier classAccessModifier) { this.classAccessModifier = classAccessModifier; } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/ConstructorDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/ConstructorDto.java index 842986b9..289e3133 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/ConstructorDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/ConstructorDto.java @@ -29,7 +29,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Set; -import javax.lang.model.element.Modifier; +import org.javahelpers.simple.builders.core.enums.AccessModifier; import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; @@ -47,7 +47,7 @@ public class ConstructorDto { private MethodCodeDto methodCodeDto = new MethodCodeDto(); /** Constructor visibility. */ - private Modifier visibility; + private AccessModifier visibility; /** Constructor javadoc. */ private JavadocDto javadoc; @@ -106,11 +106,11 @@ public void setBody(String body) { this.methodCodeDto.setCodeFormat(body); } - public Modifier getVisibility() { + public AccessModifier getVisibility() { return visibility; } - public void setVisibility(Modifier visibility) { + public void setVisibility(AccessModifier visibility) { this.visibility = visibility; } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodDto.java index 7233b94d..4d602e62 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodDto.java @@ -28,8 +28,8 @@ import java.util.LinkedList; import java.util.List; import java.util.Optional; -import javax.lang.model.element.Modifier; import org.apache.commons.lang3.StringUtils; +import org.javahelpers.simple.builders.core.enums.AccessModifier; import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; import org.javahelpers.simple.builders.processor.model.type.GenericParameterDto; @@ -44,7 +44,7 @@ public class MethodDto { public static final int PRIORITY_LOW = 60; // Specialized consumers /** Access modifier for method. */ - private Optional modifier = Optional.empty(); + private Optional modifier = Optional.empty(); /** Whether the method is static. */ private boolean isStatic = false; @@ -273,19 +273,18 @@ public List getGenericParameters() { /** * Getting the access modifier for method. Optional for usage in stream-notation. * - * @return modifier {@code java.util.Optional} access modifier of type {@code - * javax.lang.model.element.Modifier} + * @return modifier {@code java.util.Optional} access modifier of type {@code AccessModifier} */ - public Optional getModifier() { + public Optional getModifier() { return modifier; } /** * Sets the access modifier for method. * - * @param modifier access modifier of type {@code javax.lang.model.element.Modifier} + * @param modifier access modifier of type {@code AccessModifier} */ - public void setModifier(Modifier modifier) { + public void setModifier(AccessModifier modifier) { this.modifier = Optional.ofNullable(modifier); } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java index e72d1e00..235330a6 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java @@ -40,6 +40,7 @@ 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.enums.AccessModifier; import org.javahelpers.simple.builders.processor.analysis.FieldAnnotationExtractor; import org.javahelpers.simple.builders.processor.analysis.JavaLangAnalyser; import org.javahelpers.simple.builders.processor.analysis.JavaLangMapper; @@ -154,8 +155,7 @@ private static void finalizeDefinition( } // 3. Set class access modifier - builderDto.setClassAccessModifier( - JavaLangMapper.mapAccessModifier(builderDto.getConfiguration().getBuilderAccess())); + builderDto.setClassAccessModifier(builderDto.getConfiguration().getBuilderAccess()); // 4. Set static imports for TrackedValue builderDto.addStaticImport( @@ -205,7 +205,7 @@ private static ClassFieldDto convertToClassField(FieldDto field) { TypeName wrappedFieldType = new TypeNameGeneric(trackedValueType, List.of(boxedType)); classField.setFieldType(wrappedFieldType); - classField.setVisibility(Modifier.PRIVATE); + classField.setVisibility(AccessModifier.PRIVATE); classField.setLiteralInitializer("unsetValue()"); classField.setJavadoc(field.getJavaDoc()); From 9290bdbc5dc0e56e704683e694cc5657aa631c81 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sat, 11 Apr 2026 23:51:46 +0200 Subject: [PATCH 06/30] Improving code quality --- .../roaster/RoasterCodeGenerator.java | 39 +++++++---------- .../builder/ConstructorEnhancer.java | 43 ++++++------------- .../builder/CoreMethodsEnhancer.java | 5 --- .../field/AddToCollectionGenerator.java | 5 +-- .../field/ArrayBuilderConsumerGenerator.java | 2 +- .../field/ArrayConversionGenerator.java | 2 +- .../field/StringBuilderConsumerGenerator.java | 2 +- .../field/StringFormatHelperGenerator.java | 2 +- .../field/SupplierMethodGenerator.java | 2 +- .../generators/util/MethodGeneratorUtil.java | 18 ++------ .../processor/model/core/ClassFieldDto.java | 4 +- .../processor/model/method/MethodCodeDto.java | 24 +++++++++++ 12 files changed, 64 insertions(+), 84 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java index aec144a2..9496c28d 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java @@ -53,6 +53,7 @@ import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocTagDto; import org.javahelpers.simple.builders.processor.model.method.ConstructorDto; +import org.javahelpers.simple.builders.processor.model.method.MethodCodeDto; import org.javahelpers.simple.builders.processor.model.method.MethodCodePlaceholder; import org.javahelpers.simple.builders.processor.model.method.MethodCodeTypePlaceholder; import org.javahelpers.simple.builders.processor.model.method.MethodDto; @@ -164,13 +165,8 @@ private JavaClassSource createJavaClassSource(GenerationTargetClassDto classDef) } private void addClassMetadata(JavaClassSource source, GenerationTargetClassDto classDef) { - // Add class JavaDoc applyJavadoc(source, classDef.getClassJavadoc()); - - // Set class access level applyVisibility(source, classDef.getClassAccessModifier()); - - // Add superclass if specified if (classDef.getSuperType() != null) { source.setSuperType(mapType(classDef.getSuperType())); } @@ -195,16 +191,14 @@ private void appendFields(JavaClassSource source, GenerationTargetClassDto class appendField(source, fieldDto); } - logger.debugEndOperation("Fields added: %d fields", classDef.getClassFields().size()); + logger.debugEndOperation("Fields added: %d fields", source.getFields().size()); } private void appendField(JavaClassSource source, ClassFieldDto fieldDto) { FieldSource field = source.addField(); field.setName(fieldDto.getFieldName()); field.setType(mapType(fieldDto.getFieldType())); - if (fieldDto.getVisibility() != null) { - applyVisibility(field, fieldDto.getVisibility()); - } + applyVisibility(field, fieldDto.getVisibility()); if (fieldDto.getLiteralInitializer() != null) { field.setLiteralInitializer(fieldDto.getLiteralInitializer()); } @@ -224,19 +218,12 @@ private void appendConstructors(JavaClassSource source, GenerationTargetClassDto private void appendConstructor(JavaClassSource source, ConstructorDto constructor) { MethodSource method = source.addMethod(); method.setConstructor(true); - if (constructor.getVisibility() != null) { - applyVisibility(method, constructor.getVisibility()); - } + applyVisibility(method, constructor.getVisibility()); for (MethodParameterDto param : constructor.getParameters()) { method.addParameter(mapType(param.getParameterType()), param.getParameterName()); } applyJavadoc(method, constructor.getJavadoc()); - // Use getMethodCodeDto for proper template resolution - // Note: empty string is a valid body (for empty constructors) - if (constructor.getMethodCodeDto() != null - && constructor.getMethodCodeDto().getCodeFormat() != null) { - method.setBody(resolveCodeTemplate(constructor.getMethodCodeDto())); - } + applyCodeBlock(method, constructor.getMethodCodeDto()); } private void appendMethods(JavaClassSource source, GenerationTargetClassDto classDef) { @@ -305,12 +292,7 @@ private void appendMethod( boolean interfaceMethod) { MethodSource method = source.addMethod(); configureMethod(method, methodDto, nestedTypeMethod, interfaceMethod); - String body = - methodDto.getMethodCodeDto() != null - && StringUtils.isNotBlank(methodDto.getMethodCodeDto().getCodeFormat()) - ? resolveCodeTemplate(methodDto.getMethodCodeDto()) - : ""; - method.setBody(body); + applyCodeBlock(method, methodDto.getMethodCodeDto()); } private void appendNestedTypes(JavaClassSource source, GenerationTargetClassDto classDef) { @@ -404,6 +386,15 @@ private void addGenericDeclarations( } } + private void applyCodeBlock(MethodSource method, MethodCodeDto codeDto) { + if (codeDto == null || StringUtils.isBlank(codeDto.getCodeFormat())) { + // If implementation is missing, an empty body needs to be set + method.setBody(""); + return; + } + method.setBody(resolveCodeTemplate(codeDto)); + } + private void applyJavadoc( org.jboss.forge.roaster.model.source.JavaDocCapableSource source, JavadocDto javadoc) { if (javadoc == null || !javadoc.hasContent()) { diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ConstructorEnhancer.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ConstructorEnhancer.java index 29255ac0..b1c4f144 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ConstructorEnhancer.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ConstructorEnhancer.java @@ -24,12 +24,12 @@ package org.javahelpers.simple.builders.processor.generators.builder; -import java.util.concurrent.atomic.AtomicBoolean; import org.javahelpers.simple.builders.processor.generators.BuilderEnhancer; import org.javahelpers.simple.builders.processor.model.core.BuilderDefinitionDto; import org.javahelpers.simple.builders.processor.model.core.FieldDto; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; import org.javahelpers.simple.builders.processor.model.method.ConstructorDto; +import org.javahelpers.simple.builders.processor.model.method.MethodCodeDto; import org.javahelpers.simple.builders.processor.model.method.MethodParameterDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.model.type.TypeNamePrimitive; @@ -65,20 +65,14 @@ public boolean appliesTo( @Override public void enhanceBuilder(BuilderDefinitionDto builderDto, ProcessingContext context) { - // Add empty constructor - ConstructorDto emptyConstructor = createEmptyConstructor(builderDto); - builderDto.getConstructors().add(emptyConstructor); - - // Add from-instance constructor - ConstructorDto fromInstanceConstructor = createFromInstanceConstructor(builderDto); - builderDto.getConstructors().add(fromInstanceConstructor); + builderDto.addConstructor(createEmptyConstructor(builderDto)); + builderDto.addConstructor(createFromInstanceConstructor(builderDto)); } /** Creates the empty constructor. */ protected ConstructorDto createEmptyConstructor(BuilderDefinitionDto builderDto) { ConstructorDto constructor = new ConstructorDto(); constructor.setVisibility(builderDto.getConfiguration().getBuilderConstructorAccess()); - constructor.setBody(""); // Empty body String targetFullName = builderDto.getBuildingTargetTypeName().getFullQualifiedName(); constructor.setJavadoc( @@ -98,10 +92,9 @@ protected ConstructorDto createFromInstanceConstructor(BuilderDefinitionDto buil instanceParam.setParameterTypeName(builderDto.getBuildingTargetTypeName()); constructor.addParameter(instanceParam); - // Build constructor body using text blocks - // The TrackedValue.* prefixes will be replaced by RoasterCodeGenerator - StringBuilder bodyCode = new StringBuilder(); - AtomicBoolean hasNonNullChecks = new AtomicBoolean(false); + // Build constructor code body + MethodCodeDto codeDto = new MethodCodeDto(); + constructor.setMethodCodeDto(codeDto); // Initialize fields from instance for (FieldDto field : builderDto.getAllFieldsForBuilder()) { @@ -116,16 +109,16 @@ protected ConstructorDto createFromInstanceConstructor(BuilderDefinitionDto buil if (field.isNonNullable() && !(field.getFieldType() instanceof TypeNamePrimitive)) { // Non-nullable non-primitive field - validate not null // Skip primitives as they can't be compared to null (compilation error) - bodyCode.append( - """ - if (instance.%s() == null) { - throw new IllegalArgumentException("Field '%s' is non-null but instance.%s() returned null"); - } + codeDto.append( """ - .formatted(getterName, fieldName, getterName)); - hasNonNullChecks.set(true); + if (instance.%s() == null) { + throw new IllegalArgumentException("Field '%s' is non-null but instance.%s() returned null"); + } + """, + getterName, fieldName, getterName); + codeDto.addCodeBlockImport(new TypeName("java.lang", "IllegalArgumentException")); } - bodyCode.append( + codeDto.append( """ this.%s = TrackedValue.initialValue(instance.%s()); """ @@ -135,14 +128,6 @@ protected ConstructorDto createFromInstanceConstructor(BuilderDefinitionDto buil // Leave it unset (will use unsetValue() from field initializer) } - // Set the body as string (will be processed by RoasterCodeGenerator) - constructor.setBody(bodyCode.toString()); - - // Add IllegalArgumentException import only if we have non-null checks - if (hasNonNullChecks.get()) { - constructor.addCodeBlockImport(new TypeName("java.lang", "IllegalArgumentException")); - } - String targetFullName = builderDto.getBuildingTargetTypeName().getFullQualifiedName(); constructor.setJavadoc( new JavadocDto("Initialisation of builder for {@code %s} by a instance.", targetFullName) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java index 066f5224..7d781324 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java @@ -196,10 +196,7 @@ protected MethodDto createBuildMethod(BuilderDefinitionDto builderDto) { method.setCode(code.toString()); method.addArgument("dtoBaseType", builderDto.getBuildingTargetTypeName()); method.addArgument("buildResultType", returnType); - - // Add imports for types used in code block method.addCodeBlockImport(new TypeName("java.lang", "IllegalStateException")); - method.setJavadoc(new JavadocDto("Builds the configured DTO instance.")); return method; @@ -263,8 +260,6 @@ protected MethodDto createToStringMethod(BuilderDefinitionDto builderDto) { method.addArgument("toStringBuilder", toStringBuilderType); method.addArgument("toStringStyle", toStringStyleType); - - // Add imports for types used in code block method.addCodeBlockImport(toStringBuilderType); method.addCodeBlockImport(toStringStyleType); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/AddToCollectionGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/AddToCollectionGenerator.java index ba859da4..fc02dca3 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/AddToCollectionGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/AddToCollectionGenerator.java @@ -146,7 +146,7 @@ private MethodDto createAddToCollectionMethod( parameter.setParameterTypeName(elementType); methodDto.addParameter(parameter); - setMethodAccessModifier(methodDto, getMethodAccessModifier(context)); + methodDto.setModifier(getMethodAccessModifier(context)); String collectionImpl; TypeName collectionVarType; @@ -179,10 +179,7 @@ private MethodDto createAddToCollectionMethod( methodDto.addArgument("fieldName", fieldNameInBuilder); methodDto.addArgument("elementType", elementType); methodDto.addArgument("builderFieldWrapper", TRACKED_VALUE_TYPE); - - // Add imports for types used in code block methodDto.addCodeBlockImport(collectionImplType); - methodDto.setPriority(MethodDto.PRIORITY_MEDIUM); methodDto.setJavadoc( diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ArrayBuilderConsumerGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ArrayBuilderConsumerGenerator.java index ffd83cfe..d3b6dec5 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ArrayBuilderConsumerGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ArrayBuilderConsumerGenerator.java @@ -132,7 +132,7 @@ private MethodDto createFieldConsumerWithArrayBuilder( MethodDto methodDto = new MethodDto(generateBuilderMethodName(fieldName, context), returnBuilderType); methodDto.addParameter(parameter); - setMethodAccessModifier(methodDto, getMethodAccessModifier(context)); + methodDto.setModifier(getMethodAccessModifier(context)); methodDto.setCode( """ $helperType:T builder = this.$fieldName:N.isSet() diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ArrayConversionGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ArrayConversionGenerator.java index 927f06c7..379172ad 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ArrayConversionGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ArrayConversionGenerator.java @@ -119,7 +119,7 @@ private MethodDto createFieldSetterForArrayFromList( MethodDto methodDto = new MethodDto(generateBuilderMethodName(fieldName, context), builderType); methodDto.addParameter(parameter); - setMethodAccessModifier(methodDto, getMethodAccessModifier(context)); + methodDto.setModifier(getMethodAccessModifier(context)); methodDto.setCode( """ this.$fieldName:N = $builderFieldWrapper:T.changedValue($dtoMethodParams:N.toArray(new $elementType:T[0])); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringBuilderConsumerGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringBuilderConsumerGenerator.java index c27e3bf4..438a1366 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringBuilderConsumerGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringBuilderConsumerGenerator.java @@ -141,7 +141,7 @@ private MethodDto createStringBuilderConsumer( parameter.setParameterTypeName(consumerType); MethodDto methodDto = new MethodDto(generateBuilderMethodName(fieldName, context), builderType); methodDto.addParameter(parameter); - setMethodAccessModifier(methodDto, getMethodAccessModifier(context)); + methodDto.setModifier(getMethodAccessModifier(context)); methodDto.setCode( """ StringBuilder builder = new StringBuilder(); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringFormatHelperGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringFormatHelperGenerator.java index eabc4995..7f402a58 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringFormatHelperGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringFormatHelperGenerator.java @@ -184,7 +184,7 @@ private MethodDto createStringFormatMethodWithTransform( MethodDto methodDto = new MethodDto(generateBuilderMethodName(fieldName, context), builderType); methodDto.addParameter(formatParam); methodDto.addParameter(argsParam); - setMethodAccessModifier(methodDto, getMethodAccessModifier(context)); + methodDto.setModifier(getMethodAccessModifier(context)); methodDto.setCode( """ this.$fieldName:N = $builderFieldWrapper:T.changedValue($transform:N); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/SupplierMethodGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/SupplierMethodGenerator.java index 8e8ef06d..101468dd 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/SupplierMethodGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/SupplierMethodGenerator.java @@ -133,7 +133,7 @@ private MethodDto createFieldSupplier( MethodDto methodDto = new MethodDto(generateBuilderMethodName(fieldName, context), builderType); methodDto.addParameter(parameter); - setMethodAccessModifier(methodDto, getMethodAccessModifier(context)); + methodDto.setModifier(getMethodAccessModifier(context)); methodDto.setCode( """ diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java index 256cbdcd..759d3595 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java @@ -99,18 +99,6 @@ public static AccessModifier getMethodAccessModifier(ProcessingContext context) return context.getConfiguration().getMethodAccess(); } - /** - * Sets the access modifier on a MethodDto if the modifier is not null. - * - * @param method the MethodDto to update - * @param modifier the access modifier to set, or null for package-private - */ - public static void setMethodAccessModifier(MethodDto method, AccessModifier modifier) { - if (modifier != null) { - method.setModifier(modifier); - } - } - /** * Creates a generic TypeName from a base type and generic parameters. * @@ -167,7 +155,7 @@ public static MethodDto createBuilderMethodForFieldWithTransform( new MethodDto( generateBuilderMethodName(field.getOriginalFieldName(), context), builderType); methodDto.addParameter(parameter); - setMethodAccessModifier(methodDto, getMethodAccessModifier(context)); + methodDto.setModifier(getMethodAccessModifier(context)); String params; if (StringUtils.isBlank(transform)) { @@ -224,7 +212,7 @@ public static MethodDto createFieldConsumerWithBuilder( new MethodDto( generateBuilderMethodName(field.getOriginalFieldName(), context), parentBuilderType); methodDto.addParameter(parameter); - setMethodAccessModifier(methodDto, getMethodAccessModifier(context)); + methodDto.setModifier(getMethodAccessModifier(context)); String buildExpression = calculateBuildExpression(field.getFieldType()); @@ -361,7 +349,7 @@ public static MethodDto createSimpleFieldConsumer( new MethodDto( generateBuilderMethodName(field.getOriginalFieldName(), context), builderType); methodDto.addParameter(parameter); - setMethodAccessModifier(methodDto, getMethodAccessModifier(context)); + methodDto.setModifier(getMethodAccessModifier(context)); methodDto.setCode( """ diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/ClassFieldDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/ClassFieldDto.java index a19361b4..df6c9b22 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/ClassFieldDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/ClassFieldDto.java @@ -40,13 +40,13 @@ public class ClassFieldDto { /** Field name in generated class. */ private String fieldName; - /** Field type (e.g., TrackedValue represented as TypeName). */ + /** Field type (e.g., String represented as TypeName). */ private TypeName fieldType; /** Field visibility (typically PRIVATE). */ private AccessModifier visibility; - /** Literal initializer (e.g., "unsetValue()"). */ + /** Literal initializer (e.g., "exampleValue" like in {@code String field="exampleValue";}). */ private String literalInitializer; /** Field javadoc. */ diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodCodeDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodCodeDto.java index 3eda5f4b..a9e76de8 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodCodeDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodCodeDto.java @@ -28,6 +28,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Set; +import org.apache.commons.lang3.StringUtils; import org.javahelpers.simple.builders.processor.model.type.TypeName; /** DTO for holding information of code implementation. */ @@ -106,4 +107,27 @@ public Set getCodeBlockImports() { public void addCodeBlockImport(TypeName typeName) { this.codeBlockImports.add(typeName); } + + /** + * Appends additional code to the existing code format with string formatting support. + * + *

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

Supports the same formatting syntax as {@link String#format(String, Object...)}, allowing + * for dynamic value insertion using placeholders like %s, %d, etc. + * + * @param formatted the code fragment with format placeholders to append to the existing code + * format + * @param args the arguments to be formatted into the string + */ + public void append(String formatted, Object... args) { + String formattedCode = args.length > 0 ? String.format(formatted, args) : formatted; + if (StringUtils.isEmpty(codeFormat)) { + codeFormat = formattedCode; + } else { + codeFormat += "\n" + formattedCode; + } + } } From 1e8e096333fb0855b4a1b43ffff7b4fa5f35316a Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sat, 11 Apr 2026 23:52:40 +0200 Subject: [PATCH 07/30] Reintegration of protected enum value for accessModifier --- .../javahelpers/simple/builders/core/enums/AccessModifier.java | 3 +++ .../simple/builders/processor/analysis/JavaLangMapper.java | 1 + .../processor/classgen/roaster/RoasterCodeGenerator.java | 1 + 3 files changed, 5 insertions(+) diff --git a/core/src/main/java/org/javahelpers/simple/builders/core/enums/AccessModifier.java b/core/src/main/java/org/javahelpers/simple/builders/core/enums/AccessModifier.java index 38a481c2..d2b4aa19 100644 --- a/core/src/main/java/org/javahelpers/simple/builders/core/enums/AccessModifier.java +++ b/core/src/main/java/org/javahelpers/simple/builders/core/enums/AccessModifier.java @@ -41,6 +41,9 @@ public enum AccessModifier { /** Public access - accessible from anywhere */ PUBLIC("public"), + /** Protected access - accessible from the same package and subclasses */ + PROTECTED("protected"), + /** Package-private access (default) - accessible only within the same package */ PACKAGE_PRIVATE(""), diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangMapper.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangMapper.java index 4b42d3ba..3c5b66a6 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangMapper.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangMapper.java @@ -93,6 +93,7 @@ public static Modifier mapAccessModifier(AccessModifier accessModifier) { } return switch (accessModifier) { case PUBLIC, DEFAULT -> Modifier.PUBLIC; + case PROTECTED -> Modifier.PROTECTED; case PRIVATE -> Modifier.PRIVATE; case PACKAGE_PRIVATE -> null; // Package-private has no explicit modifier }; diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java index 9496c28d..ed6b6c47 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java @@ -348,6 +348,7 @@ private void applyVisibility( } switch (modifier) { case PUBLIC -> source.setPublic(); + case PROTECTED -> source.setProtected(); case PRIVATE -> source.setPrivate(); case PACKAGE_PRIVATE -> source.setPackagePrivate(); case DEFAULT -> source.setPackagePrivate(); // DEFAULT maps to PACKAGE_PRIVATE From 207aad5e6fb9aee886c40642ac629b786c4847fb Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sat, 11 Apr 2026 23:58:08 +0200 Subject: [PATCH 08/30] Removed unneeded switch and boxing --- .../processing/BuilderDefinitionCreator.java | 28 ++++--------------- 1 file changed, 5 insertions(+), 23 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java index 235330a6..9964b3f6 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java @@ -55,7 +55,6 @@ import org.javahelpers.simple.builders.processor.model.method.MethodParameterDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.model.type.TypeNameGeneric; -import org.javahelpers.simple.builders.processor.model.type.TypeNamePrimitive; /** Class for creating a specific BuilderDefinitionDto for an annotated DTO class. */ public class BuilderDefinitionCreator { @@ -181,28 +180,11 @@ private static ClassFieldDto convertToClassField(FieldDto field) { ClassFieldDto classField = new ClassFieldDto(); classField.setFieldName(field.getFieldNameInBuilder()); - // Build the field type: TrackedValue + // Build the field type: TrackedValue TypeName trackedValueType = new TypeName("org.javahelpers.simple.builders.core.util", "TrackedValue"); - // Box primitive types (int -> Integer, etc.) - TypeName boxedType; - if (field.getFieldType() instanceof TypeNamePrimitive primitive) { - boxedType = - switch (primitive.getType()) { - case BOOLEAN -> new TypeName("java.lang", "Boolean"); - case BYTE -> new TypeName("java.lang", "Byte"); - case SHORT -> new TypeName("java.lang", "Short"); - case INT -> new TypeName("java.lang", "Integer"); - case LONG -> new TypeName("java.lang", "Long"); - case CHAR -> new TypeName("java.lang", "Character"); - case FLOAT -> new TypeName("java.lang", "Float"); - case DOUBLE -> new TypeName("java.lang", "Double"); - case VOID -> new TypeName("java.lang", "Void"); - }; - } else { - boxedType = field.getFieldType(); - } - TypeName wrappedFieldType = new TypeNameGeneric(trackedValueType, List.of(boxedType)); + TypeName wrappedFieldType = + new TypeNameGeneric(trackedValueType, List.of(field.getFieldType())); classField.setFieldType(wrappedFieldType); classField.setVisibility(AccessModifier.PRIVATE); @@ -212,8 +194,8 @@ private static ClassFieldDto convertToClassField(FieldDto field) { // Add field type imports classField.addFieldTypeImport( new TypeName("org.javahelpers.simple.builders.core.util", "TrackedValue")); - // Add the boxed field type for import (derived from original field type) - classField.addFieldTypeImport(boxedType); + // Add the field type for import + classField.addFieldTypeImport(field.getFieldType()); return classField; } From 95ad7718144acb17498fb4c0ccf959b5f685d98d Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sat, 11 Apr 2026 23:58:45 +0200 Subject: [PATCH 09/30] Adding toString to ClassFieldDto and ConstructorDto --- .../processor/model/core/ClassFieldDto.java | 15 +++++++++++++++ .../model/method/ConstructorDto.java | 19 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/ClassFieldDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/ClassFieldDto.java index df6c9b22..e23a5c72 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/ClassFieldDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/ClassFieldDto.java @@ -26,6 +26,8 @@ import java.util.LinkedHashSet; import java.util.Set; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; import org.javahelpers.simple.builders.core.enums.AccessModifier; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; @@ -102,4 +104,17 @@ public Set getFieldTypeImports() { public void addFieldTypeImport(TypeName typeName) { this.fieldTypeImports.add(typeName); } + + /** + * Returns a short string representation of this field using Apache Commons ToStringBuilder. + * + * @return concise string representation showing field name, type, and visibility + */ + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) + .append("name", fieldName) + .append("type", fieldType) + .toString(); + } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/ConstructorDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/ConstructorDto.java index 289e3133..d63b6069 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/ConstructorDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/ConstructorDto.java @@ -29,6 +29,8 @@ import java.util.LinkedList; import java.util.List; import java.util.Set; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; import org.javahelpers.simple.builders.core.enums.AccessModifier; import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; @@ -150,4 +152,21 @@ public void addCodeBlockImport(TypeName typeName) { } this.methodCodeDto.addCodeBlockImport(typeName); } + + /** + * Returns a string representation of this constructor using Apache Commons ToStringBuilder. + * + *

This provides a comprehensive view of the constructor including visibility, parameters, + * annotations, javadoc, and code block information for debugging and logging purposes. + * + * @return detailed string representation of the constructor + */ + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) + .append("parameters", parameters) + .append("annotations", annotations) + .append("hasCodeBlock", methodCodeDto != null && methodCodeDto.getCodeFormat() != null) + .toString(); + } } From 9fe1d9fecf56edc1859c9cc6d59b37f4a81ed51a Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 12 Apr 2026 00:02:49 +0200 Subject: [PATCH 10/30] Improving codequality --- .../roaster/RoasterCodeGenerator.java | 10 +++-- .../processor/BuilderProcessorTest.java | 38 +++++++++---------- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java index ed6b6c47..21afdeee 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java @@ -167,9 +167,7 @@ private JavaClassSource createJavaClassSource(GenerationTargetClassDto classDef) private void addClassMetadata(JavaClassSource source, GenerationTargetClassDto classDef) { applyJavadoc(source, classDef.getClassJavadoc()); applyVisibility(source, classDef.getClassAccessModifier()); - if (classDef.getSuperType() != null) { - source.setSuperType(mapType(classDef.getSuperType())); - } + applySuperType(source, classDef.getSuperType()); // Add interfaces for (InterfaceName interfaceName : classDef.getInterfaces()) { @@ -355,6 +353,12 @@ private void applyVisibility( } } + private void applySuperType(JavaClassSource source, TypeName superType) { + if (superType != null) { + source.setSuperType(mapType(superType)); + } + } + private void addGenericDeclarations( JavaClassSource source, List generics) { 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 2cad03f8..f49ab8b5 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 @@ -1147,13 +1147,13 @@ void shouldValidateNonNullFieldsInFromInstanceConstructor() { JavaFileObject notNullAnnotation = ProcessorTestUtils.forSource( """ - package jakarta.validation.constraints; - import java.lang.annotation.*; - @Retention(RetentionPolicy.RUNTIME) - @Target({ElementType.FIELD, ElementType.PARAMETER}) - public @interface NotNull { - String message() default ""; - } + package jakarta.validation.constraints; + import java.lang.annotation.*; + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.FIELD, ElementType.PARAMETER}) + public @interface NotNull { + String message() default ""; + } """); JavaFileObject sourceFile = @@ -1165,24 +1165,24 @@ void shouldValidateNonNullFieldsInFromInstanceConstructor() { @SimpleBuilder public class NonNullValidation { - private String requiredField; - private String optionalField; - private int primitiveField; + private String requiredField; + private String optionalField; + private int primitiveField; - public String getRequiredField() { return requiredField; } - public void setRequiredField(@NotNull String requiredField) { + public String getRequiredField() { return requiredField; } + public void setRequiredField(@NotNull String requiredField) { this.requiredField = requiredField; - } + } - public String getOptionalField() { return optionalField; } - public void setOptionalField(String optionalField) { + public String getOptionalField() { return optionalField; } + public void setOptionalField(String optionalField) { this.optionalField = optionalField; - } + } - public int getPrimitiveField() { return primitiveField; } - public void setPrimitiveField(int primitiveField) { + public int getPrimitiveField() { return primitiveField; } + public void setPrimitiveField(int primitiveField) { this.primitiveField = primitiveField; - } + } } """); From 104bc5e7dc728011ae7e49e29b96393bb24ecb3e Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 12 Apr 2026 00:11:12 +0200 Subject: [PATCH 11/30] Replacing usage of named Types by referencing the class --- .../processor/generators/builder/ConstructorEnhancer.java | 2 +- .../processor/generators/builder/CoreMethodsEnhancer.java | 4 ++-- .../generators/field/ArrayBuilderConsumerGenerator.java | 2 +- .../generators/field/OptionalHelperGenerator.java | 3 ++- .../generators/field/StringBuilderConsumerGenerator.java | 3 ++- .../generators/field/StringFormatHelperGenerator.java | 3 ++- .../generators/field/VarArgsHelperGenerator.java | 8 +++++--- 7 files changed, 15 insertions(+), 10 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ConstructorEnhancer.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ConstructorEnhancer.java index b1c4f144..1060c45d 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ConstructorEnhancer.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ConstructorEnhancer.java @@ -116,7 +116,7 @@ protected ConstructorDto createFromInstanceConstructor(BuilderDefinitionDto buil } """, getterName, fieldName, getterName); - codeDto.addCodeBlockImport(new TypeName("java.lang", "IllegalArgumentException")); + codeDto.addCodeBlockImport(TypeName.of(IllegalArgumentException.class)); } codeDto.append( """ diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java index 7d781324..9f70d621 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java @@ -196,7 +196,7 @@ protected MethodDto createBuildMethod(BuilderDefinitionDto builderDto) { method.setCode(code.toString()); method.addArgument("dtoBaseType", builderDto.getBuildingTargetTypeName()); method.addArgument("buildResultType", returnType); - method.addCodeBlockImport(new TypeName("java.lang", "IllegalStateException")); + method.addCodeBlockImport(TypeName.of(IllegalStateException.class)); method.setJavadoc(new JavadocDto("Builds the configured DTO instance.")); return method; @@ -238,7 +238,7 @@ protected MethodDto createStaticCreateMethod(BuilderDefinitionDto builderDto) { /** Creates the toString() method. */ protected MethodDto createToStringMethod(BuilderDefinitionDto builderDto) { - MethodDto method = new MethodDto("toString", new TypeName("java.lang", "String")); + MethodDto method = new MethodDto("toString", TypeName.of(String.class)); method.setOrdering(ORDERING_TO_STRING); method.setPriority(MethodDto.PRIORITY_HIGHEST); method.setModifier(AccessModifier.PUBLIC); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ArrayBuilderConsumerGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ArrayBuilderConsumerGenerator.java index d3b6dec5..a445ea69 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ArrayBuilderConsumerGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ArrayBuilderConsumerGenerator.java @@ -144,7 +144,7 @@ private MethodDto createFieldConsumerWithArrayBuilder( """); // Add code block import for java.util.List.of - methodDto.getMethodCodeDto().addCodeBlockImport(new TypeName("java.util", "List")); + methodDto.getMethodCodeDto().addCodeBlockImport(TypeName.of(List.class)); methodDto.addArgument("fieldName", fieldNameInBuilder); methodDto.addArgument("dtoMethodParam", parameter.getParameterName()); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/OptionalHelperGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/OptionalHelperGenerator.java index 9d8aa3a3..9c7a45fe 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/OptionalHelperGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/OptionalHelperGenerator.java @@ -28,6 +28,7 @@ import java.util.Collections; import java.util.List; +import java.util.Optional; import org.apache.commons.collections4.CollectionUtils; import org.javahelpers.simple.builders.processor.generators.MethodGenerator; import org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil; @@ -106,7 +107,7 @@ public List generateMethods( field, "Optional.ofNullable(%s)", innerType, builderType, context); // Add code block import for Optional.ofNullable - method.getMethodCodeDto().addCodeBlockImport(new TypeName("java.util", "Optional")); + method.getMethodCodeDto().addCodeBlockImport(TypeName.of(Optional.class)); return Collections.singletonList(method); } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringBuilderConsumerGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringBuilderConsumerGenerator.java index 438a1366..6506c695 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringBuilderConsumerGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringBuilderConsumerGenerator.java @@ -29,6 +29,7 @@ import static org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil.*; import java.util.List; +import java.util.Optional; import org.javahelpers.simple.builders.processor.generators.MethodGenerator; import org.javahelpers.simple.builders.processor.generators.util.JavadocConstants; import org.javahelpers.simple.builders.processor.model.core.FieldDto; @@ -121,7 +122,7 @@ public List generateMethods( // Add code block import for Optional.of when used if (isOptionalString(field.getFieldType())) { - method.getMethodCodeDto().addCodeBlockImport(new TypeName("java.util", "Optional")); + method.getMethodCodeDto().addCodeBlockImport(TypeName.of(Optional.class)); } return List.of(method); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringFormatHelperGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringFormatHelperGenerator.java index 7f402a58..f057bde2 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringFormatHelperGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringFormatHelperGenerator.java @@ -31,6 +31,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Optional; import org.apache.commons.collections4.CollectionUtils; import org.javahelpers.simple.builders.processor.generators.MethodGenerator; import org.javahelpers.simple.builders.processor.generators.util.JavadocConstants; @@ -141,7 +142,7 @@ public List generateMethods( context); // Add code block imports for Optional.of and String.format - method.getMethodCodeDto().addCodeBlockImport(new TypeName("java.util", "Optional")); + method.getMethodCodeDto().addCodeBlockImport(TypeName.of(Optional.class)); methods.add(method); } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/VarArgsHelperGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/VarArgsHelperGenerator.java index 1fa96c3f..9f983554 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/VarArgsHelperGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/VarArgsHelperGenerator.java @@ -26,6 +26,8 @@ import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.Set; import org.javahelpers.simple.builders.processor.generators.MethodGenerator; import org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil; import org.javahelpers.simple.builders.processor.model.core.FieldDto; @@ -157,11 +159,11 @@ private MethodDto createFieldSetterByVarArgs( // Add code block imports for collection factory methods if (fieldType instanceof TypeNameList) { - method.getMethodCodeDto().addCodeBlockImport(new TypeName("java.util", "List")); + method.getMethodCodeDto().addCodeBlockImport(TypeName.of(List.class)); } else if (fieldType instanceof TypeNameSet) { - method.getMethodCodeDto().addCodeBlockImport(new TypeName("java.util", "Set")); + method.getMethodCodeDto().addCodeBlockImport(TypeName.of(Set.class)); } else if (fieldType instanceof TypeNameMap) { - method.getMethodCodeDto().addCodeBlockImport(new TypeName("java.util", "Map")); + method.getMethodCodeDto().addCodeBlockImport(TypeName.of(Map.class)); } return method; From 3de08d5d6e35c3d1c889ffb4c1b11741fa58f4c9 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 12 Apr 2026 00:11:25 +0200 Subject: [PATCH 12/30] Improving codequality --- .../classgen/roaster/RoasterCodeGenerator.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java index 21afdeee..a1a6f5d8 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java @@ -197,9 +197,7 @@ private void appendField(JavaClassSource source, ClassFieldDto fieldDto) { field.setName(fieldDto.getFieldName()); field.setType(mapType(fieldDto.getFieldType())); applyVisibility(field, fieldDto.getVisibility()); - if (fieldDto.getLiteralInitializer() != null) { - field.setLiteralInitializer(fieldDto.getLiteralInitializer()); - } + applyLiteralInitializer(field, fieldDto.getLiteralInitializer()); applyJavadoc(field, fieldDto.getJavadoc()); } @@ -359,6 +357,13 @@ private void applySuperType(JavaClassSource source, TypeName superType) { } } + private void applyLiteralInitializer( + FieldSource field, String literalInitializer) { + if (literalInitializer != null) { + field.setLiteralInitializer(literalInitializer); + } + } + private void addGenericDeclarations( JavaClassSource source, List generics) { From dce5ca1e31801d249103c0ee3710d48e02bf37f4 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 12 Apr 2026 00:11:45 +0200 Subject: [PATCH 13/30] Setting visibility of fields on Private per default --- .../simple/builders/processor/model/core/ClassFieldDto.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/ClassFieldDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/ClassFieldDto.java index e23a5c72..590aaea0 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/ClassFieldDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/ClassFieldDto.java @@ -24,6 +24,8 @@ package org.javahelpers.simple.builders.processor.model.core; +import static org.javahelpers.simple.builders.core.enums.AccessModifier.PRIVATE; + import java.util.LinkedHashSet; import java.util.Set; import org.apache.commons.lang3.builder.ToStringBuilder; @@ -46,7 +48,7 @@ public class ClassFieldDto { private TypeName fieldType; /** Field visibility (typically PRIVATE). */ - private AccessModifier visibility; + private AccessModifier visibility = PRIVATE; /** Literal initializer (e.g., "exampleValue" like in {@code String field="exampleValue";}). */ private String literalInitializer; From c55c6365f311af40b4aa5a61b6679d245b34e343 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 12 Apr 2026 08:02:04 +0200 Subject: [PATCH 14/30] Improving handling of codeBlockImports in context of constructorDto and MethodDto --- .../roaster/RoasterCodeGenerator.java | 10 ++++-- .../builder/CoreMethodsEnhancer.java | 6 ++-- .../field/AddToCollectionGenerator.java | 2 +- .../model/method/ConstructorDto.java | 34 ++++++------------- .../processor/model/method/MethodCodeDto.java | 14 ++++++-- .../processor/model/method/MethodDto.java | 19 +++++------ 6 files changed, 43 insertions(+), 42 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java index a1a6f5d8..e62781ac 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java @@ -397,7 +397,7 @@ private void addGenericDeclarations( } private void applyCodeBlock(MethodSource method, MethodCodeDto codeDto) { - if (codeDto == null || StringUtils.isBlank(codeDto.getCodeFormat())) { + if (!codeDto.hasCode()) { // If implementation is missing, an empty body needs to be set method.setBody(""); return; @@ -560,6 +560,7 @@ private Set collectImports(GenerationTargetClassDto classDef) { .forEach( param -> addTypeImports(imports, currentPackage, param.getParameterType())); constructor + .getMethodCodeDto() .getCodeBlockImports() .forEach(typeName -> addTypeImports(imports, currentPackage, typeName)); }); @@ -587,6 +588,10 @@ private void addMethodImports(Set imports, String currentPackage, Method generic .getUpperBounds() .forEach(type -> addTypeImports(imports, currentPackage, type))); + method + .getMethodCodeDto() + .getCodeBlockImports() + .forEach(typeName -> addTypeImports(imports, currentPackage, typeName)); method .getAnnotations() .forEach(annotation -> addAnnotationImports(imports, currentPackage, annotation)); @@ -665,8 +670,7 @@ private void addImportIfNeeded(Set imports, String currentPackage, Strin } private void addBodyImports(Set imports, String currentPackage, MethodDto method) { - if (method.getMethodCodeDto() == null - || StringUtils.isBlank(method.getMethodCodeDto().getCodeFormat())) { + if (!method.hasCode()) { return; } for (MethodCodePlaceholder argument : method.getMethodCodeDto().getCodeArguments()) { diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java index 9f70d621..5c577d04 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java @@ -196,7 +196,7 @@ protected MethodDto createBuildMethod(BuilderDefinitionDto builderDto) { method.setCode(code.toString()); method.addArgument("dtoBaseType", builderDto.getBuildingTargetTypeName()); method.addArgument("buildResultType", returnType); - method.addCodeBlockImport(TypeName.of(IllegalStateException.class)); + method.getMethodCodeDto().addCodeBlockImport(TypeName.of(IllegalStateException.class)); method.setJavadoc(new JavadocDto("Builds the configured DTO instance.")); return method; @@ -260,8 +260,8 @@ protected MethodDto createToStringMethod(BuilderDefinitionDto builderDto) { method.addArgument("toStringBuilder", toStringBuilderType); method.addArgument("toStringStyle", toStringStyleType); - method.addCodeBlockImport(toStringBuilderType); - method.addCodeBlockImport(toStringStyleType); + method.getMethodCodeDto().addCodeBlockImport(toStringBuilderType); + method.getMethodCodeDto().addCodeBlockImport(toStringStyleType); method.setJavadoc( new JavadocDto( diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/AddToCollectionGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/AddToCollectionGenerator.java index fc02dca3..e835219b 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/AddToCollectionGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/AddToCollectionGenerator.java @@ -179,7 +179,7 @@ private MethodDto createAddToCollectionMethod( methodDto.addArgument("fieldName", fieldNameInBuilder); methodDto.addArgument("elementType", elementType); methodDto.addArgument("builderFieldWrapper", TRACKED_VALUE_TYPE); - methodDto.addCodeBlockImport(collectionImplType); + methodDto.getMethodCodeDto().addCodeBlockImport(collectionImplType); methodDto.setPriority(MethodDto.PRIORITY_MEDIUM); methodDto.setJavadoc( diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/ConstructorDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/ConstructorDto.java index d63b6069..4bfb4b09 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/ConstructorDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/ConstructorDto.java @@ -83,6 +83,15 @@ public void setMethodCodeDto(MethodCodeDto methodCodeDto) { this.methodCodeDto = methodCodeDto; } + /** + * Checks if the constructor has a code block. + * + * @return true if the constructor has a code block, false otherwise + */ + public boolean hasCode() { + return methodCodeDto.hasCode(); + } + /** * Gets the constructor body as a plain string (for backward compatibility). * @@ -91,7 +100,7 @@ public void setMethodCodeDto(MethodCodeDto methodCodeDto) { */ @Deprecated public String getBody() { - return methodCodeDto != null ? methodCodeDto.getCodeFormat() : null; + return hasCode() ? methodCodeDto.getCodeFormat() : null; } /** @@ -132,27 +141,6 @@ public void addAnnotation(AnnotationDto annotation) { this.annotations.add(annotation); } - /** - * Gets code block imports from the method code DTO. - * - * @return set of types used in code blocks - */ - public Set getCodeBlockImports() { - return methodCodeDto != null ? methodCodeDto.getCodeBlockImports() : new LinkedHashSet<>(); - } - - /** - * Adds a code block import to the method code DTO. - * - * @param typeName the type to import - */ - public void addCodeBlockImport(TypeName typeName) { - if (this.methodCodeDto == null) { - this.methodCodeDto = new MethodCodeDto(); - } - this.methodCodeDto.addCodeBlockImport(typeName); - } - /** * Returns a string representation of this constructor using Apache Commons ToStringBuilder. * @@ -166,7 +154,7 @@ public String toString() { return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) .append("parameters", parameters) .append("annotations", annotations) - .append("hasCodeBlock", methodCodeDto != null && methodCodeDto.getCodeFormat() != null) + .append("hasCodeBlock", hasCode()) .toString(); } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodCodeDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodCodeDto.java index a9e76de8..0f05021e 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodCodeDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodCodeDto.java @@ -69,6 +69,7 @@ public void addArgument(String name, String value) { */ public void addArgument(String name, TypeName value) { codeArguments.add(new MethodCodeTypePlaceholder(name, value)); + codeBlockImports.add(value); } /** @@ -91,9 +92,18 @@ public List> getCodeArguments() { } /** - * Getter for code block imports. + * Checks if this code DTO has code content. * - * @return types used in code body needing import + * @return true if code format is not null and not blank + */ + public boolean hasCode() { + return !StringUtils.isBlank(codeFormat); + } + + /** + * Returns the set of types used in the code body that aren't covered by arguments. + * + * @return set of types used in code body needing import */ public Set getCodeBlockImports() { return codeBlockImports; diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodDto.java index 4d602e62..de378164 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodDto.java @@ -205,6 +205,15 @@ public MethodCodeDto getMethodCodeDto() { return methodCodeDto; } + /** + * Checks if the constructor has a code block. + * + * @return true if the constructor has a code block, false otherwise + */ + public boolean hasCode() { + return methodCodeDto.hasCode(); + } + /** * Getting name of method. * @@ -378,16 +387,6 @@ public void addAnnotation(AnnotationDto annotation) { this.annotations.add(annotation); } - /** - * Adds an import for a type used in the method's code block. This is a convenience method that - * delegates to the underlying MethodCodeDto. - * - * @param typeName the type to import - */ - public void addCodeBlockImport(TypeName typeName) { - this.methodCodeDto.addCodeBlockImport(typeName); - } - /** * Comparator for sorting MethodDto instances with sophisticated ordering rules. * From 6473fcae718b27a3e47836777237835e43251d51 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 12 Apr 2026 08:14:49 +0200 Subject: [PATCH 15/30] Switching import-collecting and sorting into a helper class --- .../roaster/RoasterCodeGenerator.java | 186 +----------- .../model/method/ConstructorDto.java | 3 - .../processor/model/method/MethodDto.java | 2 +- .../processor/util/ImportCollector.java | 286 ++++++++++++++++++ 4 files changed, 292 insertions(+), 185 deletions(-) create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/util/ImportCollector.java diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java index e62781ac..8df589f7 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java @@ -54,17 +54,13 @@ import org.javahelpers.simple.builders.processor.model.javadoc.JavadocTagDto; import org.javahelpers.simple.builders.processor.model.method.ConstructorDto; import org.javahelpers.simple.builders.processor.model.method.MethodCodeDto; -import org.javahelpers.simple.builders.processor.model.method.MethodCodePlaceholder; -import org.javahelpers.simple.builders.processor.model.method.MethodCodeTypePlaceholder; import org.javahelpers.simple.builders.processor.model.method.MethodDto; import org.javahelpers.simple.builders.processor.model.method.MethodParameterDto; import org.javahelpers.simple.builders.processor.model.type.NestedTypeDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.model.type.TypeNameArray; -import org.javahelpers.simple.builders.processor.model.type.TypeNameGeneric; -import org.javahelpers.simple.builders.processor.model.type.TypeNamePrimitive; -import org.javahelpers.simple.builders.processor.model.type.TypeNameVariable; import org.javahelpers.simple.builders.processor.processing.ProcessingLogger; +import org.javahelpers.simple.builders.processor.util.ImportCollector; import org.jboss.forge.roaster.Roaster; import org.jboss.forge.roaster.model.source.AnnotationSource; import org.jboss.forge.roaster.model.source.FieldSource; @@ -508,185 +504,13 @@ private void addTrackedValueStaticImports(JavaClassSource source) { } private Set collectImports(GenerationTargetClassDto classDef) { - Set imports = new LinkedHashSet<>(); String currentPackage = classDef.getTypeName().getPackageName(); + ImportCollector collector = new ImportCollector(currentPackage); - // Add superclass imports - if (classDef.getSuperType() != null) { - addTypeImports(imports, currentPackage, classDef.getSuperType()); - } - - // Add class type and generics - addTypeImports(imports, currentPackage, classDef.getTypeName()); - classDef - .getGenerics() - .forEach( - generic -> - generic - .getUpperBounds() - .forEach(type -> addTypeImports(imports, currentPackage, type))); - - // Add interface imports - classDef - .getInterfaces() - .forEach(interfaceName -> addInterfaceImports(imports, currentPackage, interfaceName)); - - // Add class annotation imports - classDef - .getClassAnnotations() - .forEach(annotation -> addAnnotationImports(imports, currentPackage, annotation)); - - // Add field imports from ClassFieldDto - classDef - .getClassFields() - .forEach( - field -> { - addTypeImports(imports, currentPackage, field.getFieldType()); - field - .getFieldTypeImports() - .forEach(typeName -> addTypeImports(imports, currentPackage, typeName)); - }); - - // Add method imports - classDef.getMethods().forEach(method -> addMethodImports(imports, currentPackage, method)); - - // Add constructor imports - classDef - .getConstructors() - .forEach( - constructor -> { - constructor - .getParameters() - .forEach( - param -> addTypeImports(imports, currentPackage, param.getParameterType())); - constructor - .getMethodCodeDto() - .getCodeBlockImports() - .forEach(typeName -> addTypeImports(imports, currentPackage, typeName)); - }); - - // Add nested type imports - classDef - .getNestedTypes() - .forEach( - nestedType -> - nestedType - .getMethods() - .forEach(method -> addMethodImports(imports, currentPackage, method))); - - return imports; - } - - private void addMethodImports(Set imports, String currentPackage, MethodDto method) { - if (method.getReturnType() != null) { - addTypeImports(imports, currentPackage, method.getReturnType()); - } - method - .getGenericParameters() - .forEach( - generic -> - generic - .getUpperBounds() - .forEach(type -> addTypeImports(imports, currentPackage, type))); - method - .getMethodCodeDto() - .getCodeBlockImports() - .forEach(typeName -> addTypeImports(imports, currentPackage, typeName)); - method - .getAnnotations() - .forEach(annotation -> addAnnotationImports(imports, currentPackage, annotation)); - method - .getParameters() - .forEach(parameter -> addParameterImports(imports, currentPackage, parameter)); - addBodyImports(imports, currentPackage, method); - } - - private void addParameterImports( - Set imports, String currentPackage, MethodParameterDto parameter) { - addTypeImports(imports, currentPackage, parameter.getParameterType()); - parameter - .getAnnotations() - .forEach(annotation -> addAnnotationImports(imports, currentPackage, annotation)); - } - - private void addInterfaceImports( - Set imports, - String currentPackage, - org.javahelpers.simple.builders.processor.model.annotation.InterfaceName interfaceName) { - if (StringUtils.isNotBlank(interfaceName.getPackageName())) { - addImportIfNeeded( - imports, - currentPackage, - interfaceName.getPackageName() + "." + interfaceName.getSimpleName()); - } - interfaceName - .getAnnotations() - .forEach(annotation -> addAnnotationImports(imports, currentPackage, annotation)); - interfaceName - .getTypeParameters() - .forEach(type -> addTypeImports(imports, currentPackage, type)); - } - - private void addAnnotationImports( - Set imports, String currentPackage, AnnotationDto annotation) { - if (annotation.getAnnotationType() != null) { - addTypeImports(imports, currentPackage, annotation.getAnnotationType()); - } - } - - private void addTypeImports(Set imports, String currentPackage, TypeName type) { - if (type == null || type instanceof TypeNamePrimitive || type instanceof TypeNameVariable) { - return; - } - - type.getAnnotations() - .forEach(annotation -> addAnnotationImports(imports, currentPackage, annotation)); - - if (type instanceof TypeNameArray arrayType) { - addTypeImports(imports, currentPackage, arrayType.getTypeOfArray()); - return; - } - - if (type instanceof TypeNameGeneric genericType) { - addImportIfNeeded( - imports, currentPackage, genericType.getFullQualifiedName().replaceAll("<.*$", "")); - genericType - .getInnerTypeArguments() - .forEach(inner -> addTypeImports(imports, currentPackage, inner)); - return; - } - - addImportIfNeeded(imports, currentPackage, type.getFullQualifiedName()); - } + // Let the ImportCollector handle all extraction logic + collector.collectImports(classDef); - private void addImportIfNeeded(Set imports, String currentPackage, String fqn) { - if (StringUtils.isBlank(fqn) - || !fqn.contains(".") - || fqn.startsWith("java.lang.") - || java.util.Objects.equals(packageNameOf(fqn), currentPackage)) { - return; - } - imports.add(fqn); - } - - private void addBodyImports(Set imports, String currentPackage, MethodDto method) { - if (!method.hasCode()) { - return; - } - for (MethodCodePlaceholder argument : method.getMethodCodeDto().getCodeArguments()) { - if (argument instanceof MethodCodeTypePlaceholder typePlaceholder) { - addTypeImports(imports, currentPackage, typePlaceholder.getValue()); - } - } - String code = method.getMethodCodeDto().getCodeFormat(); - if (code.contains("List.of(")) { - imports.add(List.class.getName()); - } - if (code.contains("Optional.of(") - || code.contains("Optional.empty(") - || code.contains("Optional.ofNullable(")) { - imports.add(java.util.Optional.class.getName()); - } + return collector.getSortedImports(); } private String formatSource(String rawSource) { diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/ConstructorDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/ConstructorDto.java index 4bfb4b09..0f09e941 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/ConstructorDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/ConstructorDto.java @@ -25,16 +25,13 @@ package org.javahelpers.simple.builders.processor.model.method; import java.util.ArrayList; -import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; -import java.util.Set; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; import org.javahelpers.simple.builders.core.enums.AccessModifier; import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; -import org.javahelpers.simple.builders.processor.model.type.TypeName; /** * Dedicated constructor representation (separate from MethodDto). diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodDto.java index de378164..38a544a4 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodDto.java @@ -213,7 +213,7 @@ public MethodCodeDto getMethodCodeDto() { public boolean hasCode() { return methodCodeDto.hasCode(); } - + /** * Getting name of method. * diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ImportCollector.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ImportCollector.java new file mode 100644 index 00000000..87e86ef8 --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ImportCollector.java @@ -0,0 +1,286 @@ +package org.javahelpers.simple.builders.processor.util; + +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.TreeSet; +import org.apache.commons.lang3.StringUtils; +import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto; +import org.javahelpers.simple.builders.processor.model.annotation.InterfaceName; +import org.javahelpers.simple.builders.processor.model.core.GenerationTargetClassDto; +import org.javahelpers.simple.builders.processor.model.method.MethodCodePlaceholder; +import org.javahelpers.simple.builders.processor.model.method.MethodCodeTypePlaceholder; +import org.javahelpers.simple.builders.processor.model.method.MethodDto; +import org.javahelpers.simple.builders.processor.model.method.MethodParameterDto; +import org.javahelpers.simple.builders.processor.model.type.TypeName; +import org.javahelpers.simple.builders.processor.model.type.TypeNameArray; +import org.javahelpers.simple.builders.processor.model.type.TypeNameGeneric; +import org.javahelpers.simple.builders.processor.model.type.TypeNamePrimitive; +import org.javahelpers.simple.builders.processor.model.type.TypeNameVariable; + +/** + * Utility class for collecting, deduplicating, and sorting imports for generated classes. + * + *

This class centralizes all import-related logic that was previously scattered throughout + * RoasterCodeGenerator, providing a clean and consistent approach to import management. + */ +public class ImportCollector { + + private final Set imports = new LinkedHashSet<>(); + private final String currentPackage; + + /** + * Creates a new ImportCollector for the specified package. + * + * @param currentPackage the package of the class being generated + */ + public ImportCollector(String currentPackage) { + this.currentPackage = currentPackage; + } + + /** + * Adds all necessary imports for a type. + * + * @param type the type to add imports for + */ + public void addTypeImports(TypeName type) { + if (type == null || type instanceof TypeNamePrimitive || type instanceof TypeNameVariable) { + return; + } + + // Add type annotation imports + type.getAnnotations().forEach(this::addAnnotationImports); + + if (type instanceof TypeNameArray arrayType) { + addTypeImports(arrayType.getTypeOfArray()); + return; + } + + if (type instanceof TypeNameGeneric genericType) { + // Add the raw generic type (without type parameters) + String rawType = genericType.getFullQualifiedName().replaceAll("<.*$", ""); + addImport(rawType); + + // Add inner type arguments + genericType.getInnerTypeArguments().forEach(this::addTypeImports); + return; + } + + // Add the concrete type + addImport(type.getFullQualifiedName()); + } + + /** + * Adds imports for an annotation. + * + * @param annotation the annotation to add imports for + */ + public void addAnnotationImports(AnnotationDto annotation) { + if (annotation.getAnnotationType() != null) { + addTypeImports(annotation.getAnnotationType()); + } + } + + /** + * Adds imports for a method parameter. + * + * @param parameter the parameter to add imports for + */ + public void addParameterImports(MethodParameterDto parameter) { + addTypeImports(parameter.getParameterType()); + parameter.getAnnotations().forEach(this::addAnnotationImports); + } + + /** + * Adds imports for an interface. + * + * @param interfaceName the interface to add imports for + */ + public void addInterfaceImports(InterfaceName interfaceName) { + // Create a TypeName from the interface's package and simple name + TypeName interfaceType = + new TypeName(interfaceName.getPackageName(), interfaceName.getSimpleName()); + addTypeImports(interfaceType); + interfaceName.getAnnotations().forEach(this::addAnnotationImports); + interfaceName.getTypeParameters().forEach(this::addTypeImports); + } + + /** + * Adds imports for a method's body and components. + * + * @param method the method to add imports for + */ + public void addMethodImports(MethodDto method) { + if (method.getReturnType() != null) { + addTypeImports(method.getReturnType()); + } + + // Add generic parameter imports + method + .getGenericParameters() + .forEach(generic -> generic.getUpperBounds().forEach(this::addTypeImports)); + + // Add code block imports + method.getMethodCodeDto().getCodeBlockImports().forEach(this::addTypeImports); + + // Add annotation imports + method.getAnnotations().forEach(this::addAnnotationImports); + + // Add parameter imports + method.getParameters().forEach(this::addParameterImports); + + // Add body argument imports + addBodyImports(method); + } + + /** + * Collects all imports from a GenerationTargetClassDto. + * + * @param classDef the class definition to extract imports from + */ + public void collectImports(GenerationTargetClassDto classDef) { + // Add superclass imports + if (classDef.getSuperType() != null) { + addTypeImports(classDef.getSuperType()); + } + + // Add class type and generics + addTypeImports(classDef.getTypeName()); + classDef + .getGenerics() + .forEach(generic -> generic.getUpperBounds().forEach(this::addTypeImports)); + + // Add interface imports + classDef.getInterfaces().forEach(this::addInterfaceImports); + + // Add class annotation imports + classDef.getClassAnnotations().forEach(this::addAnnotationImports); + + // Add field imports from ClassFieldDto + classDef + .getClassFields() + .forEach( + field -> { + addTypeImports(field.getFieldType()); + field.getFieldTypeImports().forEach(this::addTypeImports); + }); + + // Add constructor imports + classDef + .getConstructors() + .forEach( + constructor -> { + constructor.getParameters().forEach(this::addParameterImports); + constructor.getMethodCodeDto().getCodeBlockImports().forEach(this::addTypeImports); + }); + + // Add nested type imports + classDef + .getNestedTypes() + .forEach( + nestedType -> { + addTypeImports( + new TypeName(classDef.getTypeName().getPackageName(), nestedType.getTypeName())); + // NestedTypeDto doesn't have type parameters, so we skip that part + }); + + // Add method imports + classDef.getMethods().forEach(this::addMethodImports); + } + + /** + * Adds imports from method body arguments. + * + * @param method the method to analyze + */ + private void addBodyImports(MethodDto method) { + if (!method.hasCode()) { + return; + } + + for (MethodCodePlaceholder argument : method.getMethodCodeDto().getCodeArguments()) { + if (argument instanceof MethodCodeTypePlaceholder typePlaceholder) { + addTypeImports(typePlaceholder.getValue()); + } + } + + // Extract type references from code format (simplified approach) + String code = method.getMethodCodeDto().getCodeFormat(); + extractTypeReferencesFromCode(code); + } + + /** + * Extracts type references from code format string. + * + * @param code the code to analyze + */ + private void extractTypeReferencesFromCode(String code) { + if (StringUtils.isBlank(code)) { + return; + } + + // Skip automatic extraction for now to avoid invalid identifiers + // The explicit imports from code arguments and code block imports should be sufficient + // This could be enhanced later with more sophisticated parsing + } + + /** + * Adds an import if it's not in the current package and not already added. + * + * @param fqn the fully qualified name to add + */ + private void addImport(String fqn) { + if (StringUtils.isBlank(fqn)) { + return; + } + + // Skip if it's in the current package + if (fqn.startsWith(currentPackage)) { + return; + } + + // Skip java.lang package (auto-imported) + if (fqn.startsWith("java.lang.")) { + return; + } + + imports.add(fqn); + } + + /** + * Returns the collected imports as a sorted set. + * + *

Use this method when you want imports in alphabetical order. The sorting is done here to + * provide flexibility for the calling code. + * + * @return sorted set of import statements + */ + public Set getSortedImports() { + return new TreeSet<>(imports); + } + + /** + * Returns the collected imports in insertion order. + * + *

This preserves the order in which imports were added, which can be useful for maintaining a + * specific import ordering strategy. + * + * @return set of import statements in insertion order + */ + public Set getImports() { + return new LinkedHashSet<>(imports); + } + + /** Clears all collected imports. */ + public void clear() { + imports.clear(); + } + + /** + * Returns the number of collected imports. + * + * @return the number of imports + */ + public int size() { + return imports.size(); + } +} From 9cd014d6e3b4e9b86c3400fa444e6c3e7225c51f Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 12 Apr 2026 19:53:23 +0200 Subject: [PATCH 16/30] Fixing issue of missing NonNull import in testclasses --- .../builders/processor/util/ImportCollector.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ImportCollector.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ImportCollector.java index 87e86ef8..e8729d70 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ImportCollector.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ImportCollector.java @@ -233,9 +233,14 @@ private void addImport(String fqn) { return; } - // Skip if it's in the current package - if (fqn.startsWith(currentPackage)) { - return; + // Skip if it's in the exact same package (but not sub-packages) + // Extract the package from the FQN + int lastDot = fqn.lastIndexOf('.'); + if (lastDot > 0) { + String fqnPackage = fqn.substring(0, lastDot); + if (fqnPackage.equals(currentPackage)) { + return; + } } // Skip java.lang package (auto-imported) From b95baab5174195b1f1f918df03b14aa45898c1bc Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 12 Apr 2026 20:56:21 +0200 Subject: [PATCH 17/30] Implementation of ImportStatement dto to handle static and regular imports --- .../roaster/RoasterCodeGenerator.java | 55 +++--- .../processor/model/core/ClassFieldDto.java | 49 +++++- .../model/core/GenerationTargetClassDto.java | 55 +++++- .../model/imports/ImportStatement.java | 61 +++++++ .../model/imports/RegularImport.java | 97 +++++++++++ .../processor/model/imports/StaticImport.java | 115 +++++++++++++ .../processor/model/method/MethodCodeDto.java | 54 +++++- .../processing/BuilderDefinitionCreator.java | 9 +- .../processor/util/ImportCollector.java | 157 +++++++++++++----- .../ComprehensiveFeatureIntegrationTest.java | 2 - 10 files changed, 566 insertions(+), 88 deletions(-) create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/model/imports/ImportStatement.java create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/model/imports/RegularImport.java create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/model/imports/StaticImport.java diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java index 8df589f7..eca64086 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java @@ -31,7 +31,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.Writer; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Properties; @@ -42,12 +41,12 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.javahelpers.simple.builders.core.enums.AccessModifier; -import org.javahelpers.simple.builders.core.util.TrackedValue; import org.javahelpers.simple.builders.processor.exceptions.BuilderException; import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto; import org.javahelpers.simple.builders.processor.model.annotation.InterfaceName; import org.javahelpers.simple.builders.processor.model.core.ClassFieldDto; import org.javahelpers.simple.builders.processor.model.core.GenerationTargetClassDto; +import org.javahelpers.simple.builders.processor.model.imports.ImportStatement; import org.javahelpers.simple.builders.processor.model.integration.JacksonModuleDefinitionDto; import org.javahelpers.simple.builders.processor.model.integration.JacksonModuleEntryDto; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; @@ -151,10 +150,15 @@ private JavaClassSource createJavaClassSource(GenerationTargetClassDto classDef) } source.setName(classDef.getTypeName().getClassName()); addGenericDeclarations(source, classDef.getGenerics()); - addTrackedValueStaticImports(source); // Collect and add imports early (before elements are added) - collectImports(classDef).stream().sorted().forEach(source::addImport); + for (ImportStatement importStmt : collectImports(classDef)) { + if (importStmt.isStatic()) { + source.addImport(importStmt.getFullyQualifiedName()).setStatic(true); + } else { + source.addImport(importStmt.getFullyQualifiedName()); + } + } logger.debug("JavaClassSource created"); return source; @@ -497,17 +501,10 @@ private void addParameter( applyAnnotations(parameter, paramDto.getAnnotations()); } - private void addTrackedValueStaticImports(JavaClassSource source) { - source.addImport(TrackedValue.class.getName() + ".changedValue").setStatic(true); - source.addImport(TrackedValue.class.getName() + ".initialValue").setStatic(true); - source.addImport(TrackedValue.class.getName() + ".unsetValue").setStatic(true); - } + private Set collectImports(GenerationTargetClassDto classDef) { + ImportCollector collector = new ImportCollector(classDef.getTypeName()); - private Set collectImports(GenerationTargetClassDto classDef) { - String currentPackage = classDef.getTypeName().getPackageName(); - ImportCollector collector = new ImportCollector(currentPackage); - - // Let the ImportCollector handle all extraction logic + // ImportCollector handles all import extraction from the DTO collector.collectImports(classDef); return collector.getSortedImports(); @@ -623,23 +620,35 @@ public void generateJacksonModule(JacksonModuleDefinitionDto moduleDef) { moduleClass.setName(moduleClassName); moduleClass.setSuperType("SimpleModule"); - // Collect imports in a list, sort them, then add to Roaster - Set imports = new LinkedHashSet<>(); - imports.add("com.fasterxml.jackson.databind.annotation.JsonDeserialize"); - imports.add("com.fasterxml.jackson.databind.module.SimpleModule"); + // Collect imports using ImportCollector + TypeName moduleType = new TypeName(packageName, moduleClassName); + ImportCollector collector = new ImportCollector(moduleType); + + // Add Jackson-specific imports using convenience method with TypeName + collector.addTypeImports( + new org.javahelpers.simple.builders.processor.model.type.TypeName( + "com.fasterxml.jackson.databind.annotation", "JsonDeserialize")); + collector.addTypeImports( + new org.javahelpers.simple.builders.processor.model.type.TypeName( + "com.fasterxml.jackson.databind.module", "SimpleModule")); // Adding imports for DTO types moduleDef.getEntries().stream() .filter(e -> shouldAddImport(packageName, e.dtoType().getFullQualifiedName())) - .forEach(e -> imports.add(e.dtoType().getFullQualifiedName())); + .forEach(e -> collector.addTypeImports(e.dtoType())); // Adding imports for builder types moduleDef.getEntries().stream() .filter(e -> shouldAddImport(packageName, e.builderType().getFullQualifiedName())) - .forEach(e -> imports.add(e.builderType().getFullQualifiedName())); - - // Sort imports and add to Roaster - imports.stream().sorted().forEach(moduleClass::addImport); + .forEach(e -> collector.addTypeImports(e.builderType())); + + // Add sorted imports to Roaster + collector + .getSortedImports() + .forEach( + importStatement -> { + moduleClass.addImport(importStatement.getFullyQualifiedName()); + }); // Add mixin interfaces as nested interfaces for (JacksonModuleEntryDto entry : moduleDef.getEntries()) { diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/ClassFieldDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/ClassFieldDto.java index 590aaea0..04c694e3 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/ClassFieldDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/ClassFieldDto.java @@ -31,6 +31,9 @@ import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; import org.javahelpers.simple.builders.core.enums.AccessModifier; +import org.javahelpers.simple.builders.processor.model.imports.ImportStatement; +import org.javahelpers.simple.builders.processor.model.imports.RegularImport; +import org.javahelpers.simple.builders.processor.model.imports.StaticImport; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; @@ -57,7 +60,7 @@ public class ClassFieldDto { private JavadocDto javadoc; /** Types to import for this field (e.g., TrackedValue, the boxed inner type). */ - private final Set fieldTypeImports = new LinkedHashSet<>(); + private final Set fieldTypeImports = new LinkedHashSet<>(); public String getFieldName() { return fieldName; @@ -99,12 +102,52 @@ public void setJavadoc(JavadocDto javadoc) { this.javadoc = javadoc; } - public Set getFieldTypeImports() { + public Set getFieldTypeImports() { return fieldTypeImports; } public void addFieldTypeImport(TypeName typeName) { - this.fieldTypeImports.add(typeName); + this.fieldTypeImports.add(new RegularImport(typeName)); + } + + // Convenience methods for adding imports without creating DTOs + + /** + * Adds a regular import for this field (convenience method accepting Class). + * + * @param clazz the class to import + */ + public void addImport(Class clazz) { + this.fieldTypeImports.add(new RegularImport(TypeName.of(clazz))); + } + + /** + * Adds a regular import for this field (convenience method accepting TypeName). + * + * @param typeName the type to import + */ + public void addImport(TypeName typeName) { + this.fieldTypeImports.add(new RegularImport(typeName)); + } + + /** + * Adds a static import for this field (convenience method accepting Class and member name). + * + * @param clazz the class containing the static member + * @param memberName the name of the static member + */ + public void addStaticImport(Class clazz, String memberName) { + this.fieldTypeImports.add(new StaticImport(TypeName.of(clazz), memberName)); + } + + /** + * Adds a static import for this field (convenience method accepting TypeName and member name). + * + * @param type the type containing the static member + * @param memberName the name of the static member + */ + public void addStaticImport(TypeName type, String memberName) { + this.fieldTypeImports.add(new StaticImport(type, memberName)); } /** diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/GenerationTargetClassDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/GenerationTargetClassDto.java index 970fcd0f..f3a92066 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/GenerationTargetClassDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/GenerationTargetClassDto.java @@ -31,6 +31,9 @@ import org.javahelpers.simple.builders.core.enums.AccessModifier; import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto; import org.javahelpers.simple.builders.processor.model.annotation.InterfaceName; +import org.javahelpers.simple.builders.processor.model.imports.ImportStatement; +import org.javahelpers.simple.builders.processor.model.imports.RegularImport; +import org.javahelpers.simple.builders.processor.model.imports.StaticImport; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; import org.javahelpers.simple.builders.processor.model.method.ConstructorDto; import org.javahelpers.simple.builders.processor.model.method.MethodDto; @@ -66,8 +69,8 @@ public class GenerationTargetClassDto { /** Generic parameters declared on the target DTO (e.g., {@code }). */ private final List generics = new LinkedList<>(); - /** Static import FQNs (e.g., TrackedValue.changedValue). */ - private final Set staticImports = new LinkedHashSet<>(); + /** Imports (both regular and static imports). */ + private final Set imports = new LinkedHashSet<>(); /** * Nested types (interfaces or classes) to be generated inside the builder, such as the "With" @@ -142,12 +145,52 @@ public void addMethod(MethodDto method) { this.methods.add(method); } - public Set getStaticImports() { - return staticImports; + public Set getImports() { + return imports; } - public void addStaticImport(String staticImport) { - this.staticImports.add(staticImport); + public void addImport(ImportStatement importStatement) { + this.imports.add(importStatement); + } + + // Convenience methods for adding imports without creating DTOs + + /** + * Adds a regular import (convenience method accepting Class). + * + * @param clazz the class to import + */ + public void addImport(Class clazz) { + addImport(new RegularImport(TypeName.of(clazz))); + } + + /** + * Adds a regular import (convenience method accepting TypeName). + * + * @param typeName the type to import + */ + public void addImport(TypeName typeName) { + addImport(new RegularImport(typeName)); + } + + /** + * Adds a static import (convenience method accepting Class and member name). + * + * @param clazz the class containing the static member + * @param memberName the name of the static member + */ + public void addStaticImport(Class clazz, String memberName) { + addImport(new StaticImport(TypeName.of(clazz), memberName)); + } + + /** + * Adds a static import (convenience method accepting TypeName and member name). + * + * @param type the type containing the static member + * @param memberName the name of the static member + */ + public void addStaticImport(TypeName type, String memberName) { + addImport(new StaticImport(type, memberName)); } /** diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/imports/ImportStatement.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/imports/ImportStatement.java new file mode 100644 index 00000000..813306b7 --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/imports/ImportStatement.java @@ -0,0 +1,61 @@ +/* + * MIT License + * + * Copyright (c) 2026 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.processor.model.imports; + +/** + * Represents an import statement in generated code. + * + *

This is the base interface for all import types (regular and static imports). This is a pure + * data holder - business logic for filtering imports belongs in ImportCollector. + */ +public interface ImportStatement { + + /** + * Returns the fully qualified name for this import. + * + *

For regular imports: {@code com.example.MyClass} + * + *

For static imports: {@code com.example.MyClass.staticMethod} + * + * @return the fully qualified name + */ + String getFullyQualifiedName(); + + /** + * Returns the package name for this import. + * + *

Extracted from the fully qualified name by taking everything before the last dot. + * + * @return the package name, or empty string if no package + */ + String getPackageName(); + + /** + * Checks if this is a static import. + * + * @return true if this is a static import, false for regular imports + */ + boolean isStatic(); +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/imports/RegularImport.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/imports/RegularImport.java new file mode 100644 index 00000000..b86be72a --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/imports/RegularImport.java @@ -0,0 +1,97 @@ +/* + * MIT License + * + * Copyright (c) 2026 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.processor.model.imports; + +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import org.javahelpers.simple.builders.processor.model.type.TypeName; + +/** + * Represents a regular (non-static) import statement. + * + *

Example: {@code import com.example.MyClass;} + */ +public class RegularImport implements ImportStatement { + + private final TypeName type; + + /** + * Creates a regular import from a TypeName. + * + * @param type the type to import + */ + public RegularImport(TypeName type) { + if (type == null) { + throw new IllegalArgumentException("type cannot be null"); + } + this.type = type; + } + + /** + * Returns the type being imported. + * + * @return the type + */ + public TypeName getType() { + return type; + } + + @Override + public String getFullyQualifiedName() { + return type.getFullQualifiedName(); + } + + @Override + public String getPackageName() { + return type.getPackageName(); + } + + @Override + public boolean isStatic() { + return false; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RegularImport that = (RegularImport) o; + return new EqualsBuilder().append(type, that.type).isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37).append(type).toHashCode(); + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) + .append("fqn", getFullyQualifiedName()) + .toString(); + } +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/imports/StaticImport.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/imports/StaticImport.java new file mode 100644 index 00000000..307f66ff --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/imports/StaticImport.java @@ -0,0 +1,115 @@ +/* + * MIT License + * + * Copyright (c) 2026 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.processor.model.imports; + +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import org.javahelpers.simple.builders.processor.model.type.TypeName; + +/** + * Represents a static import statement. + * + *

Example: {@code import static com.example.MyClass.staticMethod;} + */ +public class StaticImport implements ImportStatement { + + private final TypeName type; + private final String memberName; + + /** + * Creates a static import. + * + * @param type the type containing the static member + * @param memberName the name of the static member (method or field) + */ + public StaticImport(TypeName type, String memberName) { + if (type == null) { + throw new IllegalArgumentException("type cannot be null"); + } + if (memberName == null) { + throw new IllegalArgumentException("memberName cannot be null"); + } + this.type = type; + this.memberName = memberName; + } + + /** + * Returns the type containing the static member. + * + * @return the type + */ + public TypeName getType() { + return type; + } + + /** + * Returns the name of the static member. + * + * @return the member name + */ + public String getMemberName() { + return memberName; + } + + @Override + public String getFullyQualifiedName() { + return type.getFullQualifiedName() + "." + memberName; + } + + @Override + public String getPackageName() { + return type.getPackageName(); + } + + @Override + public boolean isStatic() { + return true; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + StaticImport that = (StaticImport) o; + return new EqualsBuilder() + .append(type, that.type) + .append(memberName, that.memberName) + .isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37).append(type).append(memberName).toHashCode(); + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) + .append("fqn", getFullyQualifiedName()) + .toString(); + } +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodCodeDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodCodeDto.java index 0f05021e..2bf8b7b7 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodCodeDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodCodeDto.java @@ -29,7 +29,11 @@ import java.util.List; import java.util.Set; import org.apache.commons.lang3.StringUtils; +import org.javahelpers.simple.builders.processor.model.imports.ImportStatement; +import org.javahelpers.simple.builders.processor.model.imports.RegularImport; +import org.javahelpers.simple.builders.processor.model.imports.StaticImport; import org.javahelpers.simple.builders.processor.model.type.TypeName; +import org.javahelpers.simple.builders.processor.model.type.TypeNameGeneric; /** DTO for holding information of code implementation. */ public class MethodCodeDto { @@ -40,7 +44,7 @@ public class MethodCodeDto { private final List> codeArguments = new ArrayList<>(); /** Types used in the code body that aren't covered by arguments. */ - private final Set codeBlockImports = new LinkedHashSet<>(); + private final Set codeBlockImports = new LinkedHashSet<>(); /** * Setting format of code. @@ -69,7 +73,7 @@ public void addArgument(String name, String value) { */ public void addArgument(String name, TypeName value) { codeArguments.add(new MethodCodeTypePlaceholder(name, value)); - codeBlockImports.add(value); + codeBlockImports.add(new RegularImport(value)); } /** @@ -101,11 +105,11 @@ public boolean hasCode() { } /** - * Returns the set of types used in the code body that aren't covered by arguments. + * Returns the set of imports used in the code body that aren't covered by arguments. * - * @return set of types used in code body needing import + * @return set of imports used in code body */ - public Set getCodeBlockImports() { + public Set getCodeBlockImports() { return codeBlockImports; } @@ -115,7 +119,45 @@ public Set getCodeBlockImports() { * @param typeName type to import */ public void addCodeBlockImport(TypeName typeName) { - this.codeBlockImports.add(typeName); + this.codeBlockImports.add(new RegularImport(typeName)); + } + + /** + * Adds a regular import for a type used in the code block (convenience method accepting Class). + * + * @param clazz the class to import + */ + public void addImport(Class clazz) { + addCodeBlockImport(TypeName.of(clazz)); + } + + /** + * Adds a static import for a method/field used in the code block (convenience method). + * + * @param clazz the class containing the static member + * @param memberName the name of the static member + */ + public void addStaticImport(Class clazz, String memberName) { + this.codeBlockImports.add(new StaticImport(TypeName.of(clazz), memberName)); + } + + /** + * Adds type imports for a TypeName and its generic type arguments (convenience method). + * + * @param type the type to add imports for + */ + public void addTypeImports(TypeName type) { + if (type == null) { + return; + } + + // Add the main type + addCodeBlockImport(type); + + // Add generic type arguments recursively + if (type instanceof TypeNameGeneric genericType) { + genericType.getInnerTypeArguments().forEach(this::addTypeImports); + } } /** diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java index 9964b3f6..6c6e6ed6 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java @@ -41,6 +41,7 @@ import org.apache.commons.lang3.Strings; import org.javahelpers.simple.builders.core.annotations.IgnoreInBuilder; import org.javahelpers.simple.builders.core.enums.AccessModifier; +import org.javahelpers.simple.builders.core.util.TrackedValue; import org.javahelpers.simple.builders.processor.analysis.FieldAnnotationExtractor; import org.javahelpers.simple.builders.processor.analysis.JavaLangAnalyser; import org.javahelpers.simple.builders.processor.analysis.JavaLangMapper; @@ -157,11 +158,9 @@ private static void finalizeDefinition( builderDto.setClassAccessModifier(builderDto.getConfiguration().getBuilderAccess()); // 4. Set static imports for TrackedValue - builderDto.addStaticImport( - "org.javahelpers.simple.builders.core.util.TrackedValue.changedValue"); - builderDto.addStaticImport( - "org.javahelpers.simple.builders.core.util.TrackedValue.initialValue"); - builderDto.addStaticImport("org.javahelpers.simple.builders.core.util.TrackedValue.unsetValue"); + builderDto.addStaticImport(TrackedValue.class, "changedValue"); + builderDto.addStaticImport(TrackedValue.class, "initialValue"); + builderDto.addStaticImport(TrackedValue.class, "unsetValue"); context.debugEndOperation( "Finalized: %d class fields, %d methods, %d constructors", diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ImportCollector.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ImportCollector.java index e8729d70..e31646a8 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ImportCollector.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ImportCollector.java @@ -1,12 +1,17 @@ package org.javahelpers.simple.builders.processor.util; +import java.util.Comparator; import java.util.LinkedHashSet; import java.util.Set; -import java.util.TreeSet; +import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Strings; import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto; import org.javahelpers.simple.builders.processor.model.annotation.InterfaceName; import org.javahelpers.simple.builders.processor.model.core.GenerationTargetClassDto; +import org.javahelpers.simple.builders.processor.model.imports.ImportStatement; +import org.javahelpers.simple.builders.processor.model.imports.RegularImport; +import org.javahelpers.simple.builders.processor.model.imports.StaticImport; import org.javahelpers.simple.builders.processor.model.method.MethodCodePlaceholder; import org.javahelpers.simple.builders.processor.model.method.MethodCodeTypePlaceholder; import org.javahelpers.simple.builders.processor.model.method.MethodDto; @@ -22,23 +27,35 @@ * *

This class centralizes all import-related logic that was previously scattered throughout * RoasterCodeGenerator, providing a clean and consistent approach to import management. + * + *

Uses {@link ImportStatement} objects internally to represent both regular and static imports, + * providing type safety and eliminating string-based import handling. */ public class ImportCollector { - private final Set imports = new LinkedHashSet<>(); - private final String currentPackage; + private final Set imports = new LinkedHashSet<>(); + private final TypeName currentType; + + /** + * Creates a new ImportCollector for the specified type. + * + * @param currentType the type of the class being generated + */ + public ImportCollector(TypeName currentType) { + this.currentType = currentType; + } /** - * Creates a new ImportCollector for the specified package. + * Adds a regular import (convenience method accepting Class). * - * @param currentPackage the package of the class being generated + * @param clazz the class to import */ - public ImportCollector(String currentPackage) { - this.currentPackage = currentPackage; + public void addImport(Class clazz) { + addImport(TypeName.of(clazz)); } /** - * Adds all necessary imports for a type. + * Adds imports for a type. * * @param type the type to add imports for */ @@ -57,8 +74,7 @@ public void addTypeImports(TypeName type) { if (type instanceof TypeNameGeneric genericType) { // Add the raw generic type (without type parameters) - String rawType = genericType.getFullQualifiedName().replaceAll("<.*$", ""); - addImport(rawType); + addImport(type); // Add inner type arguments genericType.getInnerTypeArguments().forEach(this::addTypeImports); @@ -66,7 +82,7 @@ public void addTypeImports(TypeName type) { } // Add the concrete type - addImport(type.getFullQualifiedName()); + addImport(type); } /** @@ -120,7 +136,7 @@ public void addMethodImports(MethodDto method) { .forEach(generic -> generic.getUpperBounds().forEach(this::addTypeImports)); // Add code block imports - method.getMethodCodeDto().getCodeBlockImports().forEach(this::addTypeImports); + method.getMethodCodeDto().getCodeBlockImports().forEach(this::addImport); // Add annotation imports method.getAnnotations().forEach(this::addAnnotationImports); @@ -138,6 +154,9 @@ public void addMethodImports(MethodDto method) { * @param classDef the class definition to extract imports from */ public void collectImports(GenerationTargetClassDto classDef) { + // Add imports from the DTO (including static imports like TrackedValue) + classDef.getImports().forEach(this::addImport); + // Add superclass imports if (classDef.getSuperType() != null) { addTypeImports(classDef.getSuperType()); @@ -161,7 +180,7 @@ public void collectImports(GenerationTargetClassDto classDef) { .forEach( field -> { addTypeImports(field.getFieldType()); - field.getFieldTypeImports().forEach(this::addTypeImports); + field.getFieldTypeImports().forEach(this::addImport); }); // Add constructor imports @@ -170,18 +189,11 @@ public void collectImports(GenerationTargetClassDto classDef) { .forEach( constructor -> { constructor.getParameters().forEach(this::addParameterImports); - constructor.getMethodCodeDto().getCodeBlockImports().forEach(this::addTypeImports); + constructor.getMethodCodeDto().getCodeBlockImports().forEach(this::addImport); }); - // Add nested type imports - classDef - .getNestedTypes() - .forEach( - nestedType -> { - addTypeImports( - new TypeName(classDef.getTypeName().getPackageName(), nestedType.getTypeName())); - // NestedTypeDto doesn't have type parameters, so we skip that part - }); + // Nested types are part of the same class and don't need imports + // (e.g., the "With" interface is nested within the builder class) // Add method imports classDef.getMethods().forEach(this::addMethodImports); @@ -224,54 +236,113 @@ private void extractTypeReferencesFromCode(String code) { } /** - * Adds an import if it's not in the current package and not already added. + * Adds an import statement if it should not be skipped. * - * @param fqn the fully qualified name to add + *

This method contains the business logic for filtering imports. + * + * @param importStatement the import statement to add */ - private void addImport(String fqn) { - if (StringUtils.isBlank(fqn)) { + private void addImport(ImportStatement importStatement) { + if (importStatement == null || shouldSkipImport(importStatement)) { return; } + imports.add(importStatement); + } + + /** + * Determines if an import should be skipped (business logic). + * + * @param importStatement the import to check + * @return true if the import should be skipped + */ + private boolean shouldSkipImport(ImportStatement importStatement) { + String fqn = importStatement.getFullyQualifiedName(); + + if (StringUtils.isBlank(fqn)) { + return true; + } + + // For regular imports, skip if in exact same package or java.lang (per Java rules) + if (!importStatement.isStatic()) { + // Skip if it's in the exact same package (but not sub-packages) - Java doesn't require + // imports for same-package classes + if (Strings.CI.equals(importStatement.getPackageName(), currentType.getPackageName())) { + return true; + } - // Skip if it's in the exact same package (but not sub-packages) - // Extract the package from the FQN - int lastDot = fqn.lastIndexOf('.'); - if (lastDot > 0) { - String fqnPackage = fqn.substring(0, lastDot); - if (fqnPackage.equals(currentPackage)) { - return; + // Skip java.lang package (auto-imported), but NOT subpackages like java.lang.annotation + if (Strings.CI.equals(importStatement.getPackageName(), "java.lang")) { + return true; } } - // Skip java.lang package (auto-imported) - if (fqn.startsWith("java.lang.")) { - return; + // Static imports are never skipped based on package + // (they're always needed to use the static member without qualification) + return false; + } + + /** + * Adds a regular import for a type. + * + * @param type the type to import + */ + private void addImport(TypeName type) { + if (type != null) { + addImport(new RegularImport(type)); } + } - imports.add(fqn); + /** + * Adds a static import. + * + * @param type the type containing the static member + * @param memberName the name of the static member + */ + public void addStaticImport(TypeName type, String memberName) { + addImport(new StaticImport(type, memberName)); } /** - * Returns the collected imports as a sorted set. + * Adds a static import (convenience method accepting Class). + * + * @param clazz the class containing the static member + * @param memberName the name of the static member + */ + public void addStaticImport(Class clazz, String memberName) { + addStaticImport(TypeName.of(clazz), memberName); + } + + /** + * Returns the collected import statements as a sorted set. * *

Use this method when you want imports in alphabetical order. The sorting is done here to * provide flexibility for the calling code. * + *

Static imports are placed before regular imports, following Java conventions. Within each + * group, imports are sorted alphabetically by fully qualified name. + * * @return sorted set of import statements */ - public Set getSortedImports() { - return new TreeSet<>(imports); + public Set getSortedImports() { + Comparator staticFirstComparator = + Comparator.comparing(ImportStatement::isStatic).reversed(); + Comparator alphabeticalComparator = + Comparator.comparing(ImportStatement::getFullyQualifiedName); + + return imports.stream() + .sorted(staticFirstComparator.thenComparing(alphabeticalComparator)) + .collect(Collectors.toCollection(LinkedHashSet::new)); } /** - * Returns the collected imports in insertion order. + * Returns the collected import statements in insertion order. * *

This preserves the order in which imports were added, which can be useful for maintaining a * specific import ordering strategy. * * @return set of import statements in insertion order */ - public Set getImports() { + public Set getImports() { return new LinkedHashSet<>(imports); } diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/ComprehensiveFeatureIntegrationTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/ComprehensiveFeatureIntegrationTest.java index 5d4ccd8b..fe117168 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/ComprehensiveFeatureIntegrationTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/ComprehensiveFeatureIntegrationTest.java @@ -851,8 +851,6 @@ default PersonDtoBuilder with() { } """; - // For now, just use the actual generated code as the expected template - // This ensures the test passes and will catch any future changes ProcessorAsserts.assertNormalizedEquals( expectedCode, generatedCode, From 4edc00fabadab86c43c650076695af346f7ba0da Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Tue, 21 Apr 2026 20:36:29 +0200 Subject: [PATCH 18/30] Adepting test-expectation of full generation --- .../ComprehensiveFeatureIntegrationTest.java | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/ComprehensiveFeatureIntegrationTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/ComprehensiveFeatureIntegrationTest.java index fe117168..4cd832b2 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/ComprehensiveFeatureIntegrationTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/ComprehensiveFeatureIntegrationTest.java @@ -349,8 +349,8 @@ public PersonDtoBuilder address(AddressDto address) { */ public PersonDtoBuilder address(Consumer addressBuilderConsumer) { AddressDtoBuilder builder = this.address.isSet() - ? new AddressDtoBuilder(this.address.value()) - : new AddressDtoBuilder(); + ? new AddressDtoBuilder(this.address.value()) + : new AddressDtoBuilder(); addressBuilderConsumer.accept(builder); this.address = changedValue(builder.build()); return this; @@ -478,8 +478,8 @@ public PersonDtoBuilder metadata(Map metadata) { */ public PersonDtoBuilder metadata(Consumer> metadataBuilderConsumer) { HashMapBuilder builder = this.metadata.isSet() - ? new HashMapBuilder(this.metadata.value()) - : new HashMapBuilder(); + ? new HashMapBuilder(this.metadata.value()) + : new HashMapBuilder(); metadataBuilderConsumer.accept(builder); this.metadata = changedValue(builder.build()); return this; @@ -574,8 +574,8 @@ public PersonDtoBuilder nicknames(List nicknames) { */ public PersonDtoBuilder nicknames(Consumer> nicknamesBuilderConsumer) { ArrayListBuilder builder = this.nicknames.isSet() - ? new ArrayListBuilder(this.nicknames.value()) - : new ArrayListBuilder(); + ? new ArrayListBuilder(this.nicknames.value()) + : new ArrayListBuilder(); nicknamesBuilderConsumer.accept(builder); this.nicknames = changedValue(builder.build()); return this; @@ -622,8 +622,8 @@ public PersonDtoBuilder phoneNumbers(LinkedList phoneNumbers) { */ public PersonDtoBuilder phoneNumbers(Consumer> phoneNumbersBuilderConsumer) { ArrayListBuilder builder = this.phoneNumbers.isSet() - ? new ArrayListBuilder(this.phoneNumbers.value()) - : new ArrayListBuilder(); + ? new ArrayListBuilder(this.phoneNumbers.value()) + : new ArrayListBuilder(); phoneNumbersBuilderConsumer.accept(builder); this.phoneNumbers = changedValue(new LinkedList<>(builder.build())); return this; @@ -720,8 +720,8 @@ public PersonDtoBuilder tags(Set tags) { */ public PersonDtoBuilder tags(Consumer> tagsBuilderConsumer) { HashSetBuilder builder = this.tags.isSet() - ? new HashSetBuilder(this.tags.value()) - : new HashSetBuilder(); + ? new HashSetBuilder(this.tags.value()) + : new HashSetBuilder(); tagsBuilderConsumer.accept(builder); this.tags = changedValue(builder.build()); return this; @@ -779,14 +779,14 @@ public PersonDto build() { throw new IllegalStateException("Field 'age' is marked as non-null but null value was provided"); } PersonDto result = new PersonDto(this.name.value(), - this.age.value(), - this.email.value(), - this.nicknames.value(), - this.tags.value(), - this.metadata.value(), - this.address.value(), - this.previousAddresses.value(), - this.phoneNumbers.value()); + this.age.value(), + this.email.value(), + this.nicknames.value(), + this.tags.value(), + this.metadata.value(), + this.address.value(), + this.previousAddresses.value(), + this.phoneNumbers.value()); return result; } @@ -798,15 +798,15 @@ public PersonDto build() { @Override public String toString() { return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE).append("name", this.name) - .append("age", this.age) - .append("email", this.email) - .append("nicknames", this.nicknames) - .append("tags", this.tags) - .append("metadata", this.metadata) - .append("address", this.address) - .append("previousAddresses", this.previousAddresses) - .append("phoneNumbers", this.phoneNumbers) - .toString(); + .append("age", this.age) + .append("email", this.email) + .append("nicknames", this.nicknames) + .append("tags", this.tags) + .append("metadata", this.metadata) + .append("address", this.address) + .append("previousAddresses", this.previousAddresses) + .append("phoneNumbers", this.phoneNumbers) + .toString(); } /** From dc6bd4554a8432a8f98d2b086bee06aaa4ac34db Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Tue, 21 Apr 2026 22:02:15 +0200 Subject: [PATCH 19/30] Adapting handling of imports - GenericTypes should not get directly imports --- .../processor/model/core/ClassFieldDto.java | 18 +++++--- .../model/core/GenerationTargetClassDto.java | 14 ++++++- .../model/imports/RegularImport.java | 9 +++- .../processor/model/method/MethodCodeDto.java | 12 +++++- .../processor/model/type/TypeNameGeneric.java | 9 ++++ .../processing/BuilderDefinitionCreator.java | 5 +-- .../processor/util/ImportCollector.java | 41 +++++++++++-------- 7 files changed, 76 insertions(+), 32 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/ClassFieldDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/ClassFieldDto.java index 04c694e3..32929f87 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/ClassFieldDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/ClassFieldDto.java @@ -36,6 +36,8 @@ import org.javahelpers.simple.builders.processor.model.imports.StaticImport; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; +import org.javahelpers.simple.builders.processor.model.type.TypeNameArray; +import org.javahelpers.simple.builders.processor.model.type.TypeNameGeneric; /** * Represents a field declaration in the generated class. @@ -106,10 +108,6 @@ public Set getFieldTypeImports() { return fieldTypeImports; } - public void addFieldTypeImport(TypeName typeName) { - this.fieldTypeImports.add(new RegularImport(typeName)); - } - // Convenience methods for adding imports without creating DTOs /** @@ -127,7 +125,14 @@ public void addImport(Class clazz) { * @param typeName the type to import */ public void addImport(TypeName typeName) { - this.fieldTypeImports.add(new RegularImport(typeName)); + if (typeName instanceof TypeNameGeneric generic) { + addImport(generic.getRawType()); + generic.getInnerTypeArguments().forEach(this::addImport); + } else if (typeName instanceof TypeNameArray array) { + addImport(array.getTypeOfArray()); + } else { + this.fieldTypeImports.add(new RegularImport(typeName)); + } } /** @@ -147,7 +152,8 @@ public void addStaticImport(Class clazz, String memberName) { * @param memberName the name of the static member */ public void addStaticImport(TypeName type, String memberName) { - this.fieldTypeImports.add(new StaticImport(type, memberName)); + TypeName typeRaw = new TypeName(type.getPackageName(), type.getClassName()); + this.fieldTypeImports.add(new StaticImport(typeRaw, memberName)); } /** diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/GenerationTargetClassDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/GenerationTargetClassDto.java index f3a92066..685cde3c 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/GenerationTargetClassDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/GenerationTargetClassDto.java @@ -40,6 +40,8 @@ import org.javahelpers.simple.builders.processor.model.type.GenericParameterDto; import org.javahelpers.simple.builders.processor.model.type.NestedTypeDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; +import org.javahelpers.simple.builders.processor.model.type.TypeNameArray; +import org.javahelpers.simple.builders.processor.model.type.TypeNameGeneric; /** * Generic representation of a class to generate. @@ -170,7 +172,14 @@ public void addImport(Class clazz) { * @param typeName the type to import */ public void addImport(TypeName typeName) { - addImport(new RegularImport(typeName)); + if (typeName instanceof TypeNameGeneric generic) { + addImport(generic.getRawType()); + generic.getInnerTypeArguments().forEach(this::addImport); + } else if (typeName instanceof TypeNameArray array) { + addImport(array.getTypeOfArray()); + } else { + addImport(new RegularImport(typeName)); + } } /** @@ -190,7 +199,8 @@ public void addStaticImport(Class clazz, String memberName) { * @param memberName the name of the static member */ public void addStaticImport(TypeName type, String memberName) { - addImport(new StaticImport(type, memberName)); + TypeName typeRaw = new TypeName(type.getPackageName(), type.getClassName()); + addImport(new StaticImport(typeRaw, memberName)); } /** diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/imports/RegularImport.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/imports/RegularImport.java index b86be72a..7bdab049 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/imports/RegularImport.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/imports/RegularImport.java @@ -29,6 +29,8 @@ import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; import org.javahelpers.simple.builders.processor.model.type.TypeName; +import org.javahelpers.simple.builders.processor.model.type.TypeNameArray; +import org.javahelpers.simple.builders.processor.model.type.TypeNameGeneric; /** * Represents a regular (non-static) import statement. @@ -47,8 +49,13 @@ public class RegularImport implements ImportStatement { public RegularImport(TypeName type) { if (type == null) { throw new IllegalArgumentException("type cannot be null"); + } else if (type instanceof TypeNameArray array) { + this.type = array.getTypeOfArray(); + } else if (type instanceof TypeNameGeneric generic) { + this.type = generic.getRawType(); + } else { + this.type = type; } - this.type = type; } /** diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodCodeDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodCodeDto.java index 2bf8b7b7..3d1b66de 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodCodeDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodCodeDto.java @@ -33,6 +33,7 @@ import org.javahelpers.simple.builders.processor.model.imports.RegularImport; import org.javahelpers.simple.builders.processor.model.imports.StaticImport; import org.javahelpers.simple.builders.processor.model.type.TypeName; +import org.javahelpers.simple.builders.processor.model.type.TypeNameArray; import org.javahelpers.simple.builders.processor.model.type.TypeNameGeneric; /** DTO for holding information of code implementation. */ @@ -73,7 +74,7 @@ public void addArgument(String name, String value) { */ public void addArgument(String name, TypeName value) { codeArguments.add(new MethodCodeTypePlaceholder(name, value)); - codeBlockImports.add(new RegularImport(value)); + addCodeBlockImport(value); } /** @@ -119,7 +120,14 @@ public Set getCodeBlockImports() { * @param typeName type to import */ public void addCodeBlockImport(TypeName typeName) { - this.codeBlockImports.add(new RegularImport(typeName)); + if (typeName instanceof TypeNameGeneric generic) { + addCodeBlockImport(generic.getRawType()); + generic.getInnerTypeArguments().forEach(this::addCodeBlockImport); + } else if (typeName instanceof TypeNameArray array) { + addCodeBlockImport(array.getTypeOfArray()); + } else { + this.codeBlockImports.add(new RegularImport(typeName)); + } } /** diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/TypeNameGeneric.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/TypeNameGeneric.java index c3885e2e..115d6160 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/TypeNameGeneric.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/TypeNameGeneric.java @@ -97,6 +97,15 @@ public TypeNameGeneric(String packageName, String className, TypeName... innerTy this.innerTypeArguments = List.of(innerTypeArguments); } + /** + * Returns the raw type (without type parameters). + * + * @return the raw type + */ + public TypeName getRawType() { + return new TypeName(getPackageName(), getClassName()); + } + /** * Returns the list of inner generic type arguments. * diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java index 6c6e6ed6..2e3b355c 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java @@ -191,10 +191,9 @@ private static ClassFieldDto convertToClassField(FieldDto field) { classField.setJavadoc(field.getJavaDoc()); // Add field type imports - classField.addFieldTypeImport( - new TypeName("org.javahelpers.simple.builders.core.util", "TrackedValue")); + classField.addImport(new TypeName("org.javahelpers.simple.builders.core.util", "TrackedValue")); // Add the field type for import - classField.addFieldTypeImport(field.getFieldType()); + classField.addImport(field.getFieldType()); return classField; } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ImportCollector.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ImportCollector.java index e31646a8..c37f8015 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ImportCollector.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ImportCollector.java @@ -67,21 +67,7 @@ public void addTypeImports(TypeName type) { // Add type annotation imports type.getAnnotations().forEach(this::addAnnotationImports); - if (type instanceof TypeNameArray arrayType) { - addTypeImports(arrayType.getTypeOfArray()); - return; - } - - if (type instanceof TypeNameGeneric genericType) { - // Add the raw generic type (without type parameters) - addImport(type); - - // Add inner type arguments - genericType.getInnerTypeArguments().forEach(this::addTypeImports); - return; - } - - // Add the concrete type + // Adding the type addImport(type); } @@ -220,6 +206,7 @@ private void addBodyImports(MethodDto method) { extractTypeReferencesFromCode(code); } + // TODO Remove /** * Extracts type references from code format string. * @@ -287,9 +274,26 @@ private boolean shouldSkipImport(ImportStatement importStatement) { * @param type the type to import */ private void addImport(TypeName type) { - if (type != null) { - addImport(new RegularImport(type)); + if (type == null) { + return; } + + if (type instanceof TypeNameArray arrayType) { + addImport(arrayType.getTypeOfArray()); + return; + } + + if (type instanceof TypeNameGeneric genericType) { + // Add the raw generic type (without type parameters) + addImport(genericType.getRawType()); + + // Add inner type arguments + genericType.getInnerTypeArguments().forEach(this::addTypeImports); + return; + } + + // Add the concrete type + addImport(new RegularImport(type)); } /** @@ -299,7 +303,8 @@ private void addImport(TypeName type) { * @param memberName the name of the static member */ public void addStaticImport(TypeName type, String memberName) { - addImport(new StaticImport(type, memberName)); + TypeName typeRaw = new TypeName(type.getPackageName(), type.getClassName()); + addImport(new StaticImport(typeRaw, memberName)); } /** From b86e80ddde7d5952d19f2657ebe910ae338e9e1d Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Wed, 22 Apr 2026 20:48:04 +0200 Subject: [PATCH 20/30] Fixing codesmells from SonarQube --- .../roaster/RoasterCodeGenerator.java | 4 +-- .../model/method/ConstructorDto.java | 25 ------------------- .../processor/util/ImportCollector.java | 20 --------------- 3 files changed, 1 insertion(+), 48 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java index eca64086..5e28ff0a 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java @@ -646,9 +646,7 @@ public void generateJacksonModule(JacksonModuleDefinitionDto moduleDef) { collector .getSortedImports() .forEach( - importStatement -> { - moduleClass.addImport(importStatement.getFullyQualifiedName()); - }); + importStatement -> moduleClass.addImport(importStatement.getFullyQualifiedName())); // Add mixin interfaces as nested interfaces for (JacksonModuleEntryDto entry : moduleDef.getEntries()) { diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/ConstructorDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/ConstructorDto.java index 0f09e941..9335e0cd 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/ConstructorDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/ConstructorDto.java @@ -89,31 +89,6 @@ public boolean hasCode() { return methodCodeDto.hasCode(); } - /** - * Gets the constructor body as a plain string (for backward compatibility). - * - * @return the constructor body code - * @deprecated Use {@link #getMethodCodeDto()} instead for proper type handling - */ - @Deprecated - public String getBody() { - return hasCode() ? methodCodeDto.getCodeFormat() : null; - } - - /** - * Sets the constructor body as a plain string (for backward compatibility). - * - * @param body the constructor body code - * @deprecated Use {@link #setMethodCodeDto(MethodCodeDto)} instead for proper type handling - */ - @Deprecated - public void setBody(String body) { - if (this.methodCodeDto == null) { - this.methodCodeDto = new MethodCodeDto(); - } - this.methodCodeDto.setCodeFormat(body); - } - public AccessModifier getVisibility() { return visibility; } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ImportCollector.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ImportCollector.java index c37f8015..660442bc 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ImportCollector.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ImportCollector.java @@ -200,26 +200,6 @@ private void addBodyImports(MethodDto method) { addTypeImports(typePlaceholder.getValue()); } } - - // Extract type references from code format (simplified approach) - String code = method.getMethodCodeDto().getCodeFormat(); - extractTypeReferencesFromCode(code); - } - - // TODO Remove - /** - * Extracts type references from code format string. - * - * @param code the code to analyze - */ - private void extractTypeReferencesFromCode(String code) { - if (StringUtils.isBlank(code)) { - return; - } - - // Skip automatic extraction for now to avoid invalid identifiers - // The explicit imports from code arguments and code block imports should be sufficient - // This could be enhanced later with more sophisticated parsing } /** From 8863f3fdb84b068674cbfad297504b03524ca116 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Wed, 22 Apr 2026 20:59:22 +0200 Subject: [PATCH 21/30] separation of imports because of field type and specific field-imports --- .../builders/processor/util/ImportCollector.java | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ImportCollector.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ImportCollector.java index 660442bc..3b47d4ad 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ImportCollector.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ImportCollector.java @@ -161,13 +161,12 @@ public void collectImports(GenerationTargetClassDto classDef) { classDef.getClassAnnotations().forEach(this::addAnnotationImports); // Add field imports from ClassFieldDto - classDef - .getClassFields() - .forEach( - field -> { - addTypeImports(field.getFieldType()); - field.getFieldTypeImports().forEach(this::addImport); - }); + classDef.getClassFields().forEach(field -> addTypeImports(field.getFieldType())); + + // Add all additional imports defined for that field + classDef.getClassFields().stream() + .flatMap(field -> field.getFieldTypeImports().stream()) + .forEach(this::addImport); // Add constructor imports classDef From 06228e02d1ff5fb8e80cb97ca630a0b1707305f1 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Wed, 22 Apr 2026 21:07:30 +0200 Subject: [PATCH 22/30] Removing unused functions --- .../processor/util/ImportCollector.java | 47 +------------------ 1 file changed, 1 insertion(+), 46 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ImportCollector.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ImportCollector.java index 3b47d4ad..f4204c7c 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ImportCollector.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ImportCollector.java @@ -11,7 +11,6 @@ import org.javahelpers.simple.builders.processor.model.core.GenerationTargetClassDto; import org.javahelpers.simple.builders.processor.model.imports.ImportStatement; import org.javahelpers.simple.builders.processor.model.imports.RegularImport; -import org.javahelpers.simple.builders.processor.model.imports.StaticImport; import org.javahelpers.simple.builders.processor.model.method.MethodCodePlaceholder; import org.javahelpers.simple.builders.processor.model.method.MethodCodeTypePlaceholder; import org.javahelpers.simple.builders.processor.model.method.MethodDto; @@ -45,15 +44,6 @@ public ImportCollector(TypeName currentType) { this.currentType = currentType; } - /** - * Adds a regular import (convenience method accepting Class). - * - * @param clazz the class to import - */ - public void addImport(Class clazz) { - addImport(TypeName.of(clazz)); - } - /** * Adds imports for a type. * @@ -275,27 +265,6 @@ private void addImport(TypeName type) { addImport(new RegularImport(type)); } - /** - * Adds a static import. - * - * @param type the type containing the static member - * @param memberName the name of the static member - */ - public void addStaticImport(TypeName type, String memberName) { - TypeName typeRaw = new TypeName(type.getPackageName(), type.getClassName()); - addImport(new StaticImport(typeRaw, memberName)); - } - - /** - * Adds a static import (convenience method accepting Class). - * - * @param clazz the class containing the static member - * @param memberName the name of the static member - */ - public void addStaticImport(Class clazz, String memberName) { - addStaticImport(TypeName.of(clazz), memberName); - } - /** * Returns the collected import statements as a sorted set. * @@ -313,7 +282,7 @@ public Set getSortedImports() { Comparator alphabeticalComparator = Comparator.comparing(ImportStatement::getFullyQualifiedName); - return imports.stream() + return getImports().stream() .sorted(staticFirstComparator.thenComparing(alphabeticalComparator)) .collect(Collectors.toCollection(LinkedHashSet::new)); } @@ -329,18 +298,4 @@ public Set getSortedImports() { public Set getImports() { return new LinkedHashSet<>(imports); } - - /** Clears all collected imports. */ - public void clear() { - imports.clear(); - } - - /** - * Returns the number of collected imports. - * - * @return the number of imports - */ - public int size() { - return imports.size(); - } } From 710b66ad7d2c93e1b2cd311bbba7725145f38c6a Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Wed, 22 Apr 2026 21:28:03 +0200 Subject: [PATCH 23/30] Removing unneeded coreMethods field --- .../builders/processor/BuilderProcessor.java | 2 +- .../builder/ConditionalEnhancer.java | 4 +-- .../builder/CoreMethodsEnhancer.java | 6 ++--- .../model/core/BuilderDefinitionDto.java | 25 ------------------- .../processor/model/core/ClassFieldDto.java | 9 +++++-- .../processor/model/method/MethodDto.java | 4 +-- .../processing/BuilderDefinitionCreator.java | 5 ---- .../processor/BuilderProcessorTest.java | 2 +- .../CustomizingDocumentationTest.java | 4 +-- 9 files changed, 18 insertions(+), 43 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java index a890e4c0..74532150 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java @@ -209,7 +209,7 @@ private void process(Element annotatedElement, BuilderConfiguration config) context.debugEndOperation( "Generated builder with %d fields and %d methods for %s", builderDef.getAllFieldsForBuilder().size(), - builderDef.getCoreMethods().size(), + builderDef.getMethods().size(), builderDef.getBuilderTypeName().getClassName()); } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ConditionalEnhancer.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ConditionalEnhancer.java index e8d65245..94a38881 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ConditionalEnhancer.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ConditionalEnhancer.java @@ -104,11 +104,11 @@ public boolean appliesTo( public void enhanceBuilder(BuilderDefinitionDto builderDto, ProcessingContext context) { // Add conditional(BooleanSupplier, Consumer, Consumer) method MethodDto conditionalMethod = createConditionalMethod(builderDto); - builderDto.addCoreMethod(conditionalMethod); + builderDto.addMethod(conditionalMethod); // Add conditional(BooleanSupplier, Consumer) method MethodDto conditionalPositiveMethod = createConditionalPositiveOnlyMethod(builderDto); - builderDto.addCoreMethod(conditionalPositiveMethod); + builderDto.addMethod(conditionalPositiveMethod); context.debug( "Added conditional methods to builder %s", builderDto.getBuilderTypeName().getClassName()); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java index 5c577d04..98fc4630 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java @@ -101,15 +101,15 @@ public boolean appliesTo( public void enhanceBuilder(BuilderDefinitionDto builderDto, ProcessingContext context) { // Add build() method MethodDto buildMethod = createBuildMethod(builderDto); - builderDto.addCoreMethod(buildMethod); + builderDto.addMethod(buildMethod); // Add static create() method MethodDto createMethod = createStaticCreateMethod(builderDto); - builderDto.addCoreMethod(createMethod); + builderDto.addMethod(createMethod); // Add toString() method MethodDto toStringMethod = createToStringMethod(builderDto); - builderDto.addCoreMethod(toStringMethod); + builderDto.addMethod(toStringMethod); } /** Creates the build() method. */ diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderDefinitionDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderDefinitionDto.java index c9767856..7bc198e2 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderDefinitionDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderDefinitionDto.java @@ -26,7 +26,6 @@ import java.util.LinkedList; import java.util.List; -import org.javahelpers.simple.builders.processor.model.method.MethodDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; /** @@ -52,12 +51,6 @@ public class BuilderDefinitionDto extends GenerationTargetClassDto { */ private final List fields = new LinkedList<>(); - /** - * Core builder methods (build, create, conditional, toString, etc.) to be generated. These are - * added by BuilderEnhancers and have ordering for generation sequence. - */ - private final List coreMethods = new LinkedList<>(); - /** Configuration for builder generation. */ private BuilderConfiguration configuration; @@ -164,24 +157,6 @@ public List getAllFieldsForBuilder() { return all; } - /** - * Returns the list of core builder methods. - * - * @return the list of core methods - */ - public List getCoreMethods() { - return coreMethods; - } - - /** - * Adds a core method to be generated in the builder. - * - * @param method the core method to add - */ - public void addCoreMethod(MethodDto method) { - this.coreMethods.add(method); - } - /** * Returns the configuration for builder generation. * diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/ClassFieldDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/ClassFieldDto.java index 32929f87..c465d62d 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/ClassFieldDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/ClassFieldDto.java @@ -49,10 +49,10 @@ public class ClassFieldDto { /** Field name in generated class. */ private String fieldName; - /** Field type (e.g., String represented as TypeName). */ + /** Field type. */ private TypeName fieldType; - /** Field visibility (typically PRIVATE). */ + /** Field visibility. */ private AccessModifier visibility = PRIVATE; /** Literal initializer (e.g., "exampleValue" like in {@code String field="exampleValue";}). */ @@ -72,6 +72,11 @@ public void setFieldName(String fieldName) { this.fieldName = fieldName; } + /** + * Returns the field type. + * + * @return the field type + */ public TypeName getFieldType() { return fieldType; } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodDto.java index 38a544a4..70eb64ec 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodDto.java @@ -55,7 +55,7 @@ public class MethodDto { /** Ordering for method generation. Lower values appear first in generated class. */ private int ordering = 1000; // Default high value for field-generated methods - /** Origin description for logging (e.g., "field 'name'", "core method 'build'"). */ + /** Origin description for logging (e.g., "field 'name'"). */ private String sourceDescription; /** Name of method. */ @@ -151,7 +151,7 @@ public int getOrdering() { /** * Gets the source description for logging. * - * @return the source description (e.g., "field 'name'", "core method 'build'") + * @return the source description (e.g., "field 'name'") */ public String getSourceDescription() { return sourceDescription; diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java index 2e3b355c..ce74f5aa 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java @@ -149,11 +149,6 @@ private static void finalizeDefinition( } } - for (MethodDto method : builderDto.getCoreMethods()) { - method.setSourceDescription("core method '" + method.getMethodName() + "'"); - builderDto.addMethod(method); - } - // 3. Set class access modifier builderDto.setClassAccessModifier(builderDto.getConfiguration().getBuilderAccess()); 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 f49ab8b5..026fc763 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 @@ -142,7 +142,7 @@ void shouldLogDebugMessagesWhenVerboseModeEnabled() { "[DEBUG] │ ├─ Writing class to file: test.VerboseTestBuilder", "[DEBUG] │ └─ Successfully generated class: VerboseTestBuilder", "[DEBUG] ├─ Jackson module entry added", - "[DEBUG] └─ Generated builder with 1 fields and 5 methods for VerboseTestBuilder", + "[DEBUG] └─ Generated builder with 1 fields and 9 methods for VerboseTestBuilder", "simple-builders: Successfully generated 1 builder(s) in this processing round", ""); } diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/CustomizingDocumentationTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/CustomizingDocumentationTest.java index 7faacda1..91781cbb 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/CustomizingDocumentationTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/CustomizingDocumentationTest.java @@ -486,7 +486,7 @@ public boolean appliesTo(BuilderDefinitionDto builderDto, TypeName dtoType, Proc public void enhanceBuilder(BuilderDefinitionDto builderDto, ProcessingContext context) { // Add validation method to builder MethodDto validateMethod = createValidateMethod(); - builderDto.addCoreMethod(validateMethod); + builderDto.addMethod(validateMethod); // Add @Valid annotation if available if (isValidationAvailable(context)) { @@ -665,7 +665,7 @@ public void enhanceBuilder(BuilderDefinitionDto builderDto, ProcessingContext co factoryMethod.setCode(String.format("return new %s();", builderType.getClassName())); // Note: addStaticMethod is conceptual - shows the pattern - builderDto.addCoreMethod(factoryMethod); + builderDto.addMethod(factoryMethod); context.debug("Added factory method to builder %s", builderType.getClassName()); } From 3fa5ce17094d9db3e95609f3d8e425ac7e21ad8c Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Wed, 22 Apr 2026 21:41:09 +0200 Subject: [PATCH 24/30] Removing sourceDescription from methodDto because it is only needed for logging in conflict resolution --- .../roaster/RoasterCodeGenerator.java | 21 ++++++------------- .../processor/model/method/MethodDto.java | 21 ------------------- .../processing/BuilderDefinitionCreator.java | 4 +--- .../MethodConflictResolutionTest.java | 2 +- 4 files changed, 8 insertions(+), 40 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java index 5e28ff0a..5bf05cf6 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java @@ -251,28 +251,19 @@ private List resolveMethodConflicts(List methods) { if (existing == null) { signatureToMethod.put(signature, method); } else { - String existingSource = - existing.getSourceDescription() != null - ? existing.getSourceDescription() - : "unknown source"; - String newSource = - method.getSourceDescription() != null - ? method.getSourceDescription() - : "unknown source"; - if (method.getPriority() > existing.getPriority()) { signatureToMethod.put(signature, method); logger.warning( - " Method conflict: '%s' from %s (priority %d) dropped in favor of %s (priority %d)", - signature, existingSource, existing.getPriority(), newSource, method.getPriority()); + " Method conflict: '%s' (priority %d) dropped in favor of priority %d", + signature, existing.getPriority(), method.getPriority()); } else if (method.getPriority() < existing.getPriority()) { logger.warning( - " Method conflict: '%s' from %s (priority %d) dropped in favor of %s (priority %d)", - signature, newSource, method.getPriority(), existingSource, existing.getPriority()); + " Method conflict: '%s' (priority %d) dropped in favor of priority %d", + signature, method.getPriority(), existing.getPriority()); } else { logger.warning( - " Method conflict: '%s' from %s (priority %d) dropped in favor of %s (priority %d) - equal priority, keeping first", - signature, newSource, method.getPriority(), existingSource, existing.getPriority()); + " Method conflict: '%s' (priority %d) - equal priority, keeping first", + signature, method.getPriority()); } } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodDto.java index 70eb64ec..896986f9 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodDto.java @@ -55,9 +55,6 @@ public class MethodDto { /** Ordering for method generation. Lower values appear first in generated class. */ private int ordering = 1000; // Default high value for field-generated methods - /** Origin description for logging (e.g., "field 'name'"). */ - private String sourceDescription; - /** Name of method. */ private String methodName; @@ -148,24 +145,6 @@ public int getOrdering() { return ordering; } - /** - * Gets the source description for logging. - * - * @return the source description (e.g., "field 'name'") - */ - public String getSourceDescription() { - return sourceDescription; - } - - /** - * Sets the source description for logging. - * - * @param sourceDescription the source description - */ - public void setSourceDescription(String sourceDescription) { - this.sourceDescription = sourceDescription; - } - /** * Setting the inner implementation of a method. Supports placeholders which has to be set by * addArgument. diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java index ce74f5aa..a1158e4c 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java @@ -134,17 +134,15 @@ private static void finalizeDefinition( builderDto.addClassField(classField); } - // 2. Collect methods from fields and core methods, set source descriptions + // 2. Collect methods from fields for (FieldDto field : builderDto.getConstructorFieldsForBuilder()) { for (MethodDto method : field.getMethods()) { - method.setSourceDescription("field '" + field.getFieldNameInBuilder() + "'"); builderDto.addMethod(method); } } for (FieldDto field : builderDto.getSetterFieldsForBuilder()) { for (MethodDto method : field.getMethods()) { - method.setSourceDescription("field '" + field.getFieldNameInBuilder() + "'"); builderDto.addMethod(method); } } diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/MethodConflictResolutionTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/MethodConflictResolutionTest.java index e8811508..9ccb1665 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/MethodConflictResolutionTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/MethodConflictResolutionTest.java @@ -390,7 +390,7 @@ public void setValue(java.util.function.Supplier valueSupplier) { .filter( d -> d.getMessage(null).contains("Method conflict") - && d.getMessage(null).contains("dropped in favor of field")) + && d.getMessage(null).contains("dropped in favor of")) .filter(d -> d.getMessage(null).contains("priority 80")) .filter(d -> d.getMessage(null).contains("priority 100")) .count(); From 8df92b53c2c397586d2526585ba048a9b5fc37cf Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Thu, 23 Apr 2026 20:46:45 +0200 Subject: [PATCH 25/30] Switching interfaces visibility to use AccessModifier --- .../classgen/roaster/RoasterCodeGenerator.java | 7 ++----- .../generators/builder/WithInterfaceEnhancer.java | 3 ++- .../processor/model/type/NestedTypeDto.java | 13 +++++++------ 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java index 5bf05cf6..a8aaf6c5 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java @@ -300,11 +300,8 @@ private void appendNestedType(JavaClassSource source, NestedTypeDto nestedType) ? source.addNestedType(JavaInterfaceSource.class) : source.addNestedType(JavaClassSource.class); nestedSource.setName(nestedType.getTypeName()); - if (nestedType.isPublic()) { - nestedSource.setPublic(); - } else { - nestedSource.setPackagePrivate(); - } + applyVisibility(nestedSource, nestedType.getVisibility()); + applyAnnotations(nestedSource, nestedType.getAnnotations()); applyJavadoc(nestedSource, nestedType.getJavadoc()); boolean isInterface = nestedType.getKind() == NestedTypeDto.NestedTypeKind.INTERFACE; for (MethodDto methodDto : nestedType.getMethods()) { diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/WithInterfaceEnhancer.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/WithInterfaceEnhancer.java index 545472fd..5344d746 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/WithInterfaceEnhancer.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/WithInterfaceEnhancer.java @@ -23,6 +23,7 @@ */ package org.javahelpers.simple.builders.processor.generators.builder; +import org.javahelpers.simple.builders.core.enums.AccessModifier; import org.javahelpers.simple.builders.processor.analysis.JavaLangMapper; import org.javahelpers.simple.builders.processor.generators.BuilderEnhancer; import org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil; @@ -113,7 +114,7 @@ private NestedTypeDto createWithInterface(BuilderDefinitionDto builderDto) { NestedTypeDto withInterface = new NestedTypeDto(); withInterface.setTypeName("With"); withInterface.setKind(NestedTypeDto.NestedTypeKind.INTERFACE); - withInterface.setPublic(true); + withInterface.setVisibility(AccessModifier.PUBLIC); withInterface.setJavadoc( new JavadocDto( "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/model/type/NestedTypeDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/NestedTypeDto.java index 61ebea84..1f6f6b50 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/NestedTypeDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/NestedTypeDto.java @@ -27,6 +27,7 @@ import java.util.ArrayList; import java.util.LinkedList; import java.util.List; +import org.javahelpers.simple.builders.core.enums.AccessModifier; import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; import org.javahelpers.simple.builders.processor.model.method.MethodDto; @@ -44,8 +45,8 @@ public class NestedTypeDto { /** The kind of nested type (INTERFACE or CLASS). */ private NestedTypeKind kind; - /** Whether this nested type should be public. */ - private boolean isPublic = true; + /** Visibility of this nested type. */ + private AccessModifier visibility = AccessModifier.PUBLIC; /** Methods to be generated in this nested type. */ private final List methods = new LinkedList<>(); @@ -77,12 +78,12 @@ public void setKind(NestedTypeKind kind) { this.kind = kind; } - public boolean isPublic() { - return isPublic; + public AccessModifier getVisibility() { + return visibility; } - public void setPublic(boolean isPublic) { - this.isPublic = isPublic; + public void setVisibility(AccessModifier visibility) { + this.visibility = visibility; } public List getMethods() { From dfe89967e0747ae90f19abbc176997bce2ce4080 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Thu, 23 Apr 2026 20:58:06 +0200 Subject: [PATCH 26/30] Using flat-map-streams for import collecting --- .../processor/util/ImportCollector.java | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ImportCollector.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ImportCollector.java index f4204c7c..20716ffe 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ImportCollector.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ImportCollector.java @@ -158,17 +158,25 @@ public void collectImports(GenerationTargetClassDto classDef) { .flatMap(field -> field.getFieldTypeImports().stream()) .forEach(this::addImport); - // Add constructor imports - classDef - .getConstructors() - .forEach( - constructor -> { - constructor.getParameters().forEach(this::addParameterImports); - constructor.getMethodCodeDto().getCodeBlockImports().forEach(this::addImport); - }); - - // Nested types are part of the same class and don't need imports - // (e.g., the "With" interface is nested within the builder class) + // Add constructor parameter imports + classDef.getConstructors().stream() + .flatMap(constructor -> constructor.getParameters().stream()) + .forEach(this::addParameterImports); + + // Add constructor code block imports + classDef.getConstructors().stream() + .flatMap(constructor -> constructor.getMethodCodeDto().getCodeBlockImports().stream()) + .forEach(this::addImport); + + // Add nested-type annotation imports + classDef.getNestedTypes().stream() + .flatMap(nested -> nested.getAnnotations().stream()) + .forEach(this::addAnnotationImports); + + // Add nested-type method imports + classDef.getNestedTypes().stream() + .flatMap(nested -> nested.getMethods().stream()) + .forEach(this::addMethodImports); // Add method imports classDef.getMethods().forEach(this::addMethodImports); From 8d157bfcb7ad97777217e80755468c77d64751e3 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Thu, 23 Apr 2026 21:07:19 +0200 Subject: [PATCH 27/30] Migration generation of JacksonModule into default functionality of RoasterCodeGenerator (to have no specific code there anymore) --- .../example/SimpleBuildersJacksonModule.java | 8 +- .../builders/processor/BuilderProcessor.java | 16 ++- .../roaster/RoasterCodeGenerator.java | 121 ------------------ .../integration/JacksonModuleGenerator.java | 91 +++++++++++-- 4 files changed, 100 insertions(+), 136 deletions(-) diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SimpleBuildersJacksonModule.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SimpleBuildersJacksonModule.java index 6e406c1a..f1ecab70 100644 --- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SimpleBuildersJacksonModule.java +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SimpleBuildersJacksonModule.java @@ -5,11 +5,11 @@ public class SimpleBuildersJacksonModule extends SimpleModule { - @JsonDeserialize(builder = JacksonIntegrationDtoBuilder.class) - private interface JacksonIntegrationDtoMixin { - } - public SimpleBuildersJacksonModule() { setMixInAnnotation(JacksonIntegrationDto.class, JacksonIntegrationDtoMixin.class); } + + @JsonDeserialize(builder = JacksonIntegrationDtoBuilder.class) + private interface JacksonIntegrationDtoMixin { + } } \ No newline at end of file diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java index 74532150..07530341 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java @@ -47,6 +47,7 @@ import org.javahelpers.simple.builders.processor.generators.integration.JacksonModuleGenerator; import org.javahelpers.simple.builders.processor.model.core.BuilderConfiguration; import org.javahelpers.simple.builders.processor.model.core.BuilderDefinitionDto; +import org.javahelpers.simple.builders.processor.model.core.GenerationTargetClassDto; import org.javahelpers.simple.builders.processor.processing.BuilderConfigurationReader; import org.javahelpers.simple.builders.processor.processing.CompilerArgumentsEnum; import org.javahelpers.simple.builders.processor.processing.CompilerArgumentsReader; @@ -107,9 +108,18 @@ public boolean process(Set annotations, RoundEnvironment // Generate Jackson Module if processing is over and feature is enabled if (roundEnv.processingOver()) { - var modules = jacksonModuleGenerator.getModuleDefinitions(); - for (var module : modules) { - codeGenerator.generateJacksonModule(module); + List moduleClassDefs = + jacksonModuleGenerator.getModuleDefinitions(); + for (GenerationTargetClassDto moduleClassDef : moduleClassDefs) { + String packageName = moduleClassDef.getTypeName().getPackageName(); + context.info("Generating Jackson Module in package '%s'", packageName); + try { + codeGenerator.generateClass(moduleClassDef); + } catch (BuilderException e) { + context.warning( + "simple-builders: Error generating Jackson module for package %s: %s", + packageName, e.getMessage()); + } } // Reset indentation after Jackson module generation as well context.resetIndentation(); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java index a8aaf6c5..d555571d 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java @@ -25,7 +25,6 @@ package org.javahelpers.simple.builders.processor.classgen.roaster; import static org.javahelpers.simple.builders.processor.classgen.roaster.RoasterMapper.mapType; -import static org.javahelpers.simple.builders.processor.classgen.roaster.RoasterMapper.packageNameOf; import static org.javahelpers.simple.builders.processor.classgen.roaster.RoasterMapper.resolveCodeTemplate; import java.io.IOException; @@ -47,8 +46,6 @@ import org.javahelpers.simple.builders.processor.model.core.ClassFieldDto; import org.javahelpers.simple.builders.processor.model.core.GenerationTargetClassDto; import org.javahelpers.simple.builders.processor.model.imports.ImportStatement; -import org.javahelpers.simple.builders.processor.model.integration.JacksonModuleDefinitionDto; -import org.javahelpers.simple.builders.processor.model.integration.JacksonModuleEntryDto; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocTagDto; import org.javahelpers.simple.builders.processor.model.method.ConstructorDto; @@ -589,122 +586,4 @@ private boolean builderClassAlreadyExists(String qualifiedName) { return false; } } - - /** - * Generates a Jackson SimpleModule based on the provided definition. - * - * @param moduleDef the definition of the Jackson module to generate - */ - public void generateJacksonModule(JacksonModuleDefinitionDto moduleDef) { - String packageName = moduleDef.getTargetPackage(); - String moduleClassName = "SimpleBuildersJacksonModule"; - - logger.info("Generating Jackson Module '%s' in package '%s'", moduleClassName, packageName); - - try { - // Create the class using Roaster - JavaClassSource moduleClass = Roaster.create(JavaClassSource.class); - moduleClass.setPackage(packageName); - moduleClass.setName(moduleClassName); - moduleClass.setSuperType("SimpleModule"); - - // Collect imports using ImportCollector - TypeName moduleType = new TypeName(packageName, moduleClassName); - ImportCollector collector = new ImportCollector(moduleType); - - // Add Jackson-specific imports using convenience method with TypeName - collector.addTypeImports( - new org.javahelpers.simple.builders.processor.model.type.TypeName( - "com.fasterxml.jackson.databind.annotation", "JsonDeserialize")); - collector.addTypeImports( - new org.javahelpers.simple.builders.processor.model.type.TypeName( - "com.fasterxml.jackson.databind.module", "SimpleModule")); - - // Adding imports for DTO types - moduleDef.getEntries().stream() - .filter(e -> shouldAddImport(packageName, e.dtoType().getFullQualifiedName())) - .forEach(e -> collector.addTypeImports(e.dtoType())); - - // Adding imports for builder types - moduleDef.getEntries().stream() - .filter(e -> shouldAddImport(packageName, e.builderType().getFullQualifiedName())) - .forEach(e -> collector.addTypeImports(e.builderType())); - - // Add sorted imports to Roaster - collector - .getSortedImports() - .forEach( - importStatement -> moduleClass.addImport(importStatement.getFullyQualifiedName())); - - // Add mixin interfaces as nested interfaces - for (JacksonModuleEntryDto entry : moduleDef.getEntries()) { - String mixinName = entry.dtoType().getClassName() + "Mixin"; - - // Create the nested interface - JavaInterfaceSource mixinInterface = Roaster.create(JavaInterfaceSource.class); - mixinInterface.setName(mixinName); - mixinInterface.setPrivate(); - - // Add the JsonDeserialize annotation - AnnotationSource annotation = - mixinInterface.addAnnotation("JsonDeserialize"); - annotation.setLiteralValue("builder", entry.builderType().getClassName() + ".class"); - - // Add as nested type to the module class - moduleClass.addNestedType(mixinInterface); - } - - // Add constructor - MethodSource constructor = moduleClass.addMethod(); - constructor.setConstructor(true); - constructor.setPublic(); - - // Build constructor body - StringBuilder constructorBody = new StringBuilder(); - for (JacksonModuleEntryDto entry : moduleDef.getEntries()) { - String mixinName = entry.dtoType().getClassName() + "Mixin"; - constructorBody.append( - "setMixInAnnotation(%s.class, %s.class);%n" - .formatted(entry.dtoType().getClassName(), mixinName)); - } - constructor.setBody(constructorBody.toString()); - - // Write the generated class - String source = formatSource(moduleClass.toString()); - writeSimpleClassToFile(packageName, moduleClassName, source); - - } catch (BuilderException e) { - logger.warning( - "simple-builders: Error generating Jackson module for package %s: %s\n%s", - packageName, e.getMessage(), java.util.Arrays.toString(e.getStackTrace())); - } - } - - private boolean shouldAddImport(String currentPackage, String fqn) { - return StringUtils.isNotBlank(fqn) - && fqn.contains(".") - && !fqn.startsWith("java.lang.") - && !currentPackage.equals(packageNameOf(fqn)); - } - - private void writeSimpleClassToFile(String packageName, String className, String sourceCode) - throws BuilderException { - try { - String qualifiedName = - StringUtils.isBlank(packageName) ? className : packageName + "." + className; - JavaFileObject file = processingEnv.getFiler().createSourceFile(qualifiedName); - try (Writer writer = file.openWriter()) { - writer.write(sourceCode); - } - } catch (IOException ex) { - String message = ex.getMessage(); - String errorMessage = - """ - Unable to create class: %s. - Check the build environment and ensure all necessary directories are accessible. - """ - .formatted(StringUtils.isNotBlank(message) ? message : "Unknown error"); - throw new BuilderException(null, errorMessage); - } - } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/integration/JacksonModuleGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/integration/JacksonModuleGenerator.java index 41c56bf0..6c7e96a0 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/integration/JacksonModuleGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/integration/JacksonModuleGenerator.java @@ -31,10 +31,16 @@ import javax.lang.model.element.Element; import org.apache.commons.collections4.SetValuedMap; import org.apache.commons.collections4.multimap.HashSetValuedHashMap; +import org.javahelpers.simple.builders.core.enums.AccessModifier; +import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto; import org.javahelpers.simple.builders.processor.model.core.BuilderConfiguration; import org.javahelpers.simple.builders.processor.model.core.BuilderDefinitionDto; -import org.javahelpers.simple.builders.processor.model.integration.JacksonModuleDefinitionDto; +import org.javahelpers.simple.builders.processor.model.core.GenerationTargetClassDto; import org.javahelpers.simple.builders.processor.model.integration.JacksonModuleEntryDto; +import org.javahelpers.simple.builders.processor.model.method.ConstructorDto; +import org.javahelpers.simple.builders.processor.model.method.MethodCodeDto; +import org.javahelpers.simple.builders.processor.model.type.NestedTypeDto; +import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.processing.ProcessingLogger; /** @@ -45,6 +51,14 @@ */ public class JacksonModuleGenerator { + /** Simple name of the generated Jackson module class. */ + public static final String MODULE_CLASS_NAME = "SimpleBuildersJacksonModule"; + + private static final TypeName SIMPLE_MODULE_TYPE = + new TypeName("com.fasterxml.jackson.databind.module", "SimpleModule"); + private static final TypeName JSON_DESERIALIZE_TYPE = + new TypeName("com.fasterxml.jackson.databind.annotation", "JsonDeserialize"); + private final ProcessingEnvironment processingEnv; private final ProcessingLogger logger; private final SetValuedMap entriesByPackage = @@ -109,14 +123,17 @@ private boolean validateForModuleGeneration(BuilderConfiguration config, Element } /** - * Returns the definitions for the Jackson Modules to be generated. + * Returns the fully-built target class definitions for the Jackson Modules to be generated. + * + *

Collected entries are assembled into generic {@link GenerationTargetClassDto} instances + * ready to be rendered by the shared {@code RoasterCodeGenerator.generateClass} pipeline. * *

Logs status and clears state after retrieval. * - * @return list of JacksonModuleDefinitionDto + * @return list of target class definitions, one per package */ - public List getModuleDefinitions() { - List definitions = new ArrayList<>(); + public List getModuleDefinitions() { + List definitions = new ArrayList<>(); if (!entriesByPackage.isEmpty()) { logger.info( "simple-builders: Processing OVER. JacksonModuleEnabled: true, Packages: %d", @@ -124,15 +141,73 @@ public List getModuleDefinitions() { for (String packageName : entriesByPackage.keySet()) { Set moduleEntries = entriesByPackage.get(packageName); - JacksonModuleDefinitionDto definition = new JacksonModuleDefinitionDto(packageName); - definition.addAllEntries(moduleEntries); - definitions.add(definition); + definitions.add(buildTargetClass(packageName, moduleEntries)); } } clear(); return definitions; } + /** + * Builds a generic {@link GenerationTargetClassDto} for the Jackson module of the given package. + * + * @param packageName target package of the generated module class + * @param entries DTO/builder type pairs to register as mixins + * @return a fully populated class definition ready for code generation + */ + private GenerationTargetClassDto buildTargetClass( + String packageName, Set entries) { + GenerationTargetClassDto classDef = new GenerationTargetClassDto(); + classDef.setTypeName(new TypeName(packageName, MODULE_CLASS_NAME)); + classDef.setClassAccessModifier(AccessModifier.PUBLIC); + classDef.setSuperType(SIMPLE_MODULE_TYPE); + + // Each entry becomes a private nested mixin interface annotated with @JsonDeserialize. + for (JacksonModuleEntryDto entry : entries) { + classDef.addNestedType(createMixinInterface(entry)); + // The builder type appears as a literal in the @JsonDeserialize(builder = ...) member + // and therefore must be imported explicitly. + classDef.addImport(entry.builderType()); + } + + classDef.addConstructor(createConstructor(entries)); + + return classDef; + } + + private static NestedTypeDto createMixinInterface(JacksonModuleEntryDto entry) { + NestedTypeDto mixin = new NestedTypeDto(); + mixin.setTypeName(entry.dtoType().getClassName() + "Mixin"); + mixin.setKind(NestedTypeDto.NestedTypeKind.INTERFACE); + mixin.setVisibility(AccessModifier.PRIVATE); + + AnnotationDto jsonDeserialize = new AnnotationDto(); + jsonDeserialize.setAnnotationType(JSON_DESERIALIZE_TYPE); + jsonDeserialize.addMember("builder", entry.builderType().getClassName() + ".class"); + mixin.addAnnotation(jsonDeserialize); + + return mixin; + } + + private static ConstructorDto createConstructor(Set entries) { + ConstructorDto constructor = new ConstructorDto(); + constructor.setVisibility(AccessModifier.PUBLIC); + + MethodCodeDto code = new MethodCodeDto(); + int index = 0; + for (JacksonModuleEntryDto entry : entries) { + String dtoLabel = "dto" + index; + String mixinLabel = "mixin" + index; + code.addArgument(dtoLabel, entry.dtoType()); + code.addArgument(mixinLabel, entry.dtoType().getClassName() + "Mixin"); + code.append("setMixInAnnotation($%s:T.class, $%s:L.class);", dtoLabel, mixinLabel); + index++; + } + + constructor.setMethodCodeDto(code); + return constructor; + } + private void clear() { entriesByPackage.clear(); } From 656789725596db20bf5dabb05b5a92729690a4b6 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Thu, 23 Apr 2026 21:18:01 +0200 Subject: [PATCH 28/30] Removing unneeded JacksonModuleDefinitionDto --- .../JacksonModuleDefinitionDto.java | 58 ------------------- 1 file changed, 58 deletions(-) delete mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/model/integration/JacksonModuleDefinitionDto.java diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/integration/JacksonModuleDefinitionDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/integration/JacksonModuleDefinitionDto.java deleted file mode 100644 index e5457987..00000000 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/integration/JacksonModuleDefinitionDto.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2026 Andreas Igel - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package org.javahelpers.simple.builders.processor.model.integration; - -import java.util.Collection; -import java.util.HashSet; -import java.util.Set; - -/** - * Definition for generating a Jackson SimpleModule. Contains the target package and the entries to - * be registered in the module. - */ -public class JacksonModuleDefinitionDto { - private final String targetPackage; - private final Set entries = new HashSet<>(); - - public JacksonModuleDefinitionDto(String targetPackage) { - this.targetPackage = targetPackage; - } - - public String getTargetPackage() { - return targetPackage; - } - - public Set getEntries() { - return entries; - } - - public void addEntry(JacksonModuleEntryDto entry) { - this.entries.add(entry); - } - - public void addAllEntries(Collection entries) { - this.entries.addAll(entries); - } -} From 9a673b598649bdebe2fa3b990612b53b06e3ae43 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Thu, 23 Apr 2026 21:57:57 +0200 Subject: [PATCH 29/30] Improving code --- .../roaster/RoasterCodeGenerator.java | 7 ++-- .../builder/ConstructorEnhancer.java | 2 +- .../builder/CoreMethodsEnhancer.java | 2 +- .../field/ArrayBuilderConsumerGenerator.java | 2 +- .../field/OptionalHelperGenerator.java | 2 +- .../field/StringBuilderConsumerGenerator.java | 2 +- .../field/StringFormatHelperGenerator.java | 2 +- .../field/VarArgsHelperGenerator.java | 6 ++-- .../processor/model/method/MethodCodeDto.java | 4 +-- .../processing/BuilderDefinitionCreator.java | 1 - .../processor/util/ImportCollector.java | 33 +++++++++++++++---- 11 files changed, 39 insertions(+), 24 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java index d555571d..6c22e573 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java @@ -110,17 +110,13 @@ public void generateClass(GenerationTargetClassDto classDef) throws BuilderExcep } private String createClassSource(GenerationTargetClassDto classDef) { - // Class generation - works for all GenerationTargetClassDto instances JavaClassSource source = createJavaClassSource(classDef); addClassMetadata(source, classDef); appendFields(source, classDef); appendConstructors(source, classDef); appendMethods(source, classDef); appendNestedTypes(source, classDef); - - // Apply annotations first (Roaster will add imports) applyClassAnnotations(source, classDef); - return renderClassSource(source); } @@ -149,7 +145,8 @@ private JavaClassSource createJavaClassSource(GenerationTargetClassDto classDef) addGenericDeclarations(source, classDef.getGenerics()); // Collect and add imports early (before elements are added) - for (ImportStatement importStmt : collectImports(classDef)) { + Set importStmts = ImportCollector.collectAndSortImports(classDef); + for (ImportStatement importStmt : importStmts) { if (importStmt.isStatic()) { source.addImport(importStmt.getFullyQualifiedName()).setStatic(true); } else { diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ConstructorEnhancer.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ConstructorEnhancer.java index 1060c45d..89f46bd5 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ConstructorEnhancer.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ConstructorEnhancer.java @@ -116,7 +116,7 @@ protected ConstructorDto createFromInstanceConstructor(BuilderDefinitionDto buil } """, getterName, fieldName, getterName); - codeDto.addCodeBlockImport(TypeName.of(IllegalArgumentException.class)); + codeDto.addCodeBlockImport(IllegalArgumentException.class); } codeDto.append( """ diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java index 98fc4630..44ed603f 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java @@ -196,7 +196,7 @@ protected MethodDto createBuildMethod(BuilderDefinitionDto builderDto) { method.setCode(code.toString()); method.addArgument("dtoBaseType", builderDto.getBuildingTargetTypeName()); method.addArgument("buildResultType", returnType); - method.getMethodCodeDto().addCodeBlockImport(TypeName.of(IllegalStateException.class)); + method.getMethodCodeDto().addCodeBlockImport(IllegalStateException.class); method.setJavadoc(new JavadocDto("Builds the configured DTO instance.")); return method; diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ArrayBuilderConsumerGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ArrayBuilderConsumerGenerator.java index a445ea69..d6d02486 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ArrayBuilderConsumerGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ArrayBuilderConsumerGenerator.java @@ -144,7 +144,7 @@ private MethodDto createFieldConsumerWithArrayBuilder( """); // Add code block import for java.util.List.of - methodDto.getMethodCodeDto().addCodeBlockImport(TypeName.of(List.class)); + methodDto.getMethodCodeDto().addCodeBlockImport(List.class); methodDto.addArgument("fieldName", fieldNameInBuilder); methodDto.addArgument("dtoMethodParam", parameter.getParameterName()); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/OptionalHelperGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/OptionalHelperGenerator.java index 9c7a45fe..d60bb31e 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/OptionalHelperGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/OptionalHelperGenerator.java @@ -107,7 +107,7 @@ public List generateMethods( field, "Optional.ofNullable(%s)", innerType, builderType, context); // Add code block import for Optional.ofNullable - method.getMethodCodeDto().addCodeBlockImport(TypeName.of(Optional.class)); + method.getMethodCodeDto().addCodeBlockImport(Optional.class); return Collections.singletonList(method); } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringBuilderConsumerGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringBuilderConsumerGenerator.java index 6506c695..49eea86f 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringBuilderConsumerGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringBuilderConsumerGenerator.java @@ -122,7 +122,7 @@ public List generateMethods( // Add code block import for Optional.of when used if (isOptionalString(field.getFieldType())) { - method.getMethodCodeDto().addCodeBlockImport(TypeName.of(Optional.class)); + method.getMethodCodeDto().addCodeBlockImport(Optional.class); } return List.of(method); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringFormatHelperGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringFormatHelperGenerator.java index f057bde2..5ee2fd9f 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringFormatHelperGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringFormatHelperGenerator.java @@ -142,7 +142,7 @@ public List generateMethods( context); // Add code block imports for Optional.of and String.format - method.getMethodCodeDto().addCodeBlockImport(TypeName.of(Optional.class)); + method.getMethodCodeDto().addCodeBlockImport(Optional.class); methods.add(method); } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/VarArgsHelperGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/VarArgsHelperGenerator.java index 9f983554..3ef380d2 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/VarArgsHelperGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/VarArgsHelperGenerator.java @@ -159,11 +159,11 @@ private MethodDto createFieldSetterByVarArgs( // Add code block imports for collection factory methods if (fieldType instanceof TypeNameList) { - method.getMethodCodeDto().addCodeBlockImport(TypeName.of(List.class)); + method.getMethodCodeDto().addCodeBlockImport(List.class); } else if (fieldType instanceof TypeNameSet) { - method.getMethodCodeDto().addCodeBlockImport(TypeName.of(Set.class)); + method.getMethodCodeDto().addCodeBlockImport(Set.class); } else if (fieldType instanceof TypeNameMap) { - method.getMethodCodeDto().addCodeBlockImport(TypeName.of(Map.class)); + method.getMethodCodeDto().addCodeBlockImport(Map.class); } return method; diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodCodeDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodCodeDto.java index 3d1b66de..d4d26099 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodCodeDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodCodeDto.java @@ -131,11 +131,11 @@ public void addCodeBlockImport(TypeName typeName) { } /** - * Adds a regular import for a type used in the code block (convenience method accepting Class). + * Adding an import for a type used in the code block (convenience overload accepting Class). * * @param clazz the class to import */ - public void addImport(Class clazz) { + public void addCodeBlockImport(Class clazz) { addCodeBlockImport(TypeName.of(clazz)); } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java index a1158e4c..7f1b6dc9 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java @@ -140,7 +140,6 @@ private static void finalizeDefinition( builderDto.addMethod(method); } } - for (FieldDto field : builderDto.getSetterFieldsForBuilder()) { for (MethodDto method : field.getMethods()) { builderDto.addMethod(method); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ImportCollector.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ImportCollector.java index 20716ffe..5d2beef4 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ImportCollector.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ImportCollector.java @@ -49,9 +49,9 @@ public ImportCollector(TypeName currentType) { * * @param type the type to add imports for */ - public void addTypeImports(TypeName type) { + public ImportCollector addTypeImports(TypeName type) { if (type == null || type instanceof TypeNamePrimitive || type instanceof TypeNameVariable) { - return; + return this; } // Add type annotation imports @@ -59,6 +59,8 @@ public void addTypeImports(TypeName type) { // Adding the type addImport(type); + + return this; } /** @@ -66,10 +68,11 @@ public void addTypeImports(TypeName type) { * * @param annotation the annotation to add imports for */ - public void addAnnotationImports(AnnotationDto annotation) { + public ImportCollector addAnnotationImports(AnnotationDto annotation) { if (annotation.getAnnotationType() != null) { addTypeImports(annotation.getAnnotationType()); } + return this; } /** @@ -77,9 +80,10 @@ public void addAnnotationImports(AnnotationDto annotation) { * * @param parameter the parameter to add imports for */ - public void addParameterImports(MethodParameterDto parameter) { + public ImportCollector addParameterImports(MethodParameterDto parameter) { addTypeImports(parameter.getParameterType()); parameter.getAnnotations().forEach(this::addAnnotationImports); + return this; } /** @@ -87,13 +91,14 @@ public void addParameterImports(MethodParameterDto parameter) { * * @param interfaceName the interface to add imports for */ - public void addInterfaceImports(InterfaceName interfaceName) { + public ImportCollector addInterfaceImports(InterfaceName interfaceName) { // Create a TypeName from the interface's package and simple name TypeName interfaceType = new TypeName(interfaceName.getPackageName(), interfaceName.getSimpleName()); addTypeImports(interfaceType); interfaceName.getAnnotations().forEach(this::addAnnotationImports); interfaceName.getTypeParameters().forEach(this::addTypeImports); + return this; } /** @@ -101,7 +106,7 @@ public void addInterfaceImports(InterfaceName interfaceName) { * * @param method the method to add imports for */ - public void addMethodImports(MethodDto method) { + public ImportCollector addMethodImports(MethodDto method) { if (method.getReturnType() != null) { addTypeImports(method.getReturnType()); } @@ -122,6 +127,8 @@ public void addMethodImports(MethodDto method) { // Add body argument imports addBodyImports(method); + + return this; } /** @@ -129,7 +136,7 @@ public void addMethodImports(MethodDto method) { * * @param classDef the class definition to extract imports from */ - public void collectImports(GenerationTargetClassDto classDef) { + public ImportCollector collectImports(GenerationTargetClassDto classDef) { // Add imports from the DTO (including static imports like TrackedValue) classDef.getImports().forEach(this::addImport); @@ -180,6 +187,8 @@ public void collectImports(GenerationTargetClassDto classDef) { // Add method imports classDef.getMethods().forEach(this::addMethodImports); + + return this; } /** @@ -306,4 +315,14 @@ public Set getSortedImports() { public Set getImports() { return new LinkedHashSet<>(imports); } + + /** + * Collects and sorts imports for a given class definition. + * + * @param classDef the class definition to collect imports from + * @return sorted set of import statements + */ + public static Set collectAndSortImports(GenerationTargetClassDto classDef) { + return new ImportCollector(classDef.getTypeName()).collectImports(classDef).getSortedImports(); + } } From 477d9289cdcfd0f2a72dc45c44ea50004dfbc10f Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Thu, 23 Apr 2026 22:03:08 +0200 Subject: [PATCH 30/30] Removing unused private helper function --- .../processor/classgen/roaster/RoasterCodeGenerator.java | 9 --------- 1 file changed, 9 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java index 6c22e573..a718849a 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java @@ -483,15 +483,6 @@ private void addParameter( applyAnnotations(parameter, paramDto.getAnnotations()); } - private Set collectImports(GenerationTargetClassDto classDef) { - ImportCollector collector = new ImportCollector(classDef.getTypeName()); - - // ImportCollector handles all import extraction from the DTO - collector.collectImports(classDef); - - return collector.getSortedImports(); - } - private String formatSource(String rawSource) { if (formatterProperties.isEmpty()) { return rawSource;