From 675656813cedb260d851e716e39c8e208cd47ad3 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Tue, 17 Mar 2026 19:35:38 +0100 Subject: [PATCH 01/20] Implementation of first version using StringBuilder instead of Javapoet --- README.md | 4 +- processor/pom.xml | 12 + .../builders/processor/BuilderProcessor.java | 6 +- .../roaster/RoasterCodeGenerator.java | 957 ++++++++++++++++++ .../classgen/roaster/RoasterMapper.java | 263 +++++ .../exceptions/RoasterMapperException.java | 59 ++ .../roaster/exceptions/package-info.java | 9 + .../classgen/roaster/package-info.java | 25 + 8 files changed, 1330 insertions(+), 5 deletions(-) create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterMapper.java create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/exceptions/RoasterMapperException.java create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/exceptions/package-info.java create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/package-info.java diff --git a/README.md b/README.md index 1b7fbabd..00d55078 100644 --- a/README.md +++ b/README.md @@ -364,7 +364,7 @@ This project was made possible thanks to the following: ### Tools and Libraries -- **[JavaPoet](https://github.com/palantir/javapoet)** - An excellent library for generating Java source code. Originally created by Square, now maintained by Palantir. JavaPoet made it straightforward to generate clean, readable builder code. +- **[Roaster](https://github.com/forge/roaster)** - A fluent Java source generation and formatting library from the JBoss Forge ecosystem. Roaster is used to generate and format the builder source code. - **[Google Compile Testing](https://github.com/google/compile-testing)** - Essential for testing annotation processors with comprehensive compilation diagnostics. ### Learning Resources @@ -372,7 +372,7 @@ This project was made possible thanks to the following: The following resources were invaluable for understanding annotation processing: - **[Baeldung: Java Annotation Processing and Creating a Builder](https://www.baeldung.com/java-annotation-processing-builder)** - Comprehensive guide to annotation processing fundamentals -- **[SkyRo Tech: Code Generation with JavaPoet in Practice](https://medium.com/skyro-tech/code-generation-with-javapoet-on-practice-bfbe8ca56a61)** - Practical examples of using JavaPoet +- **[Roaster GitHub Repository](https://github.com/forge/roaster)** - Reference for Java source generation and formatting with Roaster - **[Annotation Processing Demo](https://github.com/ledungcobra/annotation-processing-demo)** by Le Dung - Hands-on examples of annotation processor implementation Thank you to all contributors and the Java community for making this project possible! diff --git a/processor/pom.xml b/processor/pom.xml index 7bec0bf3..9bf0d7e1 100644 --- a/processor/pom.xml +++ b/processor/pom.xml @@ -55,6 +55,7 @@ 1.1.1 0.12.0 + 2.31.0.Final 3.20.0 4.5.0 6.0.3 @@ -115,6 +116,17 @@ javapoet ${javapoet.version} + + org.jboss.forge.roaster + roaster-api + ${roaster.version} + + + org.jboss.forge.roaster + roaster-jdt + ${roaster.version} + runtime + 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 f578e00a..5ab749ff 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 @@ -42,7 +42,7 @@ import javax.lang.model.element.TypeElement; import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; import org.javahelpers.simple.builders.core.annotations.SimpleBuilder.Template; -import org.javahelpers.simple.builders.processor.classgen.javapoet.JavaCodeGenerator; +import org.javahelpers.simple.builders.processor.classgen.roaster.RoasterCodeGenerator; import org.javahelpers.simple.builders.processor.exceptions.BuilderException; import org.javahelpers.simple.builders.processor.generators.integration.JacksonModuleGenerator; import org.javahelpers.simple.builders.processor.model.core.BuilderConfiguration; @@ -62,7 +62,7 @@ @SupportedAnnotationTypes("*") public class BuilderProcessor extends AbstractProcessor { private ProcessingContext context; - private JavaCodeGenerator codeGenerator; + private RoasterCodeGenerator codeGenerator; private JacksonModuleGenerator jacksonModuleGenerator; private boolean supportedJdk = true; @@ -78,7 +78,7 @@ public synchronized void init(ProcessingEnvironment processingEnv) { logger.debug("Loaded global configuration from compiler arguments: %s", globalConfig); this.context = new ProcessingContext(logger, globalConfig, processingEnv); - this.codeGenerator = new JavaCodeGenerator(processingEnv, logger); + this.codeGenerator = new RoasterCodeGenerator(processingEnv, logger); this.jacksonModuleGenerator = new JacksonModuleGenerator(processingEnv, logger); // Initialize GeneratorRegistry once during processor initialization 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 new file mode 100644 index 00000000..4b5c3832 --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java @@ -0,0 +1,957 @@ +/* + * 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.classgen.roaster; + +import static javax.lang.model.element.Modifier.PUBLIC; +import static org.javahelpers.simple.builders.processor.classgen.roaster.RoasterMapper.mapAnnotation; +import static org.javahelpers.simple.builders.processor.classgen.roaster.RoasterMapper.mapAnnotations; +import static org.javahelpers.simple.builders.processor.classgen.roaster.RoasterMapper.mapBoxedType; +import static org.javahelpers.simple.builders.processor.classgen.roaster.RoasterMapper.mapGenericDeclaration; +import static org.javahelpers.simple.builders.processor.classgen.roaster.RoasterMapper.mapType; +import static org.javahelpers.simple.builders.processor.classgen.roaster.RoasterMapper.quote; +import static org.javahelpers.simple.builders.processor.classgen.roaster.RoasterMapper.resolveCodeTemplate; + +import java.io.IOException; +import java.io.Writer; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +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.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.core.BuilderDefinitionDto; +import org.javahelpers.simple.builders.processor.model.core.FieldDto; +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.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; + +/** Roaster-based code generator for builder source files. */ +public class RoasterCodeGenerator { + private static final String INDENT = " "; + private static final String DOUBLE_INDENT = INDENT + INDENT; + private static final String TRIPLE_INDENT = DOUBLE_INDENT + INDENT; + + /** Processing environment for accessing filer and element utilities. */ + private final ProcessingEnvironment processingEnv; + + /** Logger for debug output during code generation. */ + private final ProcessingLogger logger; + + /** + * Constructor for RoasterCodeGenerator. + * + * @param processingEnv Processing environment for accessing filer and element utilities + * @param logger Logger for debug output + */ + public RoasterCodeGenerator(ProcessingEnvironment processingEnv, ProcessingLogger logger) { + this.processingEnv = processingEnv; + this.logger = logger; + } + + /** + * Generates a builder class from the given builder definition. + * + * @param builderDef dto of all information to create the builder + * @throws BuilderException if there is an error in source code generation + */ + public void generateBuilder(BuilderDefinitionDto builderDef) throws BuilderException { + logger.debugStartOperation( + "Code generation for builder: %s", builderDef.getBuilderTypeName().getClassName()); + + String sourceCode = createBuilderSource(builderDef); + writeBuilderClassToFile(sourceCode, builderDef); + + logger.debugEndOperation( + "Successfully generated builder: %s", builderDef.getBuilderTypeName().getClassName()); + } + + private String createBuilderSource(BuilderDefinitionDto builderDef) { + logger.debug("Creating builder source for %s", builderDef.getBuilderTypeName()); + + StringBuilder source = new StringBuilder(); + String packageName = builderDef.getBuilderTypeName().getPackageName(); + if (StringUtils.isNotBlank(packageName)) { + source.append("package ").append(packageName).append(";\n\n"); + } + appendTrackedValueStaticImports(source); + appendImports(source, collectImports(builderDef)); + + logger.debug("Class builder created"); + + appendClassJavadoc(source, builderDef.getClassJavadoc(), ""); + logger.debug("Class metadata added"); + appendAnnotations(source, builderDef.getClassAnnotations(), ""); + source.append(buildClassHeader(builderDef)).append(" {\n\n"); + + appendFields(source, builderDef); + appendConstructors(source, builderDef); + appendMethods(source, builderDef); + appendNestedTypes(source, builderDef); + logger.debug("Class-level annotations added"); + + trimTrailingBlankLinesBeforeClassClosingBrace(source); + source.append("\n}\n"); + logger.debug("Builder source created"); + return formatSource(source.toString()); + } + + private void trimTrailingBlankLinesBeforeClassClosingBrace(StringBuilder source) { + while (source.length() > 0 && Character.isWhitespace(source.charAt(source.length() - 1))) { + source.deleteCharAt(source.length() - 1); + } + } + + private String buildClassHeader(BuilderDefinitionDto builderDef) { + StringBuilder header = new StringBuilder(); + Modifier builderAccessModifier = + JavaLangMapper.mapAccessModifier(builderDef.getConfiguration().getBuilderAccess()); + if (builderAccessModifier != null) { + header.append(toSourceModifier(builderAccessModifier)).append(" "); + } + header + .append("class ") + .append(builderDef.getBuilderTypeName().getClassName()) + .append(mapGenericDeclaration(builderDef.getGenerics())); + + if (CollectionUtils.isNotEmpty(builderDef.getInterfaces())) { + String interfaces = + builderDef.getInterfaces().stream() + .map(RoasterMapper::mapInterfaceToTypeName) + .reduce((a, b) -> a + ", " + b) + .orElse(""); + header.append(" implements ").append(interfaces); + } + return header.toString(); + } + + private void appendFields(StringBuilder source, BuilderDefinitionDto builderDef) { + logger.debugStartOperation( + "Generating %d constructor fields and %d setter fields", + builderDef.getConstructorFieldsForBuilder().size(), + builderDef.getSetterFieldsForBuilder().size()); + + for (FieldDto fieldDto : builderDef.getConstructorFieldsForBuilder()) { + appendField(source, fieldDto); + } + for (FieldDto fieldDto : builderDef.getSetterFieldsForBuilder()) { + appendField(source, fieldDto); + } + + logger.debugEndOperation("Fields added: %d fields", builderDef.getAllFieldsForBuilder().size()); + } + + private void appendField(StringBuilder source, FieldDto fieldDto) { + String boxedFieldType = mapBoxedType(fieldDto.getFieldType()); + appendJavadoc( + source, + "Tracked value for %s: %s." + .formatted( + fieldDto.getFieldNameInBuilder(), StringUtils.defaultString(fieldDto.getJavaDoc())), + INDENT); + source + .append(INDENT) + .append("private ") + .append(TrackedValue.class.getSimpleName()) + .append("<") + .append(boxedFieldType) + .append("> ") + .append(fieldDto.getFieldNameInBuilder()) + .append(" = ") + .append("unsetValue();\n\n"); + } + + private void appendConstructors(StringBuilder source, BuilderDefinitionDto builderDef) { + generateConstructors(source, builderDef); + logger.debug("Constructors added"); + } + + private void generateConstructors(StringBuilder source, BuilderDefinitionDto builderDef) { + Modifier constructorAccessModifier = + JavaLangMapper.mapAccessModifier( + builderDef.getConfiguration().getBuilderConstructorAccess()); + TypeName dtoBaseClass = builderDef.getBuildingTargetTypeName(); + + appendEmptyConstructor( + source, + dtoBaseClass, + builderDef.getBuilderTypeName().getClassName(), + constructorAccessModifier); + source.append("\n"); + appendConstructorWithInstance( + source, + dtoBaseClass, + builderDef.getBuilderTypeName().getClassName(), + builderDef.getAllFieldsForBuilder(), + constructorAccessModifier); + source.append("\n"); + } + + private void appendEmptyConstructor( + StringBuilder source, TypeName dtoClass, String builderClassName, Modifier accessModifier) { + appendJavadoc( + source, + "Empty constructor of builder for {@code %s}.".formatted(dtoClass.getFullQualifiedName()), + INDENT); + source + .append(INDENT) + .append(buildMethodPrefix(accessModifier, false, null)) + .append(builderClassName) + .append("() {\n") + .append(INDENT) + .append("}\n"); + } + + private void appendConstructorWithInstance( + StringBuilder source, + TypeName dtoBaseClass, + String builderClassName, + List fields, + Modifier accessModifier) { + appendJavadoc( + source, + """ + Initialisation of builder for {@code %s} by a instance. + + @param instance object instance for initialisiation + """ + .formatted(dtoBaseClass.getFullQualifiedName()), + INDENT); + source + .append(INDENT) + .append(buildMethodPrefix(accessModifier, false, null)) + .append(builderClassName) + .append("(") + .append(mapType(dtoBaseClass)) + .append(" instance) {\n"); + + String body = buildConstructorBody(fields); + appendIndentedBody(source, body, DOUBLE_INDENT); + source.append(INDENT).append("}\n"); + } + + 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(); + body.append("this.") + .append(fieldInBuilder) + .append(" = initialValue(instance.") + .append(getter) + .append("());\n"); + + if (field.isNonNullable()) { + body.append("if (this.") + .append(fieldInBuilder) + .append(".value() == null) {\n") + .append(INDENT) + .append("throw new ") + .append(IllegalArgumentException.class.getSimpleName()) + .append("(") + .append( + quote( + "Cannot initialize builder from instance: field '" + + fieldInBuilder + + "' is marked as non-null but source object has null value")) + .append(");\n") + .append("}\n"); + } + } + + private void appendMethods(StringBuilder source, BuilderDefinitionDto builderDef) { + Map allMethods = collectAllMethods(builderDef); + logger.debugStartOperation("Adding Methods for %d candidates", allMethods.size()); + + List resolvedMethods = resolveMethodConflicts(allMethods); + logger.debug("Resolved %d methods after conflict resolution", resolvedMethods.size()); + + int generatedCnt = 0; + for (MethodDto methodDto : resolvedMethods) { + appendMethod(source, methodDto, false, false); + source.append("\n"); + generatedCnt++; + } + logger.debugEndOperation("%d Methods added", generatedCnt); + } + + 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); + } + + return allMethods; + } + + private List resolveMethodConflicts(Map methodToField) { + MethodDto.MethodComparator comparator = new MethodDto.MethodComparator(); + + List> sortedEntries = + methodToField.entrySet().stream() + .sorted((e1, e2) -> comparator.compare(e1.getKey(), e2.getKey())) + .toList(); + + Map signatureToMethod = new java.util.LinkedHashMap<>(); + + for (Map.Entry entry : sortedEntries) { + MethodDto method = entry.getKey(); + FieldDto field = entry.getValue(); + String signature = method.getSignatureKey(); + + MethodDto existing = signatureToMethod.get(signature); + if (existing == null) { + signatureToMethod.put(signature, method); + } else { + String existingSource = getSourceDescription(existing, methodToField.get(existing)); + String newSource = getSourceDescription(method, field); + + 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()); + } 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()); + } 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()); + } + } + } + + return new java.util.ArrayList<>(signatureToMethod.values()); + } + + private String getSourceDescription(MethodDto method, FieldDto field) { + if (field == null) { + return "core method '" + method.getMethodName() + "'"; + } + return "field '" + field.getFieldNameInBuilder() + "'"; + } + + private void appendMethod( + StringBuilder source, + MethodDto methodDto, + boolean nestedTypeMethod, + boolean interfaceMethod) { + appendJavadoc(source, methodDto.getJavadoc(), INDENT); + appendAnnotations(source, methodDto.getAnnotations(), INDENT); + + source + .append(INDENT) + .append(buildMethodSignature(methodDto, nestedTypeMethod, interfaceMethod)) + .append(" {"); + + String body = + methodDto.getMethodCodeDto() != null + && StringUtils.isNotBlank(methodDto.getMethodCodeDto().getCodeFormat()) + ? resolveCodeTemplate(methodDto.getMethodCodeDto()) + : ""; + + if (StringUtils.isNotBlank(body)) { + source.append("\n"); + appendIndentedBody(source, body, DOUBLE_INDENT); + source.append(INDENT); + } + source.append("}\n"); + } + + private String buildMethodSignature( + MethodDto methodDto, boolean nestedTypeMethod, boolean interfaceMethod) { + StringBuilder signature = new StringBuilder(); + Modifier modifier = methodDto.getModifier().orElse(null); + String genericDeclaration = mapGenericDeclaration(methodDto.getGenericParameters()); + + if (nestedTypeMethod) { + signature.append(PUBLIC.toString().toLowerCase()).append(" "); + if (interfaceMethod + && methodDto.getMethodCodeDto() != null + && StringUtils.isNotBlank(methodDto.getMethodCodeDto().getCodeFormat())) { + signature.append("default "); + } + } else { + signature.append(buildMethodPrefix(modifier, methodDto.isStatic(), genericDeclaration)); + } + + if (nestedTypeMethod && StringUtils.isNotBlank(genericDeclaration)) { + signature.append(genericDeclaration).append(" "); + } + + signature + .append(mapType(methodDto.getReturnType())) + .append(" ") + .append(methodDto.getMethodName()) + .append("(") + .append(buildParameterList(methodDto.getParameters())) + .append(")"); + return signature.toString(); + } + + private String buildMethodPrefix(Modifier modifier, boolean isStatic, String genericDeclaration) { + StringBuilder prefix = new StringBuilder(); + if (modifier != null) { + prefix.append(toSourceModifier(modifier)).append(" "); + } + if (isStatic) { + prefix.append("static "); + } + if (StringUtils.isNotBlank(genericDeclaration)) { + prefix.append(genericDeclaration).append(" "); + } + return prefix.toString(); + } + + private String buildParameterList(List parameters) { + StringBuilder result = new StringBuilder(); + for (int i = 0; i < parameters.size(); i++) { + if (i > 0) { + result.append(", "); + } + result.append(buildParameter(parameters.get(i), i == parameters.size() - 1)); + } + return result.toString(); + } + + private String buildParameter(MethodParameterDto paramDto, boolean lastParameter) { + StringBuilder parameter = new StringBuilder(); + if (CollectionUtils.isNotEmpty(paramDto.getAnnotations())) { + parameter + .append( + mapAnnotations(paramDto.getAnnotations()).stream() + .reduce((a, b) -> a + " " + b) + .orElse("")) + .append(" "); + } + + if (lastParameter && paramDto.getParameterType() instanceof TypeNameArray arrayType) { + parameter + .append(mapType(arrayType.getTypeOfArray())) + .append("...") + .append(" ") + .append(paramDto.getParameterName()); + } else { + parameter + .append(mapType(paramDto.getParameterType())) + .append(" ") + .append(paramDto.getParameterName()); + } + return parameter.toString(); + } + + private void appendNestedTypes(StringBuilder source, BuilderDefinitionDto builderDef) { + if (CollectionUtils.isEmpty(builderDef.getNestedTypes())) { + return; + } + logger.debugStartOperation("Generating %d nested type(s)", builderDef.getNestedTypes().size()); + for (NestedTypeDto nestedType : builderDef.getNestedTypes()) { + appendNestedType(source, nestedType); + source.append("\n"); + logger.debug("Generated nested type: %s", nestedType.getTypeName()); + } + logger.debugEndOperation("Nested types added"); + } + + private void appendNestedType(StringBuilder source, NestedTypeDto nestedType) { + appendJavadoc(source, nestedType.getJavadoc(), INDENT); + source.append(INDENT); + if (nestedType.isPublic()) { + source.append(PUBLIC.toString().toLowerCase()).append(" "); + } + source + .append( + nestedType.getKind() == NestedTypeDto.NestedTypeKind.INTERFACE + ? "interface " + : "class ") + .append(nestedType.getTypeName()) + .append(" {\n\n"); + + boolean isInterface = nestedType.getKind() == NestedTypeDto.NestedTypeKind.INTERFACE; + for (MethodDto methodDto : nestedType.getMethods()) { + appendNestedMethod(source, methodDto, isInterface); + source.append("\n"); + } + + source.append(INDENT).append("}\n"); + } + + private void appendNestedMethod(StringBuilder source, MethodDto methodDto, boolean isInterface) { + appendJavadoc(source, methodDto.getJavadoc(), DOUBLE_INDENT); + appendAnnotations(source, methodDto.getAnnotations(), DOUBLE_INDENT); + + source.append(DOUBLE_INDENT); + StringBuilder signature = new StringBuilder(); + String genericDeclaration = mapGenericDeclaration(methodDto.getGenericParameters()); + if (StringUtils.isNotBlank(genericDeclaration)) { + signature.append(genericDeclaration).append(" "); + } + if (isInterface + && methodDto.getMethodCodeDto() != null + && StringUtils.isNotBlank(methodDto.getMethodCodeDto().getCodeFormat())) { + signature.append("default "); + } else { + signature.append(PUBLIC.toString().toLowerCase()).append(" "); + } + signature + .append(mapType(methodDto.getReturnType())) + .append(" ") + .append(methodDto.getMethodName()) + .append("(") + .append(buildParameterList(methodDto.getParameters())) + .append(")"); + source.append(signature); + + if (methodDto.getMethodCodeDto() == null + || StringUtils.isBlank(methodDto.getMethodCodeDto().getCodeFormat())) { + source.append(";\n"); + return; + } + + source.append(" {\n"); + appendIndentedBody(source, resolveCodeTemplate(methodDto.getMethodCodeDto()), TRIPLE_INDENT); + source.append(DOUBLE_INDENT).append("}\n"); + } + + private void appendClassJavadoc(StringBuilder source, String javadoc, String indent) { + appendJavadoc(source, javadoc, indent); + } + + private void appendImports(StringBuilder source, Set imports) { + if (imports.isEmpty()) { + return; + } + imports.stream() + .sorted() + .forEach(value -> source.append("import ").append(value).append(";\n")); + source.append("\n"); + } + + private void appendTrackedValueStaticImports(StringBuilder source) { + source.append("import static ").append(TrackedValue.class.getName()).append(".changedValue;\n"); + source.append("import static ").append(TrackedValue.class.getName()).append(".initialValue;\n"); + source.append("import static ").append(TrackedValue.class.getName()).append(".unsetValue;\n\n"); + } + + private Set collectImports(BuilderDefinitionDto builderDef) { + Set imports = new LinkedHashSet<>(); + String currentPackage = builderDef.getBuilderTypeName().getPackageName(); + + addImportIfNeeded(imports, currentPackage, TrackedValue.class.getName()); + addTypeImports(imports, currentPackage, builderDef.getBuildingTargetTypeName()); + addTypeImports(imports, currentPackage, builderDef.getBuilderTypeName()); + builderDef + .getGenerics() + .forEach( + generic -> + generic + .getUpperBounds() + .forEach(type -> addTypeImports(imports, currentPackage, type))); + builderDef + .getInterfaces() + .forEach(interfaceName -> addInterfaceImports(imports, currentPackage, interfaceName)); + builderDef + .getClassAnnotations() + .forEach(annotation -> addAnnotationImports(imports, currentPackage, annotation)); + builderDef + .getAllFieldsForBuilder() + .forEach(field -> addFieldImports(imports, currentPackage, field)); + builderDef + .getCoreMethods() + .forEach(method -> addMethodImports(imports, currentPackage, method)); + builderDef + .getNestedTypes() + .forEach( + nestedType -> + nestedType + .getMethods() + .forEach(method -> addMethodImports(imports, currentPackage, method))); + + 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()); + } + method + .getGenericParameters() + .forEach( + generic -> + generic + .getUpperBounds() + .forEach(type -> addTypeImports(imports, currentPackage, type))); + 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()); + } + + private void addImportIfNeeded(Set imports, String currentPackage, String fqn) { + if (StringUtils.isBlank(fqn) + || !fqn.contains(".") + || fqn.startsWith("java.lang.") + || currentPackage.equals(packageNameOf(fqn))) { + return; + } + imports.add(fqn); + } + + private String packageNameOf(String fqn) { + int idx = fqn.lastIndexOf('.'); + return idx < 0 ? "" : fqn.substring(0, idx); + } + + private void addBodyImports(Set imports, String currentPackage, MethodDto method) { + if (method.getMethodCodeDto() == null + || StringUtils.isBlank(method.getMethodCodeDto().getCodeFormat())) { + 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()); + } + } + + private void appendJavadoc(StringBuilder source, String javadoc, String indent) { + if (StringUtils.isBlank(javadoc)) { + return; + } + source.append(indent).append("/**\n"); + String normalized = javadoc.replace("\r\n", "\n").replace("\r", "\n").replaceFirst("\\n+$", ""); + for (String line : normalized.split("\n", -1)) { + source.append(indent).append(" *"); + if (!line.isEmpty()) { + source.append(" ").append(line.replace("*/", "* /")); + } + source.append("\n"); + } + source.append(indent).append(" */\n"); + } + + private void appendAnnotations( + StringBuilder source, java.util.Collection annotations, String indent) { + if (CollectionUtils.isEmpty(annotations)) { + return; + } + for (AnnotationDto annotation : annotations) { + source.append(indent).append(mapAnnotation(annotation)).append("\n"); + } + } + + private void appendIndentedBody(StringBuilder source, String body, String indent) { + if (StringUtils.isBlank(body)) { + return; + } + String normalized = body.replace("\r\n", "\n").replace("\r", "\n").replaceFirst("\\n+$", ""); + for (String line : normalized.split("\n", -1)) { + if (line.isEmpty()) { + source.append(indent).append("\n"); + } else { + source.append(indent).append(line).append("\n"); + } + } + } + + private String toSourceModifier(Modifier modifier) { + return modifier.toString().toLowerCase(); + } + + private String formatSource(String rawSource) { + return rawSource; + } + + private void writeBuilderClassToFile(String sourceCode, BuilderDefinitionDto builderDef) + throws BuilderException { + logger.debug( + "Writing builder class to file: %s.%s", + builderDef.getBuilderTypeName().getPackageName(), + builderDef.getBuilderTypeName().getClassName()); + + String qualifiedName = builderDef.getBuilderTypeName().getFullQualifiedName(); + if (builderClassAlreadyExists(qualifiedName)) { + throw new BuilderException( + null, + """ + Builder class '%s' already exists. This may be a manually written builder or a previously generated builder. + To resolve this: + 1. If you have a manual builder, consider renaming it or removing @SimpleBuilder from the DTO + 2. If this is from a previous compilation, clean and rebuild the project + 3. Check that you're not trying to generate multiple builders for the same DTO + """ + .formatted(qualifiedName)); + } + + try { + 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 builder class '%s': %s. + Check the build environment and ensure all necessary directories are accessible. + """ + .formatted( + qualifiedName, StringUtils.isNotBlank(message) ? message : "Unknown error"); + throw new BuilderException(null, errorMessage); + } + } + + /** + * Checks if a builder class already exists by attempting to find the type element. + * + * @param qualifiedName the fully qualified name of the class to check + * @return true if the class already exists, false otherwise + */ + private boolean builderClassAlreadyExists(String qualifiedName) { + try { + TypeElement existingType = processingEnv.getElementUtils().getTypeElement(qualifiedName); + return existingType != null; + } catch (Exception e) { + logger.debug( + "Error checking if builder class '%s' already exists: %s", + qualifiedName, StringUtils.isNotBlank(e.getMessage()) ? e.getMessage() : "No message"); + 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); + + StringBuilder source = new StringBuilder(); + if (StringUtils.isNotBlank(packageName)) { + source.append("package ").append(packageName).append(";\n\n"); + } + source.append("import com.fasterxml.jackson.databind.annotation.JsonDeserialize;\n"); + source.append("import com.fasterxml.jackson.databind.module.SimpleModule;\n"); + for (JacksonModuleEntryDto entry : moduleDef.getEntries()) { + appendModuleImport(source, packageName, entry.dtoType().getFullQualifiedName()); + appendModuleImport(source, packageName, entry.builderType().getFullQualifiedName()); + } + source.append("\n"); + + source + .append("public class ") + .append(moduleClassName) + .append(" extends ") + .append("SimpleModule") + .append(" {\n\n"); + + for (JacksonModuleEntryDto entry : moduleDef.getEntries()) { + String mixinName = entry.dtoType().getClassName() + "Mixin"; + source + .append(INDENT) + .append("@JsonDeserialize(builder = ") + .append(entry.builderType().getClassName()) + .append(".class)\n") + .append(INDENT) + .append("private interface ") + .append(mixinName) + .append(" {}\n\n"); + } + + source.append(INDENT).append("public ").append(moduleClassName).append("() {\n"); + for (JacksonModuleEntryDto entry : moduleDef.getEntries()) { + String mixinName = entry.dtoType().getClassName() + "Mixin"; + source + .append(DOUBLE_INDENT) + .append("setMixInAnnotation(") + .append(entry.dtoType().getClassName()) + .append(".class, ") + .append(mixinName) + .append(".class);\n"); + } + source.append(INDENT).append("}\n"); + source.append("}\n"); + + try { + writeSimpleClassToFile(packageName, moduleClassName, formatSource(source.toString())); + } 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 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); + } + } + + private void appendModuleImport(StringBuilder source, String currentPackage, String fqn) { + if (StringUtils.isBlank(fqn) + || !fqn.contains(".") + || fqn.startsWith("java.lang.") + || currentPackage.equals(packageNameOf(fqn))) { + return; + } + source.append("import ").append(fqn).append(";\n"); + } +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterMapper.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterMapper.java new file mode 100644 index 00000000..a70c1752 --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterMapper.java @@ -0,0 +1,263 @@ +/* + * 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.classgen.roaster; + +import java.util.List; +import java.util.Map; +import org.apache.commons.collections4.CollectionUtils; +import org.javahelpers.simple.builders.processor.classgen.roaster.exceptions.RoasterMapperException; +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.method.MethodCodeDto; +import org.javahelpers.simple.builders.processor.model.method.MethodCodePlaceholder; +import org.javahelpers.simple.builders.processor.model.method.MethodCodeStringPlaceholder; +import org.javahelpers.simple.builders.processor.model.method.MethodCodeTypePlaceholder; +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.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; + +/** Helper functions to create Roaster-compatible source code strings from DTOs. */ +public final class RoasterMapper { + + private RoasterMapper() {} + + /** + * Maps a type model to Java source code using fully qualified names for robustness. + * + * @param typeName type to map + * @return Java source representation of the type + */ + public static String mapType(TypeName typeName) { + String mappedType; + if (typeName instanceof TypeNameVariable) { + mappedType = typeName.getClassName(); + } else if (typeName instanceof TypeNamePrimitive primitive) { + mappedType = primitive.getFullQualifiedName(); + } else if (typeName instanceof TypeNameArray arrayType) { + mappedType = mapType(arrayType.getTypeOfArray()) + "[]"; + } else if (typeName instanceof TypeNameGeneric genericType) { + mappedType = mapGenericType(genericType); + } else { + mappedType = typeName.getClassName(); + } + + return prependTypeUseAnnotations(mappedType, typeName.getAnnotations()); + } + + /** + * Maps a type to a boxed Java source representation. + * + * @param typeName type to map + * @return boxed Java source representation of the type + */ + public static String mapBoxedType(TypeName typeName) { + if (typeName instanceof TypeNamePrimitive primitive) { + String boxedType = + switch (primitive.getType()) { + case BOOLEAN -> Boolean.class.getSimpleName(); + case BYTE -> Byte.class.getSimpleName(); + case CHAR -> Character.class.getSimpleName(); + case DOUBLE -> Double.class.getSimpleName(); + case FLOAT -> Float.class.getSimpleName(); + case INT -> Integer.class.getSimpleName(); + case LONG -> Long.class.getSimpleName(); + case SHORT -> Short.class.getSimpleName(); + case VOID -> Void.class.getSimpleName(); + }; + return prependTypeUseAnnotations(boxedType, typeName.getAnnotations()); + } + return mapType(typeName); + } + + private static String mapGenericType(TypeNameGeneric genericType) { + if (genericType.getInnerTypeArguments().isEmpty()) { + return genericType.getClassName(); + } + String innerTypes = + genericType.getInnerTypeArguments().stream() + .map(RoasterMapper::mapBoxedType) + .reduce((a, b) -> a + ", " + b) + .orElse(""); + return genericType.getClassName() + "<" + innerTypes + ">"; + } + + /** + * Maps generic declarations like {@code }. + * + * @param generics generic parameter list + * @return source code for generic declaration or empty string + */ + public static String mapGenericDeclaration(List generics) { + if (CollectionUtils.isEmpty(generics)) { + return ""; + } + String value = + generics.stream() + .map(RoasterMapper::mapGenericDeclaration) + .reduce((a, b) -> a + ", " + b) + .orElse(""); + return "<" + value + ">"; + } + + private static String mapGenericDeclaration(GenericParameterDto genericParameter) { + if (CollectionUtils.isEmpty(genericParameter.getUpperBounds())) { + return genericParameter.getName(); + } + String bounds = + genericParameter.getUpperBounds().stream() + .map(RoasterMapper::mapType) + .reduce((a, b) -> a + " & " + b) + .orElse(""); + return genericParameter.getName() + " extends " + bounds; + } + + /** + * Maps an annotation to source code. + * + * @param annotationDto annotation DTO + * @return Java source representation of the annotation + */ + public static String mapAnnotation(AnnotationDto annotationDto) { + try { + String annotationType = mapType(annotationDto.getAnnotationType()); + if (annotationDto.getMembers().isEmpty()) { + return "@" + annotationType; + } + String members = + annotationDto.getMembers().entrySet().stream() + .map(RoasterMapper::mapAnnotationMember) + .reduce((a, b) -> a + ", " + b) + .orElse(""); + return "@" + annotationType + "(" + members + ")"; + } catch (Exception e) { + throw new RoasterMapperException( + e, + "Failed to map annotation %s: %s", + annotationDto.getAnnotationType().getClassName(), + e.getMessage()); + } + } + + private static String mapAnnotationMember(Map.Entry member) { + if ("value".equals(member.getKey())) { + return member.getValue(); + } + return member.getKey() + " = " + member.getValue(); + } + + /** + * Maps a list of annotations to source code lines. + * + * @param annotations annotations to map + * @return list of annotation strings + */ + public static List mapAnnotations(List annotations) { + return annotations.stream().map(RoasterMapper::mapAnnotation).toList(); + } + + /** + * Maps an interface name to Java source code. + * + * @param interfaceName interface model + * @return source representation of the interface type + */ + public static String mapInterfaceToTypeName(InterfaceName interfaceName) { + try { + String qualifiedName = interfaceName.getSimpleName(); + if (interfaceName.hasTypeParameters()) { + String typeParameters = + interfaceName.getTypeParameters().stream() + .map(RoasterMapper::mapType) + .reduce((a, b) -> a + ", " + b) + .orElse(""); + qualifiedName = qualifiedName + "<" + typeParameters + ">"; + } + return prependTypeUseAnnotations(qualifiedName, interfaceName.getAnnotations()); + } catch (Exception e) { + throw new RoasterMapperException( + e, "Failed to map interface %s: %s", interfaceName.toString(), e.getMessage()); + } + } + + /** + * Resolves the JavaPoet-style named template used in MethodCodeDto to plain Java source code. + * + * @param codeDto code template DTO + * @return resolved Java source code + */ + public static String resolveCodeTemplate(MethodCodeDto codeDto) { + String code = codeDto.getCodeFormat(); + for (MethodCodePlaceholder placeHolderValue : codeDto.getCodeArguments()) { + String label = placeHolderValue.getLabel(); + if (placeHolderValue instanceof MethodCodeStringPlaceholder stringPlaceholder) { + code = code.replace("$" + label + ":N", stringPlaceholder.getValue()); + code = code.replace("$" + label + ":L", stringPlaceholder.getValue()); + code = code.replace("$" + label + ":S", quote(stringPlaceholder.getValue())); + } else if (placeHolderValue instanceof MethodCodeTypePlaceholder typePlaceholder) { + code = code.replace("$" + label + ":T", mapType(typePlaceholder.getValue())); + } else { + throw new RoasterMapperException( + "Unsupported placeholder type: %s", placeHolderValue.getClass().getName()); + } + } + code = code.replace("TrackedValue.initialValue", "initialValue"); + code = code.replace("TrackedValue.changedValue", "changedValue"); + code = code.replace("TrackedValue.unsetValue", "unsetValue"); + return code; + } + + /** + * Converts plain text to a Java string literal. + * + * @param value text value + * @return quoted and escaped Java string literal + */ + public static String quote(String value) { + String escaped = + value + .replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t"); + return "\"" + escaped + "\""; + } + + private static String prependTypeUseAnnotations( + String baseType, List annotations) { + if (CollectionUtils.isEmpty(annotations)) { + return baseType; + } + String prefix = + annotations.stream() + .map(RoasterMapper::mapAnnotation) + .reduce((a, b) -> a + " " + b) + .orElse(""); + return prefix + " " + baseType; + } +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/exceptions/RoasterMapperException.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/exceptions/RoasterMapperException.java new file mode 100644 index 00000000..0d039883 --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/exceptions/RoasterMapperException.java @@ -0,0 +1,59 @@ +/* + * 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.classgen.roaster.exceptions; + +/** Special exception for errors in mapping to Roaster classes. */ +public class RoasterMapperException extends RuntimeException { + + /** + * Creating an exception with message and parameters. + * + * @param cause root cause of current exception, containing stacktrace + */ + public RoasterMapperException(Throwable cause) { + super(cause.getMessage(), cause); + } + + /** + * Creating an exception with message and parameters. + * + * @param message A specific message, supports String.format arguments + * @param args Arguments for String.format on message + */ + public RoasterMapperException(String message, Object... args) { + super(String.format(message, args)); + } + + /** + * Creating an exception with message and parameters. + * + * @param cause root cause of current exception, containing stacktrace + * @param message A specific message, supports String.format arguments + * @param args Arguments for String.format on message + */ + public RoasterMapperException(Throwable cause, String message, Object... args) { + super(String.format(message, args), cause); + } +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/exceptions/package-info.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/exceptions/package-info.java new file mode 100644 index 00000000..9c16ae95 --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/exceptions/package-info.java @@ -0,0 +1,9 @@ +/** + * Package org.javahelpers.simple.builders.processor.classgen.roaster.exceptions + * + *

Roaster-specific exception classes. + * + *

This package contains exceptions specific to Roaster code generation, providing error handling + * for mapping and code generation issues within the Roaster layer. + */ +package org.javahelpers.simple.builders.processor.classgen.roaster.exceptions; diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/package-info.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/package-info.java new file mode 100644 index 00000000..31bbf744 --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/package-info.java @@ -0,0 +1,25 @@ +/** + * Package org.javahelpers.simple.builders.processor.classgen.roaster + * + *

Roaster-based code generation for simple-builders processor. + * + *

This package contains the core code generation components: + * + *

    + *
  • {@link org.javahelpers.simple.builders.processor.classgen.roaster.RoasterCodeGenerator} - + * Main code generator that creates builder classes + *
  • {@link org.javahelpers.simple.builders.processor.classgen.roaster.RoasterMapper} - Utility + * for mapping DTOs to Java source strings + *
+ * + *

Exception classes are organized in separate packages: + * + *

    + *
  • {@link org.javahelpers.simple.builders.processor.exceptions.BuilderException} - Exception + * for code generation errors + *
  • {@link + * org.javahelpers.simple.builders.processor.classgen.roaster.exceptions.RoasterMapperException} + * - Exception for mapping errors + *
+ */ +package org.javahelpers.simple.builders.processor.classgen.roaster; From d526322dc29d740cebafe3cd14c76a8f80a43c16 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Tue, 17 Mar 2026 21:29:57 +0100 Subject: [PATCH 02/20] First implementation of RasterBased CodeGenerator --- .../roaster/RoasterCodeGenerator.java | 564 +++++++++++++----- 1 file changed, 409 insertions(+), 155 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 4b5c3832..bb698cef 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 @@ -50,6 +50,7 @@ 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.FieldDto; import org.javahelpers.simple.builders.processor.model.integration.JacksonModuleDefinitionDto; @@ -65,6 +66,15 @@ 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.jboss.forge.roaster.Roaster; +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.JavaInterfaceSource; +import org.jboss.forge.roaster.model.source.JavaSource; +import org.jboss.forge.roaster.model.source.MethodSource; +import org.jboss.forge.roaster.model.source.ParameterSource; +import org.jboss.forge.roaster.model.source.TypeVariableSource; /** Roaster-based code generator for builder source files. */ public class RoasterCodeGenerator { @@ -108,64 +118,53 @@ public void generateBuilder(BuilderDefinitionDto builderDef) throws BuilderExcep private String createBuilderSource(BuilderDefinitionDto builderDef) { logger.debug("Creating builder source for %s", builderDef.getBuilderTypeName()); - - StringBuilder source = new StringBuilder(); - String packageName = builderDef.getBuilderTypeName().getPackageName(); - if (StringUtils.isNotBlank(packageName)) { - source.append("package ").append(packageName).append(";\n\n"); - } - appendTrackedValueStaticImports(source); - appendImports(source, collectImports(builderDef)); - logger.debug("Class builder created"); - - appendClassJavadoc(source, builderDef.getClassJavadoc(), ""); + JavaClassSource source = createClassSource(builderDef); logger.debug("Class metadata added"); - appendAnnotations(source, builderDef.getClassAnnotations(), ""); - source.append(buildClassHeader(builderDef)).append(" {\n\n"); - appendFields(source, builderDef); appendConstructors(source, builderDef); appendMethods(source, builderDef); appendNestedTypes(source, builderDef); logger.debug("Class-level annotations added"); - - trimTrailingBlankLinesBeforeClassClosingBrace(source); - source.append("\n}\n"); logger.debug("Builder source created"); - return formatSource(source.toString()); + return formatSource(renderClassSource(source, builderDef)); } - private void trimTrailingBlankLinesBeforeClassClosingBrace(StringBuilder source) { - while (source.length() > 0 && Character.isWhitespace(source.charAt(source.length() - 1))) { - source.deleteCharAt(source.length() - 1); + private JavaClassSource createClassSource(BuilderDefinitionDto builderDef) { + JavaClassSource source = Roaster.create(JavaClassSource.class); + String packageName = builderDef.getBuilderTypeName().getPackageName(); + if (StringUtils.isNotBlank(packageName)) { + source.setPackage(packageName); + } else { + source.setDefaultPackage(); } - } - - private String buildClassHeader(BuilderDefinitionDto builderDef) { - StringBuilder header = new StringBuilder(); - Modifier builderAccessModifier = - JavaLangMapper.mapAccessModifier(builderDef.getConfiguration().getBuilderAccess()); - if (builderAccessModifier != null) { - header.append(toSourceModifier(builderAccessModifier)).append(" "); + source.setName(builderDef.getBuilderTypeName().getClassName()); + applyVisibility( + source, JavaLangMapper.mapAccessModifier(builderDef.getConfiguration().getBuilderAccess())); + addGenericDeclarations(source, builderDef.getGenerics()); + collectImports(builderDef).forEach(source::addImport); + for (InterfaceName interfaceName : builderDef.getInterfaces()) { + source.addInterface(RoasterMapper.mapInterfaceToTypeName(interfaceName)); } - header - .append("class ") - .append(builderDef.getBuilderTypeName().getClassName()) - .append(mapGenericDeclaration(builderDef.getGenerics())); + applyJavadoc(source, builderDef.getClassJavadoc()); + applyAnnotations(source, builderDef.getClassAnnotations()); + return source; + } - if (CollectionUtils.isNotEmpty(builderDef.getInterfaces())) { - String interfaces = - builderDef.getInterfaces().stream() - .map(RoasterMapper::mapInterfaceToTypeName) - .reduce((a, b) -> a + ", " + b) - .orElse(""); - header.append(" implements ").append(interfaces); - } - return header.toString(); + private String renderClassSource(JavaClassSource source, BuilderDefinitionDto builderDef) { + String rendered = source.toUnformattedString(); + rendered = injectTrackedValueStaticImports(rendered); + rendered = normalizeJavadocs(rendered); + rendered = normalizeBuilderClassJavadoc(rendered); + rendered = + simplifyImportedTypeReferences( + rendered, collectImports(builderDef), builderDef.getBuilderTypeName().getPackageName()); + rendered = normalizeToStringChains(rendered); + rendered = alignClosingBraceSpacing(rendered); + return rendered; } - private void appendFields(StringBuilder source, BuilderDefinitionDto builderDef) { + private void appendFields(JavaClassSource source, BuilderDefinitionDto builderDef) { logger.debugStartOperation( "Generating %d constructor fields and %d setter fields", builderDef.getConstructorFieldsForBuilder().size(), @@ -181,32 +180,27 @@ private void appendFields(StringBuilder source, BuilderDefinitionDto builderDef) logger.debugEndOperation("Fields added: %d fields", builderDef.getAllFieldsForBuilder().size()); } - private void appendField(StringBuilder source, FieldDto fieldDto) { + private void appendField(JavaClassSource source, FieldDto fieldDto) { String boxedFieldType = mapBoxedType(fieldDto.getFieldType()); - appendJavadoc( - source, + FieldSource field = source.addField(); + field.setName(fieldDto.getFieldNameInBuilder()); + field.setType(TrackedValue.class.getSimpleName() + "<" + boxedFieldType + ">"); + field.setPrivate(); + field.setLiteralInitializer("unsetValue()"); + applyJavadoc( + field, "Tracked value for %s: %s." .formatted( - fieldDto.getFieldNameInBuilder(), StringUtils.defaultString(fieldDto.getJavaDoc())), - INDENT); - source - .append(INDENT) - .append("private ") - .append(TrackedValue.class.getSimpleName()) - .append("<") - .append(boxedFieldType) - .append("> ") - .append(fieldDto.getFieldNameInBuilder()) - .append(" = ") - .append("unsetValue();\n\n"); - } - - private void appendConstructors(StringBuilder source, BuilderDefinitionDto builderDef) { + fieldDto.getFieldNameInBuilder(), + StringUtils.defaultString(fieldDto.getJavaDoc()))); + } + + private void appendConstructors(JavaClassSource source, BuilderDefinitionDto builderDef) { generateConstructors(source, builderDef); logger.debug("Constructors added"); } - private void generateConstructors(StringBuilder source, BuilderDefinitionDto builderDef) { + private void generateConstructors(JavaClassSource source, BuilderDefinitionDto builderDef) { Modifier constructorAccessModifier = JavaLangMapper.mapAccessModifier( builderDef.getConfiguration().getBuilderConstructorAccess()); @@ -217,57 +211,44 @@ private void generateConstructors(StringBuilder source, BuilderDefinitionDto bui dtoBaseClass, builderDef.getBuilderTypeName().getClassName(), constructorAccessModifier); - source.append("\n"); appendConstructorWithInstance( source, dtoBaseClass, builderDef.getBuilderTypeName().getClassName(), builderDef.getAllFieldsForBuilder(), constructorAccessModifier); - source.append("\n"); } private void appendEmptyConstructor( - StringBuilder source, TypeName dtoClass, String builderClassName, Modifier accessModifier) { - appendJavadoc( - source, - "Empty constructor of builder for {@code %s}.".formatted(dtoClass.getFullQualifiedName()), - INDENT); - source - .append(INDENT) - .append(buildMethodPrefix(accessModifier, false, null)) - .append(builderClassName) - .append("() {\n") - .append(INDENT) - .append("}\n"); + JavaClassSource source, TypeName dtoClass, String builderClassName, Modifier accessModifier) { + MethodSource constructor = source.addMethod(); + constructor.setConstructor(true); + applyVisibility(constructor, accessModifier); + constructor.setBody(""); + applyJavadoc( + constructor, + "Empty constructor of builder for {@code %s}.".formatted(dtoClass.getFullQualifiedName())); } private void appendConstructorWithInstance( - StringBuilder source, + JavaClassSource source, TypeName dtoBaseClass, String builderClassName, List fields, Modifier accessModifier) { - appendJavadoc( - source, + MethodSource constructor = source.addMethod(); + constructor.setConstructor(true); + applyVisibility(constructor, accessModifier); + constructor.addParameter(mapType(dtoBaseClass), "instance"); + constructor.setBody(buildConstructorBody(fields)); + applyJavadoc( + constructor, """ Initialisation of builder for {@code %s} by a instance. @param instance object instance for initialisiation """ - .formatted(dtoBaseClass.getFullQualifiedName()), - INDENT); - source - .append(INDENT) - .append(buildMethodPrefix(accessModifier, false, null)) - .append(builderClassName) - .append("(") - .append(mapType(dtoBaseClass)) - .append(" instance) {\n"); - - String body = buildConstructorBody(fields); - appendIndentedBody(source, body, DOUBLE_INDENT); - source.append(INDENT).append("}\n"); + .formatted(dtoBaseClass.getFullQualifiedName())); } private String buildConstructorBody(List fields) { @@ -307,7 +288,7 @@ private void addFieldInitializationWithValidation( } } - private void appendMethods(StringBuilder source, BuilderDefinitionDto builderDef) { + private void appendMethods(JavaClassSource source, BuilderDefinitionDto builderDef) { Map allMethods = collectAllMethods(builderDef); logger.debugStartOperation("Adding Methods for %d candidates", allMethods.size()); @@ -317,7 +298,6 @@ private void appendMethods(StringBuilder source, BuilderDefinitionDto builderDef int generatedCnt = 0; for (MethodDto methodDto : resolvedMethods) { appendMethod(source, methodDto, false, false); - source.append("\n"); generatedCnt++; } logger.debugEndOperation("%d Methods added", generatedCnt); @@ -393,30 +373,18 @@ private String getSourceDescription(MethodDto method, FieldDto field) { } private void appendMethod( - StringBuilder source, + JavaClassSource source, MethodDto methodDto, boolean nestedTypeMethod, boolean interfaceMethod) { - appendJavadoc(source, methodDto.getJavadoc(), INDENT); - appendAnnotations(source, methodDto.getAnnotations(), INDENT); - - source - .append(INDENT) - .append(buildMethodSignature(methodDto, nestedTypeMethod, interfaceMethod)) - .append(" {"); - + MethodSource method = source.addMethod(); + configureMethod(method, methodDto, nestedTypeMethod, interfaceMethod); String body = methodDto.getMethodCodeDto() != null && StringUtils.isNotBlank(methodDto.getMethodCodeDto().getCodeFormat()) ? resolveCodeTemplate(methodDto.getMethodCodeDto()) : ""; - - if (StringUtils.isNotBlank(body)) { - source.append("\n"); - appendIndentedBody(source, body, DOUBLE_INDENT); - source.append(INDENT); - } - source.append("}\n"); + method.setBody(body); } private String buildMethodSignature( @@ -501,77 +469,363 @@ private String buildParameter(MethodParameterDto paramDto, boolean lastParameter return parameter.toString(); } - private void appendNestedTypes(StringBuilder source, BuilderDefinitionDto builderDef) { + private void appendNestedTypes(JavaClassSource source, BuilderDefinitionDto builderDef) { if (CollectionUtils.isEmpty(builderDef.getNestedTypes())) { return; } logger.debugStartOperation("Generating %d nested type(s)", builderDef.getNestedTypes().size()); for (NestedTypeDto nestedType : builderDef.getNestedTypes()) { appendNestedType(source, nestedType); - source.append("\n"); logger.debug("Generated nested type: %s", nestedType.getTypeName()); } logger.debugEndOperation("Nested types added"); } - private void appendNestedType(StringBuilder source, NestedTypeDto nestedType) { - appendJavadoc(source, nestedType.getJavadoc(), INDENT); - source.append(INDENT); + private void appendNestedType(JavaClassSource source, NestedTypeDto nestedType) { + JavaSource nestedSource = + nestedType.getKind() == NestedTypeDto.NestedTypeKind.INTERFACE + ? source.addNestedType(JavaInterfaceSource.class) + : source.addNestedType(JavaClassSource.class); + nestedSource.setName(nestedType.getTypeName()); if (nestedType.isPublic()) { - source.append(PUBLIC.toString().toLowerCase()).append(" "); + nestedSource.setPublic(); + } else { + nestedSource.setPackagePrivate(); } - source - .append( - nestedType.getKind() == NestedTypeDto.NestedTypeKind.INTERFACE - ? "interface " - : "class ") - .append(nestedType.getTypeName()) - .append(" {\n\n"); - + applyJavadoc(nestedSource, nestedType.getJavadoc()); boolean isInterface = nestedType.getKind() == NestedTypeDto.NestedTypeKind.INTERFACE; for (MethodDto methodDto : nestedType.getMethods()) { - appendNestedMethod(source, methodDto, isInterface); - source.append("\n"); + appendNestedMethod(nestedSource, methodDto, isInterface); } + } - source.append(INDENT).append("}\n"); + private void appendNestedMethod(JavaSource source, MethodDto methodDto, boolean isInterface) { + org.jboss.forge.roaster.model.source.MethodHolderSource methodHolder = + (org.jboss.forge.roaster.model.source.MethodHolderSource) source; + MethodSource method = methodHolder.addMethod(); + configureMethod(method, methodDto, true, isInterface); + if (methodDto.getMethodCodeDto() != null + && StringUtils.isNotBlank(methodDto.getMethodCodeDto().getCodeFormat())) { + method.setBody(resolveCodeTemplate(methodDto.getMethodCodeDto())); + } else { + method.setAbstract(true); + method.setBody(""); + } } - private void appendNestedMethod(StringBuilder source, MethodDto methodDto, boolean isInterface) { - appendJavadoc(source, methodDto.getJavadoc(), DOUBLE_INDENT); - appendAnnotations(source, methodDto.getAnnotations(), DOUBLE_INDENT); + private void applyVisibility( + org.jboss.forge.roaster.model.source.VisibilityScopedSource source, Modifier modifier) { + if (modifier == null) { + source.setPackagePrivate(); + return; + } + switch (modifier) { + case PUBLIC -> source.setPublic(); + case PROTECTED -> source.setProtected(); + case PRIVATE -> source.setPrivate(); + default -> source.setPackagePrivate(); + } + } - source.append(DOUBLE_INDENT); - StringBuilder signature = new StringBuilder(); - String genericDeclaration = mapGenericDeclaration(methodDto.getGenericParameters()); - if (StringUtils.isNotBlank(genericDeclaration)) { - signature.append(genericDeclaration).append(" "); + private void addGenericDeclarations( + JavaClassSource source, + List generics) { + if (CollectionUtils.isEmpty(generics)) { + return; } - if (isInterface - && methodDto.getMethodCodeDto() != null - && StringUtils.isNotBlank(methodDto.getMethodCodeDto().getCodeFormat())) { - signature.append("default "); - } else { - signature.append(PUBLIC.toString().toLowerCase()).append(" "); + for (org.javahelpers.simple.builders.processor.model.type.GenericParameterDto generic : + generics) { + TypeVariableSource typeVariable = source.addTypeVariable(generic.getName()); + if (CollectionUtils.isNotEmpty(generic.getUpperBounds())) { + typeVariable.setBounds( + generic.getUpperBounds().stream().map(RoasterMapper::mapType).toArray(String[]::new)); + } } - signature - .append(mapType(methodDto.getReturnType())) - .append(" ") - .append(methodDto.getMethodName()) - .append("(") - .append(buildParameterList(methodDto.getParameters())) - .append(")"); - source.append(signature); + } - if (methodDto.getMethodCodeDto() == null - || StringUtils.isBlank(methodDto.getMethodCodeDto().getCodeFormat())) { - source.append(";\n"); + private void addGenericDeclarations( + MethodSource source, + List generics) { + if (CollectionUtils.isEmpty(generics)) { return; } + for (org.javahelpers.simple.builders.processor.model.type.GenericParameterDto generic : + generics) { + TypeVariableSource typeVariable = source.addTypeVariable(generic.getName()); + if (CollectionUtils.isNotEmpty(generic.getUpperBounds())) { + typeVariable.setBounds( + generic.getUpperBounds().stream().map(RoasterMapper::mapType).toArray(String[]::new)); + } + } + } + + private void applyJavadoc( + org.jboss.forge.roaster.model.source.JavaDocCapableSource source, String javadoc) { + if (StringUtils.isBlank(javadoc)) { + return; + } + String normalized = javadoc.replace("\r\n", "\n").replace("\r", "\n").replaceFirst("\\n+$", ""); + String[] lines = normalized.split("\n", -1); + StringBuilder text = new StringBuilder(); + source.getJavaDoc().removeAllTags(); + boolean inTags = false; + for (String line : lines) { + if (!inTags && line.startsWith("@")) { + inTags = true; + } + if (inTags) { + int firstSpace = line.indexOf(' '); + if (firstSpace > 1) { + source + .getJavaDoc() + .addTagValue(line.substring(1, firstSpace), line.substring(firstSpace + 1)); + } else if (line.length() > 1) { + source.getJavaDoc().addTagValue(line.substring(1), ""); + } + } else { + if (text.length() > 0) { + text.append("\n"); + } + text.append(line); + } + } + source.getJavaDoc().setText(text.toString()); + } + + private void applyAnnotations( + org.jboss.forge.roaster.model.source.AnnotationTargetSource source, + java.util.Collection annotations) { + if (CollectionUtils.isEmpty(annotations)) { + return; + } + for (AnnotationDto annotationDto : annotations) { + AnnotationSource annotation = source.addAnnotation(); + annotation.setName(annotationDto.getAnnotationType().getClassName()); + for (Map.Entry member : annotationDto.getMembers().entrySet()) { + if ("value".equals(member.getKey())) { + annotation.setLiteralValue(member.getValue()); + } else { + annotation.setLiteralValue(member.getKey(), member.getValue()); + } + } + } + } + + private void configureMethod( + MethodSource method, + MethodDto methodDto, + boolean nestedTypeMethod, + boolean interfaceMethod) { + method.setName(methodDto.getMethodName()); + if (methodDto.getReturnType() == null) { + method.setReturnTypeVoid(); + } else { + method.setReturnType(mapType(methodDto.getReturnType())); + } + + if (nestedTypeMethod) { + boolean hasBody = + interfaceMethod + && methodDto.getMethodCodeDto() != null + && StringUtils.isNotBlank(methodDto.getMethodCodeDto().getCodeFormat()); + if (hasBody) { + method.setDefault(true); + } else { + method.setPublic(); + } + } else { + applyVisibility(method, methodDto.getModifier().orElse(null)); + method.setStatic(methodDto.isStatic()); + } + + addGenericDeclarations(method, methodDto.getGenericParameters()); + applyJavadoc(method, methodDto.getJavadoc()); + applyAnnotations(method, methodDto.getAnnotations()); + + for (int i = 0; i < methodDto.getParameters().size(); i++) { + MethodParameterDto paramDto = methodDto.getParameters().get(i); + boolean lastParameter = i == methodDto.getParameters().size() - 1; + String parameterType = + lastParameter && paramDto.getParameterType() instanceof TypeNameArray arrayType + ? mapType(arrayType.getTypeOfArray()) + : mapType(paramDto.getParameterType()); + ParameterSource parameter = + method.addParameter(parameterType, paramDto.getParameterName()); + if (lastParameter && paramDto.getParameterType() instanceof TypeNameArray) { + parameter.setVarArgs(true); + } + applyAnnotations(parameter, paramDto.getAnnotations()); + } + } + + private String injectTrackedValueStaticImports(String sourceCode) { + String staticImports = + "import static " + + TrackedValue.class.getName() + + ".changedValue;\n" + + "import static " + + TrackedValue.class.getName() + + ".initialValue;\n" + + "import static " + + TrackedValue.class.getName() + + ".unsetValue;\n\n"; + + int packageEnd = sourceCode.startsWith("package ") ? sourceCode.indexOf("\n\n") : -1; + if (packageEnd >= 0) { + return sourceCode.substring(0, packageEnd + 2) + + staticImports + + sourceCode.substring(packageEnd + 2); + } + return staticImports + sourceCode; + } + + private String alignClosingBraceSpacing(String sourceCode) { + String normalized = sourceCode.replace("\r\n", "\n").replace("\r", "\n"); + normalized = normalized.replaceAll("\\n{3,}", "\n\n"); + normalized = normalized.replaceAll("\\n\\s*\\n\\}", "\n}"); + normalized = normalized.replaceAll("\\}\\s+\\}", "}\n}"); + normalized = normalized.replaceAll("(?m)^}(\\n([ \\t]+)})", "\t}$1"); + normalized = normalized.replaceAll("(import [^\\n]+;)(/\\*\\*)", "$1\n\n$2"); + normalized = normalized.replaceAll("@java\\.lang\\.Override", "@Override"); + normalized = + normalized.replaceAll("@javax\\.annotation\\.processing\\.Generated", "@Generated"); + normalized = + normalized.replaceAll( + "@org\\.javahelpers\\.simple\\.builders\\.core\\.annotations\\.BuilderImplementation", + "@BuilderImplementation"); + return normalized; + } - source.append(" {\n"); - appendIndentedBody(source, resolveCodeTemplate(methodDto.getMethodCodeDto()), TRIPLE_INDENT); - source.append(DOUBLE_INDENT).append("}\n"); + private String simplifyImportedTypeReferences( + String sourceCode, Set imports, String currentPackage) { + String normalized = sourceCode; + for (String importedType : imports) { + if (StringUtils.isBlank(importedType) || !importedType.contains(".")) { + continue; + } + String simpleName = importedType.substring(importedType.lastIndexOf('.') + 1); + normalized = normalized.replace("@" + importedType, "@" + simpleName); + } + if (StringUtils.isNotBlank(currentPackage)) { + String escapedPackage = java.util.regex.Pattern.quote(currentPackage); + normalized = + normalized.replaceAll( + "(\\(\\s*)@" + escapedPackage + "\\.([A-Za-z_$][A-Za-z0-9_$]*)", "$1@$2"); + normalized = + normalized.replaceAll( + "(,\\s*)@" + escapedPackage + "\\.([A-Za-z_$][A-Za-z0-9_$]*)", "$1@$2"); + } + return normalized; + } + + private String normalizeJavadocs(String sourceCode) { + String normalized = sourceCode.replace("\r\n", "\n").replace("\r", "\n"); + java.util.regex.Pattern pattern = + java.util.regex.Pattern.compile( + "(?m)^([ \\t]*)/\\*\\*(.*?)^[ \\t]*\\*/", java.util.regex.Pattern.DOTALL); + java.util.regex.Matcher matcher = pattern.matcher(normalized); + StringBuffer result = new StringBuffer(); + while (matcher.find()) { + String indent = matcher.group(1); + String content = matcher.group(2); + String replacement = rebuildJavadocBlock(indent, content); + matcher.appendReplacement(result, java.util.regex.Matcher.quoteReplacement(replacement)); + } + matcher.appendTail(result); + return result.toString(); + } + + private String rebuildJavadocBlock(String indent, String content) { + StringBuilder rebuilt = new StringBuilder(); + rebuilt.append(indent).append("/**\n"); + String[] lines = content.split("\n", -1); + int start = 0; + int end = lines.length; + while (start < end && lines[start].isBlank()) { + start++; + } + while (end > start && lines[end - 1].isBlank()) { + end--; + } + for (int i = start; i < end; i++) { + String rawLine = lines[i]; + String line = rawLine.stripLeading(); + if (line.startsWith("*")) { + line = line.substring(1).stripLeading(); + } + if (line.startsWith("

")) { + rebuilt.append(indent).append(" *

\n"); + String remainder = line.substring(3).stripLeading(); + if (!remainder.isEmpty()) { + rebuilt.append(indent).append(" * ").append(remainder).append("\n"); + } + continue; + } + if (line.startsWith("param ")) { + line = "@param " + line.substring("param ".length()); + } else if (line.startsWith("return ")) { + line = "@return " + line.substring("return ".length()); + } else if (line.startsWith("throws ")) { + line = "@throws " + line.substring("throws ".length()); + } else if (line.startsWith("see ")) { + line = "@see " + line.substring("see ".length()); + } + if (line.isBlank()) { + rebuilt.append(indent).append(" *\n"); + } else { + rebuilt.append(indent).append(" * ").append(line).append("\n"); + } + } + rebuilt.append(indent).append(" */"); + return rebuilt.toString(); + } + + private String normalizeBuilderClassJavadoc(String sourceCode) { + String normalized = sourceCode.replace("\r\n", "\n").replace("\r", "\n"); + normalized = normalized.replace("\n

\n", "\n *

\n"); + normalized = + normalized.replace( + "\nThis builder provides a fluent API for creating instances of", + "\n * This builder provides a fluent API for creating instances of"); + normalized = + normalized.replace( + "\nmethod chaining and validation. Use the static {@code create()} method", + "\n * method chaining and validation. Use the static {@code create()} method"); + normalized = + normalized.replace( + "\nto obtain a new builder instance, configure the desired properties using", + "\n * to obtain a new builder instance, configure the desired properties using"); + normalized = + normalized.replace( + "\nthe setter methods, and then call {@code build()} to create the final DTO.", + "\n * the setter methods, and then call {@code build()} to create the final DTO."); + return normalized; + } + + private String normalizeToStringChains(String sourceCode) { + java.util.regex.Pattern pattern = + java.util.regex.Pattern.compile( + "(?m)^([ \\t]*)return new ToStringBuilder\\(this, BuilderToStringStyle\\.INSTANCE\\)([\\s\\S]*?)\\.toString\\(\\);"); + java.util.regex.Matcher matcher = pattern.matcher(sourceCode); + StringBuffer result = new StringBuffer(); + while (matcher.find()) { + String indent = matcher.group(1); + String middle = matcher.group(2); + java.util.regex.Matcher appendMatcher = + java.util.regex.Pattern.compile("\\.append\\([\\s\\S]*?\\)").matcher(middle); + StringBuilder replacement = new StringBuilder(); + replacement + .append(indent) + .append("return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE)\n"); + while (appendMatcher.find()) { + replacement.append(indent).append(" ").append(appendMatcher.group()).append("\n"); + } + replacement.append(indent).append(" .toString();"); + matcher.appendReplacement( + result, java.util.regex.Matcher.quoteReplacement(replacement.toString())); + } + matcher.appendTail(result); + return result.toString(); } private void appendClassJavadoc(StringBuilder source, String javadoc, String indent) { From 1ac4a999e73906833bfd53dfdf930c39878badcb Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Tue, 17 Mar 2026 21:47:41 +0100 Subject: [PATCH 03/20] Adding formatting for generated sources based on Eclipse JDT formatter profile --- .../roaster/RoasterCodeGenerator.java | 41 ++++++++++++++++++- .../main/resources/eclipse-java-format.xml | 23 +++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 processor/src/main/resources/eclipse-java-format.xml 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 bb698cef..37de69d8 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 @@ -34,11 +34,13 @@ import static org.javahelpers.simple.builders.processor.classgen.roaster.RoasterMapper.resolveCodeTemplate; 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; +import java.util.Properties; import java.util.Set; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.Modifier; @@ -75,9 +77,11 @@ import org.jboss.forge.roaster.model.source.MethodSource; import org.jboss.forge.roaster.model.source.ParameterSource; import org.jboss.forge.roaster.model.source.TypeVariableSource; +import org.jboss.forge.roaster.model.util.FormatterProfileReader; /** Roaster-based code generator for builder source files. */ public class RoasterCodeGenerator { + private static final String FORMATTER_PROFILE_RESOURCE = "eclipse-java-format.xml"; private static final String INDENT = " "; private static final String DOUBLE_INDENT = INDENT + INDENT; private static final String TRIPLE_INDENT = DOUBLE_INDENT + INDENT; @@ -88,6 +92,8 @@ public class RoasterCodeGenerator { /** Logger for debug output during code generation. */ private final ProcessingLogger logger; + private final Properties formatterProperties; + /** * Constructor for RoasterCodeGenerator. * @@ -97,6 +103,7 @@ public class RoasterCodeGenerator { public RoasterCodeGenerator(ProcessingEnvironment processingEnv, ProcessingLogger logger) { this.processingEnv = processingEnv; this.logger = logger; + this.formatterProperties = loadFormatterProperties(); } /** @@ -1052,7 +1059,39 @@ private String toSourceModifier(Modifier modifier) { } private String formatSource(String rawSource) { - return rawSource; + if (formatterProperties == null) { + return rawSource; + } + try { + return Roaster.format(formatterProperties, rawSource); + } catch (Exception ex) { + logger.warning( + "simple-builders: Failed to format generated source with bundled Eclipse formatter profile: %s", + StringUtils.defaultIfBlank(ex.getMessage(), ex.getClass().getSimpleName())); + return rawSource; + } + } + + private Properties loadFormatterProperties() { + try (InputStream inputStream = + RoasterCodeGenerator.class + .getClassLoader() + .getResourceAsStream(FORMATTER_PROFILE_RESOURCE)) { + if (inputStream == null) { + logger.warning( + "simple-builders: Bundled Eclipse formatter profile '%s' was not found on the processor classpath.", + FORMATTER_PROFILE_RESOURCE); + return null; + } + FormatterProfileReader profileReader = FormatterProfileReader.fromEclipseXml(inputStream); + return profileReader.getDefaultProperties(); + } catch (IOException ex) { + logger.warning( + "simple-builders: Failed to load bundled Eclipse formatter profile '%s': %s", + FORMATTER_PROFILE_RESOURCE, + StringUtils.defaultIfBlank(ex.getMessage(), ex.getClass().getSimpleName())); + return null; + } } private void writeBuilderClassToFile(String sourceCode, BuilderDefinitionDto builderDef) diff --git a/processor/src/main/resources/eclipse-java-format.xml b/processor/src/main/resources/eclipse-java-format.xml new file mode 100644 index 00000000..3b732794 --- /dev/null +++ b/processor/src/main/resources/eclipse-java-format.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + From c7775995e10fd9788a9108eb43f32214b6bbc20f Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Tue, 17 Mar 2026 22:26:00 +0100 Subject: [PATCH 04/20] Fixing static imports and javadoc being generated by Roaster-Native functionality --- .../roaster/RoasterCodeGenerator.java | 37 +++++-------------- 1 file changed, 9 insertions(+), 28 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 37de69d8..98459797 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 @@ -149,6 +149,7 @@ private JavaClassSource createClassSource(BuilderDefinitionDto builderDef) { applyVisibility( source, JavaLangMapper.mapAccessModifier(builderDef.getConfiguration().getBuilderAccess())); addGenericDeclarations(source, builderDef.getGenerics()); + addTrackedValueStaticImports(source); collectImports(builderDef).forEach(source::addImport); for (InterfaceName interfaceName : builderDef.getInterfaces()) { source.addInterface(RoasterMapper.mapInterfaceToTypeName(interfaceName)); @@ -160,14 +161,9 @@ private JavaClassSource createClassSource(BuilderDefinitionDto builderDef) { private String renderClassSource(JavaClassSource source, BuilderDefinitionDto builderDef) { String rendered = source.toUnformattedString(); - rendered = injectTrackedValueStaticImports(rendered); - rendered = normalizeJavadocs(rendered); - rendered = normalizeBuilderClassJavadoc(rendered); rendered = simplifyImportedTypeReferences( rendered, collectImports(builderDef), builderDef.getBuilderTypeName().getPackageName()); - rendered = normalizeToStringChains(rendered); - rendered = alignClosingBraceSpacing(rendered); return rendered; } @@ -580,18 +576,18 @@ private void applyJavadoc( if (!inTags && line.startsWith("@")) { inTags = true; } - if (inTags) { + if (inTags && line.startsWith("@")) { int firstSpace = line.indexOf(' '); if (firstSpace > 1) { source .getJavaDoc() - .addTagValue(line.substring(1, firstSpace), line.substring(firstSpace + 1)); + .addTagValue(line.substring(0, firstSpace), line.substring(firstSpace + 1)); } else if (line.length() > 1) { - source.getJavaDoc().addTagValue(line.substring(1), ""); + source.getJavaDoc().addTagValue(line, ""); } } else { if (text.length() > 0) { - text.append("\n"); + text.append('\n'); } text.append(line); } @@ -665,25 +661,10 @@ private void configureMethod( } } - private String injectTrackedValueStaticImports(String sourceCode) { - String staticImports = - "import static " - + TrackedValue.class.getName() - + ".changedValue;\n" - + "import static " - + TrackedValue.class.getName() - + ".initialValue;\n" - + "import static " - + TrackedValue.class.getName() - + ".unsetValue;\n\n"; - - int packageEnd = sourceCode.startsWith("package ") ? sourceCode.indexOf("\n\n") : -1; - if (packageEnd >= 0) { - return sourceCode.substring(0, packageEnd + 2) - + staticImports - + sourceCode.substring(packageEnd + 2); - } - return staticImports + sourceCode; + 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 String alignClosingBraceSpacing(String sourceCode) { From fccc5e1bad727de4294f377f780cc7adc62de041 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Tue, 17 Mar 2026 22:26:36 +0100 Subject: [PATCH 05/20] Fixing formating in javadoc and function/argument alignment on line breaks --- processor/src/main/resources/eclipse-java-format.xml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/processor/src/main/resources/eclipse-java-format.xml b/processor/src/main/resources/eclipse-java-format.xml index 3b732794..287ffafd 100644 --- a/processor/src/main/resources/eclipse-java-format.xml +++ b/processor/src/main/resources/eclipse-java-format.xml @@ -4,9 +4,20 @@ xsi:schemaLocation="http://www.eclipse.org/jdt/core/formatter/profiles http://www.eclipse.org/jdt/core/formatter/profiles/formatterprofiles.xsd" version="21"> + + + + + + + + + + + From da83f92b486088643a5514ebb972685b30f4506e Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Tue, 17 Mar 2026 23:33:05 +0100 Subject: [PATCH 06/20] Adding further optimizations of Builder-Generation format --- processor/src/main/resources/eclipse-java-format.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/processor/src/main/resources/eclipse-java-format.xml b/processor/src/main/resources/eclipse-java-format.xml index 287ffafd..62bf76cf 100644 --- a/processor/src/main/resources/eclipse-java-format.xml +++ b/processor/src/main/resources/eclipse-java-format.xml @@ -18,6 +18,10 @@ + + + + From 4f705ece8970e28a7abee979a5143b529ecec912 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Wed, 18 Mar 2026 00:02:45 +0100 Subject: [PATCH 07/20] Improving handling of annotations --- .../roaster/RoasterCodeGenerator.java | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 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 98459797..89326843 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 @@ -42,6 +42,8 @@ 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; @@ -134,7 +136,7 @@ private String createBuilderSource(BuilderDefinitionDto builderDef) { appendNestedTypes(source, builderDef); logger.debug("Class-level annotations added"); logger.debug("Builder source created"); - return formatSource(renderClassSource(source, builderDef)); + return renderClassSource(source, builderDef); } private JavaClassSource createClassSource(BuilderDefinitionDto builderDef) { @@ -150,7 +152,7 @@ private JavaClassSource createClassSource(BuilderDefinitionDto builderDef) { source, JavaLangMapper.mapAccessModifier(builderDef.getConfiguration().getBuilderAccess())); addGenericDeclarations(source, builderDef.getGenerics()); addTrackedValueStaticImports(source); - collectImports(builderDef).forEach(source::addImport); + collectImports(builderDef).stream().sorted().forEach(source::addImport); for (InterfaceName interfaceName : builderDef.getInterfaces()) { source.addInterface(RoasterMapper.mapInterfaceToTypeName(interfaceName)); } @@ -161,10 +163,7 @@ private JavaClassSource createClassSource(BuilderDefinitionDto builderDef) { private String renderClassSource(JavaClassSource source, BuilderDefinitionDto builderDef) { String rendered = source.toUnformattedString(); - rendered = - simplifyImportedTypeReferences( - rendered, collectImports(builderDef), builderDef.getBuilderTypeName().getPackageName()); - return rendered; + return formatSource(rendered); } private void appendFields(JavaClassSource source, BuilderDefinitionDto builderDef) { @@ -602,8 +601,8 @@ private void applyAnnotations( return; } for (AnnotationDto annotationDto : annotations) { - AnnotationSource annotation = source.addAnnotation(); - annotation.setName(annotationDto.getAnnotationType().getClassName()); + AnnotationSource annotation = + source.addAnnotation(annotationDto.getAnnotationType().getFullQualifiedName()); for (Map.Entry member : annotationDto.getMembers().entrySet()) { if ("value".equals(member.getKey())) { annotation.setLiteralValue(member.getValue()); @@ -841,6 +840,8 @@ private Set collectImports(BuilderDefinitionDto builderDef) { 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 @@ -963,7 +964,7 @@ private void addImportIfNeeded(Set imports, String currentPackage, Strin if (StringUtils.isBlank(fqn) || !fqn.contains(".") || fqn.startsWith("java.lang.") - || currentPackage.equals(packageNameOf(fqn))) { + || StringUtils.equals(packageNameOf(fqn), currentPackage)) { return; } imports.add(fqn); From b5bcfa70c4574fbb067a802c4e3b105713da81f5 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Wed, 18 Mar 2026 00:39:57 +0100 Subject: [PATCH 08/20] Updating expectation after changes in class formattings --- .../BuilderConfigurationReaderTest.java | 14 ++-- .../ComprehensiveFeatureIntegrationTest.java | 79 ++++++++++++------- .../processor/ToStringGenerationTest.java | 7 +- .../builders/processor/WithInterfaceTest.java | 33 +++++--- 4 files changed, 83 insertions(+), 50 deletions(-) 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 f38290aa..9deb48e6 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,6 +463,8 @@ 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; @@ -471,17 +473,16 @@ public class PersonDto { /** * Builder for {@code test.PersonDto}. *

- * This builder provides a fluent API for creating instances of test.PersonDto with - * method chaining and validation. Use the static {@code create()} method - * to obtain a new builder instance, configure the desired properties using - * the setter methods, and then call {@code build()} to create the final DTO. + * This builder provides a fluent API for creating instances of test.PersonDto with method chaining and validation. Use + * the static {@code create()} method to obtain a new builder instance, configure the desired properties using the + * setter methods, and then call {@code build()} to create the final DTO. */ public class PersonDtoMinimalBuilder implements IBuilderBase { + /** * Tracked value for name: name. */ private TrackedValue name = unsetValue(); - /** * Tracked value for tags: tags. */ @@ -552,8 +553,7 @@ public PersonDto build() { */ @Override public String toString() { - return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE) - .append("name", this.name) + return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE).append("name", this.name) .append("tags", this.tags) .toString(); } diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/ComprehensiveFeatureIntegrationTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/ComprehensiveFeatureIntegrationTest.java index d8c0f47a..86a4fe7b 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 @@ -173,15 +173,12 @@ public PersonDto(String name, int age, Optional email, /** * Builder for {@code test.PersonDto}. *

- * This builder provides a fluent API for creating instances of test.PersonDto with - * method chaining and validation. Use the static {@code create()} method - * to obtain a new builder instance, configure the desired properties using - * the setter methods, and then call {@code build()} to create the final DTO. + * This builder provides a fluent API for creating instances of test.PersonDto with method chaining and validation. Use + * the static {@code create()} method to obtain a new builder instance, configure the desired properties using the + * setter methods, and then call {@code build()} to create the final DTO. */ @Generated("Generated by org.javahelpers.simple.builders.processor.BuilderProcessor") - @BuilderImplementation( - forClass = PersonDto.class - ) + @BuilderImplementation(forClass = PersonDto.class) public class PersonDtoBuilder implements IBuilderBase { /** * Tracked value for name: name. @@ -243,7 +240,8 @@ 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"); + 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()); @@ -353,7 +351,9 @@ public PersonDtoBuilder address(AddressDto address) { * @return current instance of builder */ public PersonDtoBuilder address(Consumer addressBuilderConsumer) { - AddressDtoBuilder builder = this.address.isSet() ? new AddressDtoBuilder(this.address.value()) : new AddressDtoBuilder(); + AddressDtoBuilder builder = this.address.isSet() + ? new AddressDtoBuilder(this.address.value()) + : new AddressDtoBuilder(); addressBuilderConsumer.accept(builder); this.address = changedValue(builder.build()); return this; @@ -439,8 +439,8 @@ public PersonDtoBuilder email(Supplier> emailSupplier) { } /** - * Sets the String value for email by using String.format(format, args). - * See {@link String#format(String, Object...)} for details. + * Sets the String value for email by using String.format(format, args). See + * {@link String#format(String, Object...)} for details. * * @param format A format string * @param args Arguments referenced by the format specifiers in the format string. @@ -479,9 +479,10 @@ public PersonDtoBuilder metadata(Map metadata) { * @param metadataBuilderConsumer consumer providing an instance of a builder for metadata * @return current instance of builder */ - public PersonDtoBuilder metadata( - Consumer> metadataBuilderConsumer) { - HashMapBuilder builder = this.metadata.isSet() ? new HashMapBuilder(this.metadata.value()) : new HashMapBuilder(); + public PersonDtoBuilder metadata(Consumer> metadataBuilderConsumer) { + HashMapBuilder builder = this.metadata.isSet() + ? new HashMapBuilder(this.metadata.value()) + : new HashMapBuilder(); metadataBuilderConsumer.accept(builder); this.metadata = changedValue(builder.build()); return this; @@ -534,8 +535,8 @@ public PersonDtoBuilder name(Supplier nameSupplier) { } /** - * Sets the String value for name by using String.format(format, args). - * See {@link String#format(String, Object...)} for details. + * Sets the String value for name by using String.format(format, args). See + * {@link String#format(String, Object...)} for details. * * @param format A format string * @param args Arguments referenced by the format specifiers in the format string. @@ -575,7 +576,9 @@ public PersonDtoBuilder nicknames(List nicknames) { * @return current instance of builder */ public PersonDtoBuilder nicknames(Consumer> nicknamesBuilderConsumer) { - ArrayListBuilder builder = this.nicknames.isSet() ? new ArrayListBuilder(this.nicknames.value()) : new ArrayListBuilder(); + ArrayListBuilder builder = this.nicknames.isSet() + ? new ArrayListBuilder(this.nicknames.value()) + : new ArrayListBuilder(); nicknamesBuilderConsumer.accept(builder); this.nicknames = changedValue(builder.build()); return this; @@ -622,7 +625,9 @@ public PersonDtoBuilder phoneNumbers(LinkedList phoneNumbers) { */ public PersonDtoBuilder phoneNumbers( Consumer> phoneNumbersBuilderConsumer) { - ArrayListBuilder builder = this.phoneNumbers.isSet() ? new ArrayListBuilder(this.phoneNumbers.value()) : new ArrayListBuilder(); + ArrayListBuilder builder = this.phoneNumbers.isSet() + ? new ArrayListBuilder(this.phoneNumbers.value()) + : new ArrayListBuilder(); phoneNumbersBuilderConsumer.accept(builder); this.phoneNumbers = changedValue(new LinkedList<>(builder.build())); return this; @@ -669,7 +674,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); + ArrayListBuilderWithElementBuilders builder = this.previousAddresses.isSet() + ? new ArrayListBuilderWithElementBuilders(this.previousAddresses.value(), AddressDtoBuilder::create) + : new ArrayListBuilderWithElementBuilders(AddressDtoBuilder::create); previousAddressesBuilderConsumer.accept(builder); this.previousAddresses = changedValue(builder.build()); return this; @@ -715,7 +722,9 @@ public PersonDtoBuilder tags(Set tags) { * @return current instance of builder */ public PersonDtoBuilder tags(Consumer> tagsBuilderConsumer) { - HashSetBuilder builder = this.tags.isSet() ? new HashSetBuilder(this.tags.value()) : new HashSetBuilder(); + HashSetBuilder builder = this.tags.isSet() + ? new HashSetBuilder(this.tags.value()) + : new HashSetBuilder(); tagsBuilderConsumer.accept(builder); this.tags = changedValue(builder.build()); return this; @@ -739,8 +748,7 @@ public PersonDtoBuilder tags(Supplier> tagsSupplier) { * @param yesCondition the consumer to apply if condition is true * @return this builder instance */ - public PersonDtoBuilder conditional(BooleanSupplier condition, - Consumer yesCondition) { + public PersonDtoBuilder conditional(BooleanSupplier condition, Consumer yesCondition) { return conditional(condition, yesCondition, null); } @@ -752,8 +760,7 @@ public PersonDtoBuilder conditional(BooleanSupplier condition, * @param falseCase the consumer to apply if condition is false (can be null) * @return this builder instance */ - public PersonDtoBuilder conditional(BooleanSupplier condition, - Consumer trueCase, Consumer falseCase) { + public PersonDtoBuilder conditional(BooleanSupplier condition, Consumer trueCase, Consumer falseCase) { if (condition.getAsBoolean()) { trueCase.accept(this); } else if (falseCase != null) { @@ -773,7 +780,15 @@ public PersonDto build() { if (this.age.value() == null) { 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()); + 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()); return result; } @@ -784,8 +799,7 @@ public PersonDto build() { */ @Override public String toString() { - return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE) - .append("name", this.name) + return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE).append("name", this.name) .append("age", this.age) .append("email", this.email) .append("nicknames", this.nicknames) @@ -802,7 +816,8 @@ public String toString() { */ public interface With { /** - * Initializes a builder from an instance of this class, using methods of this builder to change values and returns the new built object. + * Initializes a builder from an instance of this class, using methods of this builder to change values and returns + * the new built object. * * @param b the consumer to apply modifications * @return the modified instance @@ -812,7 +827,9 @@ default PersonDto with(Consumer b) { try { builder = new PersonDtoBuilder(PersonDto.class.cast(this)); } catch (ClassCastException ex) { - throw new IllegalArgumentException("The interface 'PersonDtoBuilder.With' should only be implemented by classes, which could be casted to 'PersonDto'", ex); + throw new IllegalArgumentException( + "The interface 'PersonDtoBuilder.With' should only be implemented by classes, which could be casted to 'PersonDto'", + ex); } b.accept(builder); return builder.build(); @@ -827,7 +844,9 @@ default PersonDtoBuilder with() { try { return new PersonDtoBuilder(PersonDto.class.cast(this)); } catch (ClassCastException ex) { - throw new IllegalArgumentException("The interface 'PersonDtoBuilder.With' should only be implemented by classes, which could be casted to 'PersonDto'", ex); + throw new IllegalArgumentException( + "The interface 'PersonDtoBuilder.With' should only be implemented by classes, which could be casted to 'PersonDto'", + ex); } } } diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/ToStringGenerationTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/ToStringGenerationTest.java index d6ab54d6..19096c54 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/ToStringGenerationTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/ToStringGenerationTest.java @@ -186,10 +186,9 @@ public class ProductDto { """ @Override public String toString() { - return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE) - .append("name", this.name) - .append("price", this.price) - .toString(); + return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE).append("name", this.name) + .append("price", this.price) + .toString(); } """; diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/WithInterfaceTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/WithInterfaceTest.java index c83522c4..4c2872a7 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/WithInterfaceTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/WithInterfaceTest.java @@ -52,7 +52,8 @@ public class Project { */ public interface With { /** - * Initializes a builder from an instance of this class, using methods of this builder to change values and returns the new built object. + * Initializes a builder from an instance of this class, using methods of this builder to change values and returns + * the new built object. * * @param b the consumer to apply modifications * @return the modified instance @@ -62,7 +63,9 @@ default Project with(Consumer b) { try { builder = new ProjectBuilder(Project.class.cast(this)); } catch (ClassCastException ex) { - throw new IllegalArgumentException("The interface 'ProjectBuilder.With' should only be implemented by classes, which could be casted to 'Project'", ex); + throw new IllegalArgumentException( + "The interface 'ProjectBuilder.With' should only be implemented by classes, which could be casted to 'Project'", + ex); } b.accept(builder); return builder.build(); @@ -77,7 +80,9 @@ default ProjectBuilder with() { try { return new ProjectBuilder(Project.class.cast(this)); } catch (ClassCastException ex) { - throw new IllegalArgumentException("The interface 'ProjectBuilder.With' should only be implemented by classes, which could be casted to 'Project'", ex); + throw new IllegalArgumentException( + "The interface 'ProjectBuilder.With' should only be implemented by classes, which could be casted to 'Project'", + ex); } } } @@ -121,7 +126,8 @@ public User(String username) { */ public interface With { /** - * Initializes a builder from an instance of this class, using methods of this builder to change values and returns the new built object. + * Initializes a builder from an instance of this class, using methods of this builder to change values and returns + * the new built object. * * @param b the consumer to apply modifications * @return the modified instance @@ -131,7 +137,9 @@ default User with(Consumer b) { try { builder = new UserBuilder(User.class.cast(this)); } catch (ClassCastException ex) { - throw new IllegalArgumentException("The interface 'UserBuilder.With' should only be implemented by classes, which could be casted to 'User'", ex); + throw new IllegalArgumentException( + "The interface 'UserBuilder.With' should only be implemented by classes, which could be casted to 'User'", + ex); } b.accept(builder); return builder.build(); @@ -146,7 +154,9 @@ default UserBuilder with() { try { return new UserBuilder(User.class.cast(this)); } catch (ClassCastException ex) { - throw new IllegalArgumentException("The interface 'UserBuilder.With' should only be implemented by classes, which could be casted to 'User'", ex); + throw new IllegalArgumentException( + "The interface 'UserBuilder.With' should only be implemented by classes, which could be casted to 'User'", + ex); } } } @@ -187,7 +197,8 @@ public class Config { */ public interface With { /** - * Initializes a builder from an instance of this class, using methods of this builder to change values and returns the new built object. + * Initializes a builder from an instance of this class, using methods of this builder to change values and returns + * the new built object. * * @param b the consumer to apply modifications * @return the modified instance @@ -197,7 +208,9 @@ default Config with(Consumer b) { try { builder = new ConfigBuilder(Config.class.cast(this)); } catch (ClassCastException ex) { - throw new IllegalArgumentException("The interface 'ConfigBuilder.With' should only be implemented by classes, which could be casted to 'Config'", ex); + throw new IllegalArgumentException( + "The interface 'ConfigBuilder.With' should only be implemented by classes, which could be casted to 'Config'", + ex); } b.accept(builder); return builder.build(); @@ -212,7 +225,9 @@ default ConfigBuilder with() { try { return new ConfigBuilder(Config.class.cast(this)); } catch (ClassCastException ex) { - throw new IllegalArgumentException("The interface 'ConfigBuilder.With' should only be implemented by classes, which could be casted to 'Config'", ex); + throw new IllegalArgumentException( + "The interface 'ConfigBuilder.With' should only be implemented by classes, which could be casted to 'Config'", + ex); } } } From 44a6d95dc41e5a2f45abb5f951265a086a85e31b Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Wed, 18 Mar 2026 00:48:59 +0100 Subject: [PATCH 09/20] Removing unused functionality from RoasterCodeGenerator and RoasterMapper --- .../roaster/RoasterCodeGenerator.java | 302 +----------------- .../classgen/roaster/RoasterMapper.java | 41 --- 2 files changed, 1 insertion(+), 342 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 89326843..3536c652 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,11 +24,7 @@ package org.javahelpers.simple.builders.processor.classgen.roaster; -import static javax.lang.model.element.Modifier.PUBLIC; -import static org.javahelpers.simple.builders.processor.classgen.roaster.RoasterMapper.mapAnnotation; -import static org.javahelpers.simple.builders.processor.classgen.roaster.RoasterMapper.mapAnnotations; import static org.javahelpers.simple.builders.processor.classgen.roaster.RoasterMapper.mapBoxedType; -import static org.javahelpers.simple.builders.processor.classgen.roaster.RoasterMapper.mapGenericDeclaration; import static org.javahelpers.simple.builders.processor.classgen.roaster.RoasterMapper.mapType; import static org.javahelpers.simple.builders.processor.classgen.roaster.RoasterMapper.quote; import static org.javahelpers.simple.builders.processor.classgen.roaster.RoasterMapper.resolveCodeTemplate; @@ -86,7 +82,6 @@ public class RoasterCodeGenerator { private static final String FORMATTER_PROFILE_RESOURCE = "eclipse-java-format.xml"; private static final String INDENT = " "; private static final String DOUBLE_INDENT = INDENT + INDENT; - private static final String TRIPLE_INDENT = DOUBLE_INDENT + INDENT; /** Processing environment for accessing filer and element utilities. */ private final ProcessingEnvironment processingEnv; @@ -389,88 +384,6 @@ private void appendMethod( method.setBody(body); } - private String buildMethodSignature( - MethodDto methodDto, boolean nestedTypeMethod, boolean interfaceMethod) { - StringBuilder signature = new StringBuilder(); - Modifier modifier = methodDto.getModifier().orElse(null); - String genericDeclaration = mapGenericDeclaration(methodDto.getGenericParameters()); - - if (nestedTypeMethod) { - signature.append(PUBLIC.toString().toLowerCase()).append(" "); - if (interfaceMethod - && methodDto.getMethodCodeDto() != null - && StringUtils.isNotBlank(methodDto.getMethodCodeDto().getCodeFormat())) { - signature.append("default "); - } - } else { - signature.append(buildMethodPrefix(modifier, methodDto.isStatic(), genericDeclaration)); - } - - if (nestedTypeMethod && StringUtils.isNotBlank(genericDeclaration)) { - signature.append(genericDeclaration).append(" "); - } - - signature - .append(mapType(methodDto.getReturnType())) - .append(" ") - .append(methodDto.getMethodName()) - .append("(") - .append(buildParameterList(methodDto.getParameters())) - .append(")"); - return signature.toString(); - } - - private String buildMethodPrefix(Modifier modifier, boolean isStatic, String genericDeclaration) { - StringBuilder prefix = new StringBuilder(); - if (modifier != null) { - prefix.append(toSourceModifier(modifier)).append(" "); - } - if (isStatic) { - prefix.append("static "); - } - if (StringUtils.isNotBlank(genericDeclaration)) { - prefix.append(genericDeclaration).append(" "); - } - return prefix.toString(); - } - - private String buildParameterList(List parameters) { - StringBuilder result = new StringBuilder(); - for (int i = 0; i < parameters.size(); i++) { - if (i > 0) { - result.append(", "); - } - result.append(buildParameter(parameters.get(i), i == parameters.size() - 1)); - } - return result.toString(); - } - - private String buildParameter(MethodParameterDto paramDto, boolean lastParameter) { - StringBuilder parameter = new StringBuilder(); - if (CollectionUtils.isNotEmpty(paramDto.getAnnotations())) { - parameter - .append( - mapAnnotations(paramDto.getAnnotations()).stream() - .reduce((a, b) -> a + " " + b) - .orElse("")) - .append(" "); - } - - if (lastParameter && paramDto.getParameterType() instanceof TypeNameArray arrayType) { - parameter - .append(mapType(arrayType.getTypeOfArray())) - .append("...") - .append(" ") - .append(paramDto.getParameterName()); - } else { - parameter - .append(mapType(paramDto.getParameterType())) - .append(" ") - .append(paramDto.getParameterName()); - } - return parameter.toString(); - } - private void appendNestedTypes(JavaClassSource source, BuilderDefinitionDto builderDef) { if (CollectionUtils.isEmpty(builderDef.getNestedTypes())) { return; @@ -666,175 +579,6 @@ private void addTrackedValueStaticImports(JavaClassSource source) { source.addImport(TrackedValue.class.getName() + ".unsetValue").setStatic(true); } - private String alignClosingBraceSpacing(String sourceCode) { - String normalized = sourceCode.replace("\r\n", "\n").replace("\r", "\n"); - normalized = normalized.replaceAll("\\n{3,}", "\n\n"); - normalized = normalized.replaceAll("\\n\\s*\\n\\}", "\n}"); - normalized = normalized.replaceAll("\\}\\s+\\}", "}\n}"); - normalized = normalized.replaceAll("(?m)^}(\\n([ \\t]+)})", "\t}$1"); - normalized = normalized.replaceAll("(import [^\\n]+;)(/\\*\\*)", "$1\n\n$2"); - normalized = normalized.replaceAll("@java\\.lang\\.Override", "@Override"); - normalized = - normalized.replaceAll("@javax\\.annotation\\.processing\\.Generated", "@Generated"); - normalized = - normalized.replaceAll( - "@org\\.javahelpers\\.simple\\.builders\\.core\\.annotations\\.BuilderImplementation", - "@BuilderImplementation"); - return normalized; - } - - private String simplifyImportedTypeReferences( - String sourceCode, Set imports, String currentPackage) { - String normalized = sourceCode; - for (String importedType : imports) { - if (StringUtils.isBlank(importedType) || !importedType.contains(".")) { - continue; - } - String simpleName = importedType.substring(importedType.lastIndexOf('.') + 1); - normalized = normalized.replace("@" + importedType, "@" + simpleName); - } - if (StringUtils.isNotBlank(currentPackage)) { - String escapedPackage = java.util.regex.Pattern.quote(currentPackage); - normalized = - normalized.replaceAll( - "(\\(\\s*)@" + escapedPackage + "\\.([A-Za-z_$][A-Za-z0-9_$]*)", "$1@$2"); - normalized = - normalized.replaceAll( - "(,\\s*)@" + escapedPackage + "\\.([A-Za-z_$][A-Za-z0-9_$]*)", "$1@$2"); - } - return normalized; - } - - private String normalizeJavadocs(String sourceCode) { - String normalized = sourceCode.replace("\r\n", "\n").replace("\r", "\n"); - java.util.regex.Pattern pattern = - java.util.regex.Pattern.compile( - "(?m)^([ \\t]*)/\\*\\*(.*?)^[ \\t]*\\*/", java.util.regex.Pattern.DOTALL); - java.util.regex.Matcher matcher = pattern.matcher(normalized); - StringBuffer result = new StringBuffer(); - while (matcher.find()) { - String indent = matcher.group(1); - String content = matcher.group(2); - String replacement = rebuildJavadocBlock(indent, content); - matcher.appendReplacement(result, java.util.regex.Matcher.quoteReplacement(replacement)); - } - matcher.appendTail(result); - return result.toString(); - } - - private String rebuildJavadocBlock(String indent, String content) { - StringBuilder rebuilt = new StringBuilder(); - rebuilt.append(indent).append("/**\n"); - String[] lines = content.split("\n", -1); - int start = 0; - int end = lines.length; - while (start < end && lines[start].isBlank()) { - start++; - } - while (end > start && lines[end - 1].isBlank()) { - end--; - } - for (int i = start; i < end; i++) { - String rawLine = lines[i]; - String line = rawLine.stripLeading(); - if (line.startsWith("*")) { - line = line.substring(1).stripLeading(); - } - if (line.startsWith("

")) { - rebuilt.append(indent).append(" *

\n"); - String remainder = line.substring(3).stripLeading(); - if (!remainder.isEmpty()) { - rebuilt.append(indent).append(" * ").append(remainder).append("\n"); - } - continue; - } - if (line.startsWith("param ")) { - line = "@param " + line.substring("param ".length()); - } else if (line.startsWith("return ")) { - line = "@return " + line.substring("return ".length()); - } else if (line.startsWith("throws ")) { - line = "@throws " + line.substring("throws ".length()); - } else if (line.startsWith("see ")) { - line = "@see " + line.substring("see ".length()); - } - if (line.isBlank()) { - rebuilt.append(indent).append(" *\n"); - } else { - rebuilt.append(indent).append(" * ").append(line).append("\n"); - } - } - rebuilt.append(indent).append(" */"); - return rebuilt.toString(); - } - - private String normalizeBuilderClassJavadoc(String sourceCode) { - String normalized = sourceCode.replace("\r\n", "\n").replace("\r", "\n"); - normalized = normalized.replace("\n

\n", "\n *

\n"); - normalized = - normalized.replace( - "\nThis builder provides a fluent API for creating instances of", - "\n * This builder provides a fluent API for creating instances of"); - normalized = - normalized.replace( - "\nmethod chaining and validation. Use the static {@code create()} method", - "\n * method chaining and validation. Use the static {@code create()} method"); - normalized = - normalized.replace( - "\nto obtain a new builder instance, configure the desired properties using", - "\n * to obtain a new builder instance, configure the desired properties using"); - normalized = - normalized.replace( - "\nthe setter methods, and then call {@code build()} to create the final DTO.", - "\n * the setter methods, and then call {@code build()} to create the final DTO."); - return normalized; - } - - private String normalizeToStringChains(String sourceCode) { - java.util.regex.Pattern pattern = - java.util.regex.Pattern.compile( - "(?m)^([ \\t]*)return new ToStringBuilder\\(this, BuilderToStringStyle\\.INSTANCE\\)([\\s\\S]*?)\\.toString\\(\\);"); - java.util.regex.Matcher matcher = pattern.matcher(sourceCode); - StringBuffer result = new StringBuffer(); - while (matcher.find()) { - String indent = matcher.group(1); - String middle = matcher.group(2); - java.util.regex.Matcher appendMatcher = - java.util.regex.Pattern.compile("\\.append\\([\\s\\S]*?\\)").matcher(middle); - StringBuilder replacement = new StringBuilder(); - replacement - .append(indent) - .append("return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE)\n"); - while (appendMatcher.find()) { - replacement.append(indent).append(" ").append(appendMatcher.group()).append("\n"); - } - replacement.append(indent).append(" .toString();"); - matcher.appendReplacement( - result, java.util.regex.Matcher.quoteReplacement(replacement.toString())); - } - matcher.appendTail(result); - return result.toString(); - } - - private void appendClassJavadoc(StringBuilder source, String javadoc, String indent) { - appendJavadoc(source, javadoc, indent); - } - - private void appendImports(StringBuilder source, Set imports) { - if (imports.isEmpty()) { - return; - } - imports.stream() - .sorted() - .forEach(value -> source.append("import ").append(value).append(";\n")); - source.append("\n"); - } - - private void appendTrackedValueStaticImports(StringBuilder source) { - source.append("import static ").append(TrackedValue.class.getName()).append(".changedValue;\n"); - source.append("import static ").append(TrackedValue.class.getName()).append(".initialValue;\n"); - source.append("import static ").append(TrackedValue.class.getName()).append(".unsetValue;\n\n"); - } - private Set collectImports(BuilderDefinitionDto builderDef) { Set imports = new LinkedHashSet<>(); String currentPackage = builderDef.getBuilderTypeName().getPackageName(); @@ -964,7 +708,7 @@ private void addImportIfNeeded(Set imports, String currentPackage, Strin if (StringUtils.isBlank(fqn) || !fqn.contains(".") || fqn.startsWith("java.lang.") - || StringUtils.equals(packageNameOf(fqn), currentPackage)) { + || java.util.Objects.equals(packageNameOf(fqn), currentPackage)) { return; } imports.add(fqn); @@ -996,50 +740,6 @@ private void addBodyImports(Set imports, String currentPackage, MethodDt } } - private void appendJavadoc(StringBuilder source, String javadoc, String indent) { - if (StringUtils.isBlank(javadoc)) { - return; - } - source.append(indent).append("/**\n"); - String normalized = javadoc.replace("\r\n", "\n").replace("\r", "\n").replaceFirst("\\n+$", ""); - for (String line : normalized.split("\n", -1)) { - source.append(indent).append(" *"); - if (!line.isEmpty()) { - source.append(" ").append(line.replace("*/", "* /")); - } - source.append("\n"); - } - source.append(indent).append(" */\n"); - } - - private void appendAnnotations( - StringBuilder source, java.util.Collection annotations, String indent) { - if (CollectionUtils.isEmpty(annotations)) { - return; - } - for (AnnotationDto annotation : annotations) { - source.append(indent).append(mapAnnotation(annotation)).append("\n"); - } - } - - private void appendIndentedBody(StringBuilder source, String body, String indent) { - if (StringUtils.isBlank(body)) { - return; - } - String normalized = body.replace("\r\n", "\n").replace("\r", "\n").replaceFirst("\\n+$", ""); - for (String line : normalized.split("\n", -1)) { - if (line.isEmpty()) { - source.append(indent).append("\n"); - } else { - source.append(indent).append(line).append("\n"); - } - } - } - - private String toSourceModifier(Modifier modifier) { - return modifier.toString().toLowerCase(); - } - private String formatSource(String rawSource) { if (formatterProperties == null) { return rawSource; diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterMapper.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterMapper.java index a70c1752..4a35732a 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterMapper.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterMapper.java @@ -34,7 +34,6 @@ import org.javahelpers.simple.builders.processor.model.method.MethodCodePlaceholder; import org.javahelpers.simple.builders.processor.model.method.MethodCodeStringPlaceholder; import org.javahelpers.simple.builders.processor.model.method.MethodCodeTypePlaceholder; -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.TypeNameArray; import org.javahelpers.simple.builders.processor.model.type.TypeNameGeneric; @@ -106,36 +105,6 @@ private static String mapGenericType(TypeNameGeneric genericType) { return genericType.getClassName() + "<" + innerTypes + ">"; } - /** - * Maps generic declarations like {@code }. - * - * @param generics generic parameter list - * @return source code for generic declaration or empty string - */ - public static String mapGenericDeclaration(List generics) { - if (CollectionUtils.isEmpty(generics)) { - return ""; - } - String value = - generics.stream() - .map(RoasterMapper::mapGenericDeclaration) - .reduce((a, b) -> a + ", " + b) - .orElse(""); - return "<" + value + ">"; - } - - private static String mapGenericDeclaration(GenericParameterDto genericParameter) { - if (CollectionUtils.isEmpty(genericParameter.getUpperBounds())) { - return genericParameter.getName(); - } - String bounds = - genericParameter.getUpperBounds().stream() - .map(RoasterMapper::mapType) - .reduce((a, b) -> a + " & " + b) - .orElse(""); - return genericParameter.getName() + " extends " + bounds; - } - /** * Maps an annotation to source code. * @@ -170,16 +139,6 @@ private static String mapAnnotationMember(Map.Entry member) { return member.getKey() + " = " + member.getValue(); } - /** - * Maps a list of annotations to source code lines. - * - * @param annotations annotations to map - * @return list of annotation strings - */ - public static List mapAnnotations(List annotations) { - return annotations.stream().map(RoasterMapper::mapAnnotation).toList(); - } - /** * Maps an interface name to Java source code. * From 7d0848cca087aa6f93413163b702516e5d3134be Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Wed, 18 Mar 2026 12:37:36 +0100 Subject: [PATCH 10/20] Improving source code by restructuring position of helper methods --- .../roaster/RoasterCodeGenerator.java | 19 +++++++++++-------- .../classgen/roaster/RoasterMapper.java | 11 +++++++++++ 2 files changed, 22 insertions(+), 8 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 3536c652..a9e2ae66 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 @@ -26,6 +26,7 @@ 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.quote; import static org.javahelpers.simple.builders.processor.classgen.roaster.RoasterMapper.resolveCodeTemplate; @@ -339,8 +340,8 @@ private List resolveMethodConflicts(Map methodTo if (existing == null) { signatureToMethod.put(signature, method); } else { - String existingSource = getSourceDescription(existing, methodToField.get(existing)); - String newSource = getSourceDescription(method, field); + String existingSource = createSourceDescriptionForLogging(existing, methodToField.get(existing)); + String newSource = createSourceDescriptionForLogging(method, field); if (method.getPriority() > existing.getPriority()) { signatureToMethod.put(signature, method); @@ -362,7 +363,14 @@ private List resolveMethodConflicts(Map methodTo return new java.util.ArrayList<>(signatureToMethod.values()); } - private String getSourceDescription(MethodDto method, FieldDto field) { + /** + * 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() + "'"; } @@ -714,11 +722,6 @@ private void addImportIfNeeded(Set imports, String currentPackage, Strin imports.add(fqn); } - private String packageNameOf(String fqn) { - int idx = fqn.lastIndexOf('.'); - return idx < 0 ? "" : fqn.substring(0, idx); - } - private void addBodyImports(Set imports, String currentPackage, MethodDto method) { if (method.getMethodCodeDto() == null || StringUtils.isBlank(method.getMethodCodeDto().getCodeFormat())) { diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterMapper.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterMapper.java index 4a35732a..8769949a 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterMapper.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterMapper.java @@ -207,6 +207,17 @@ public static String quote(String value) { return "\"" + escaped + "\""; } + /** + * Extracts package name from fully qualified name. + * + * @param fqn fully qualified name + * @return package name or empty string if no package + */ + public static String packageNameOf(String fqn) { + int idx = fqn.lastIndexOf('.'); + return idx < 0 ? "" : fqn.substring(0, idx); + } + private static String prependTypeUseAnnotations( String baseType, List annotations) { if (CollectionUtils.isEmpty(annotations)) { From c285184180b58bf960551d79d5bdcb57c5b9f909 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Wed, 18 Mar 2026 20:23:04 +0100 Subject: [PATCH 11/20] Refactoring JacksonModule generation and replacing other StringBuilder functionality --- .../roaster/RoasterCodeGenerator.java | 187 +++++++++--------- 1 file changed, 97 insertions(+), 90 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 a9e2ae66..33efc9ac 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 @@ -27,7 +27,6 @@ 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.quote; import static org.javahelpers.simple.builders.processor.classgen.roaster.RoasterMapper.resolveCodeTemplate; import java.io.IOException; @@ -81,8 +80,6 @@ /** Roaster-based code generator for builder source files. */ public class RoasterCodeGenerator { private static final String FORMATTER_PROFILE_RESOURCE = "eclipse-java-format.xml"; - private static final String INDENT = " "; - private static final String DOUBLE_INDENT = INDENT + INDENT; /** Processing environment for accessing filer and element utilities. */ private final ProcessingEnvironment processingEnv; @@ -242,10 +239,10 @@ private void appendConstructorWithInstance( applyJavadoc( constructor, """ - Initialisation of builder for {@code %s} by a instance. + Initialisation of builder for {@code %s} by a instance. - @param instance object instance for initialisiation - """ + @param instance object instance for initialisiation + """ .formatted(dtoBaseClass.getFullQualifiedName())); } @@ -262,28 +259,26 @@ private String buildConstructorBody(List fields) { private void addFieldInitializationWithValidation( StringBuilder body, FieldDto field, String getter) { String fieldInBuilder = field.getFieldNameInBuilder(); - body.append("this.") - .append(fieldInBuilder) - .append(" = initialValue(instance.") - .append(getter) - .append("());\n"); + String initialisationCode = + """ + this.%s = initialValue(instance.%s()); + """ + .formatted(fieldInBuilder, getter); + + String validationCode = ""; if (field.isNonNullable()) { - body.append("if (this.") - .append(fieldInBuilder) - .append(".value() == null) {\n") - .append(INDENT) - .append("throw new ") - .append(IllegalArgumentException.class.getSimpleName()) - .append("(") - .append( - quote( - "Cannot initialize builder from instance: field '" - + fieldInBuilder - + "' is marked as non-null but source object has null value")) - .append(");\n") - .append("}\n"); + 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); } + + body.append(initialisationCode).append(validationCode); } private void appendMethods(JavaClassSource source, BuilderDefinitionDto builderDef) { @@ -340,7 +335,8 @@ private List resolveMethodConflicts(Map methodTo if (existing == null) { signatureToMethod.put(signature, method); } else { - String existingSource = createSourceDescriptionForLogging(existing, methodToField.get(existing)); + String existingSource = + createSourceDescriptionForLogging(existing, methodToField.get(existing)); String newSource = createSourceDescriptionForLogging(method, field); if (method.getPriority() > existing.getPriority()) { @@ -363,7 +359,7 @@ 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 @@ -791,12 +787,12 @@ private void writeBuilderClassToFile(String sourceCode, BuilderDefinitionDto bui throw new BuilderException( null, """ - Builder class '%s' already exists. This may be a manually written builder or a previously generated builder. - To resolve this: - 1. If you have a manual builder, consider renaming it or removing @SimpleBuilder from the DTO - 2. If this is from a previous compilation, clean and rebuild the project - 3. Check that you're not trying to generate multiple builders for the same DTO - """ + Builder class '%s' already exists. This may be a manually written builder or a previously generated builder. + To resolve this: + 1. If you have a manual builder, consider renaming it or removing @SimpleBuilder from the DTO + 2. If this is from a previous compilation, clean and rebuild the project + 3. Check that you're not trying to generate multiple builders for the same DTO + """ .formatted(qualifiedName)); } @@ -847,54 +843,68 @@ public void generateJacksonModule(JacksonModuleDefinitionDto moduleDef) { logger.info("Generating Jackson Module '%s' in package '%s'", moduleClassName, packageName); - StringBuilder source = new StringBuilder(); - if (StringUtils.isNotBlank(packageName)) { - source.append("package ").append(packageName).append(";\n\n"); - } - source.append("import com.fasterxml.jackson.databind.annotation.JsonDeserialize;\n"); - source.append("import com.fasterxml.jackson.databind.module.SimpleModule;\n"); - for (JacksonModuleEntryDto entry : moduleDef.getEntries()) { - appendModuleImport(source, packageName, entry.dtoType().getFullQualifiedName()); - appendModuleImport(source, packageName, entry.builderType().getFullQualifiedName()); - } - source.append("\n"); - - source - .append("public class ") - .append(moduleClassName) - .append(" extends ") - .append("SimpleModule") - .append(" {\n\n"); - - for (JacksonModuleEntryDto entry : moduleDef.getEntries()) { - String mixinName = entry.dtoType().getClassName() + "Mixin"; - source - .append(INDENT) - .append("@JsonDeserialize(builder = ") - .append(entry.builderType().getClassName()) - .append(".class)\n") - .append(INDENT) - .append("private interface ") - .append(mixinName) - .append(" {}\n\n"); - } - - source.append(INDENT).append("public ").append(moduleClassName).append("() {\n"); - for (JacksonModuleEntryDto entry : moduleDef.getEntries()) { - String mixinName = entry.dtoType().getClassName() + "Mixin"; - source - .append(DOUBLE_INDENT) - .append("setMixInAnnotation(") - .append(entry.dtoType().getClassName()) - .append(".class, ") - .append(mixinName) - .append(".class);\n"); - } - source.append(INDENT).append("}\n"); - source.append("}\n"); - try { - writeSimpleClassToFile(packageName, moduleClassName, formatSource(source.toString())); + // Create the class using Roaster + JavaClassSource moduleClass = Roaster.create(JavaClassSource.class); + moduleClass.setPackage(packageName); + 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"); + + // Adding imports for DTO types + moduleDef.getEntries().stream() + .filter(e -> shouldAddImport(packageName, e.dtoType().getFullQualifiedName())) + .forEach(e -> imports.add(e.dtoType().getFullQualifiedName())); + + // 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); + + // 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", @@ -902,6 +912,13 @@ public void generateJacksonModule(JacksonModuleDefinitionDto moduleDef) { } } + 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 { @@ -922,14 +939,4 @@ private void writeSimpleClassToFile(String packageName, String className, String throw new BuilderException(null, errorMessage); } } - - private void appendModuleImport(StringBuilder source, String currentPackage, String fqn) { - if (StringUtils.isBlank(fqn) - || !fqn.contains(".") - || fqn.startsWith("java.lang.") - || currentPackage.equals(packageNameOf(fqn))) { - return; - } - source.append("import ").append(fqn).append(";\n"); - } } From a0ded137c5c8f6ec992ae853224aec998f59fcdd Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Wed, 18 Mar 2026 20:51:44 +0100 Subject: [PATCH 12/20] Improving code structure from new RoasterCodeGenerator --- .../roaster/RoasterCodeGenerator.java | 78 ++++++++++++------- 1 file changed, 51 insertions(+), 27 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 33efc9ac..b7357101 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 @@ -70,6 +70,7 @@ 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; @@ -119,20 +120,30 @@ public void generateBuilder(BuilderDefinitionDto builderDef) throws BuilderExcep } private String createBuilderSource(BuilderDefinitionDto builderDef) { - logger.debug("Creating builder source for %s", builderDef.getBuilderTypeName()); - logger.debug("Class builder created"); JavaClassSource source = createClassSource(builderDef); - logger.debug("Class metadata added"); + addClassMetadata(source, builderDef); appendFields(source, builderDef); appendConstructors(source, builderDef); appendMethods(source, builderDef); appendNestedTypes(source, builderDef); - logger.debug("Class-level annotations added"); - logger.debug("Builder source created"); + applyClassAnnotations(source, builderDef); return renderClassSource(source, builderDef); } + private void applyClassAnnotations(JavaClassSource source, BuilderDefinitionDto builderDef) { + if (CollectionUtils.isEmpty(builderDef.getClassAnnotations())) { + return; + } + // Adding annotations from enhancers + applyAnnotations(source, builderDef.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()); + } + JavaClassSource source = Roaster.create(JavaClassSource.class); String packageName = builderDef.getBuilderTypeName().getPackageName(); if (StringUtils.isNotBlank(packageName)) { @@ -141,17 +152,27 @@ private JavaClassSource createClassSource(BuilderDefinitionDto builderDef) { source.setDefaultPackage(); } source.setName(builderDef.getBuilderTypeName().getClassName()); - applyVisibility( - source, JavaLangMapper.mapAccessModifier(builderDef.getConfiguration().getBuilderAccess())); addGenericDeclarations(source, builderDef.getGenerics()); addTrackedValueStaticImports(source); collectImports(builderDef).stream().sorted().forEach(source::addImport); + logger.debug("Class builder created"); + return source; + } + + private void addClassMetadata(JavaClassSource source, BuilderDefinitionDto builderDef) { + // Add class JavaDoc if provided by enhancer + applyJavadoc(source, builderDef.getClassJavadoc()); + + // Set builder class access level + applyVisibility( + source, JavaLangMapper.mapAccessModifier(builderDef.getConfiguration().getBuilderAccess())); + + // Adding interfaces from enhancers for (InterfaceName interfaceName : builderDef.getInterfaces()) { source.addInterface(RoasterMapper.mapInterfaceToTypeName(interfaceName)); } - applyJavadoc(source, builderDef.getClassJavadoc()); - applyAnnotations(source, builderDef.getClassAnnotations()); - return source; + + logger.debug("Class metadata added"); } private String renderClassSource(JavaClassSource source, BuilderDefinitionDto builderDef) { @@ -182,8 +203,11 @@ private void appendField(JavaClassSource source, FieldDto fieldDto) { field.setType(TrackedValue.class.getSimpleName() + "<" + boxedFieldType + ">"); field.setPrivate(); field.setLiteralInitializer("unsetValue()"); - applyJavadoc( - field, + applyJavaDocToField(field.getJavaDoc(), fieldDto); + } + + private void applyJavaDocToField(JavaDocSource javaDoc, FieldDto fieldDto) { + javaDoc.setText( "Tracked value for %s: %s." .formatted( fieldDto.getFieldNameInBuilder(), @@ -191,11 +215,6 @@ private void appendField(JavaClassSource source, FieldDto fieldDto) { } private void appendConstructors(JavaClassSource source, BuilderDefinitionDto builderDef) { - generateConstructors(source, builderDef); - logger.debug("Constructors added"); - } - - private void generateConstructors(JavaClassSource source, BuilderDefinitionDto builderDef) { Modifier constructorAccessModifier = JavaLangMapper.mapAccessModifier( builderDef.getConfiguration().getBuilderConstructorAccess()); @@ -212,6 +231,7 @@ private void generateConstructors(JavaClassSource source, BuilderDefinitionDto b builderDef.getBuilderTypeName().getClassName(), builderDef.getAllFieldsForBuilder(), constructorAccessModifier); + logger.debug("Constructors added"); } private void appendEmptyConstructor( @@ -564,17 +584,21 @@ private void configureMethod( for (int i = 0; i < methodDto.getParameters().size(); i++) { MethodParameterDto paramDto = methodDto.getParameters().get(i); boolean lastParameter = i == methodDto.getParameters().size() - 1; - String parameterType = - lastParameter && paramDto.getParameterType() instanceof TypeNameArray arrayType - ? mapType(arrayType.getTypeOfArray()) - : mapType(paramDto.getParameterType()); - ParameterSource parameter = - method.addParameter(parameterType, paramDto.getParameterName()); - if (lastParameter && paramDto.getParameterType() instanceof TypeNameArray) { - parameter.setVarArgs(true); - } - applyAnnotations(parameter, paramDto.getAnnotations()); + addParameter(method, paramDto, lastParameter); + } + } + + private void addParameter( + MethodSource method, MethodParameterDto paramDto, boolean lastParameter) { + String parameterType = + lastParameter && paramDto.getParameterType() instanceof TypeNameArray arrayType + ? mapType(arrayType.getTypeOfArray()) + : mapType(paramDto.getParameterType()); + ParameterSource parameter = method.addParameter(parameterType, paramDto.getParameterName()); + if (lastParameter && paramDto.getParameterType() instanceof TypeNameArray) { + parameter.setVarArgs(true); } + applyAnnotations(parameter, paramDto.getAnnotations()); } private void addTrackedValueStaticImports(JavaClassSource source) { From 7a07af3d400a1158f432f9639e67e5a7051e9a4d Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Wed, 18 Mar 2026 21:18:10 +0100 Subject: [PATCH 13/20] Updating formats and generated classes --- .../builders/example/BookDtoBuilder.java | 150 ++++++++---------- .../example/JacksonIntegrationDtoBuilder.java | 83 +++++----- .../example/MannschaftDtoBuilder.java | 90 ++++++----- .../builders/example/PersonDtoBuilder.java | 122 +++++++------- .../example/ProductRecordBuilder.java | 101 ++++++------ .../example/SimpleBuildersJacksonModule.java | 13 +- .../builders/example/SponsorDtoBuilder.java | 69 ++++---- .../main/resources/eclipse-java-format.xml | 3 + 8 files changed, 312 insertions(+), 319 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 3b58cd81..94e880ca 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 @@ -3,7 +3,6 @@ import static org.javahelpers.simple.builders.core.util.TrackedValue.changedValue; import static org.javahelpers.simple.builders.core.util.TrackedValue.initialValue; import static org.javahelpers.simple.builders.core.util.TrackedValue.unsetValue; - import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; @@ -11,6 +10,8 @@ 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; @@ -19,101 +20,83 @@ * Builder for {@code org.javahelpers.simple.builders.example.BookDto}. *

* This builder provides a fluent API for creating instances of org.javahelpers.simple.builders.example.BookDto with - * method chaining and validation. Use the static {@code create()} method - * to obtain a new builder instance, configure the desired properties using - * the setter methods, and then call {@code build()} to create the final DTO. + * method chaining and validation. Use the static {@code create()} method to obtain a new builder instance, configure + * the desired properties using the setter methods, and then call {@code build()} to create the final DTO. */ public class BookDtoBuilder { + /** * Tracked value for author: the book author to set. */ private TrackedValue author = unsetValue(); - /** * Tracked value for available: true if available, false otherwise. */ private TrackedValue available = unsetValue(); - /** * Tracked value for category: the category code to set. */ private TrackedValue category = unsetValue(); - /** * Tracked value for discount: the discount percentage to set. */ private TrackedValue discount = unsetValue(); - /** * Tracked value for edition: the edition number to set. */ private TrackedValue edition = unsetValue(); - /** * Tracked value for exactPrice: the exact book price to set. */ private TrackedValue exactPrice = unsetValue(); - /** * Tracked value for genres: the set of genres to set. */ private TrackedValue> genres = unsetValue(); - /** * Tracked value for isbn: the ISBN to set. */ private TrackedValue isbn = unsetValue(); - /** * Tracked value for lastUpdated: the last update timestamp to set. */ private TrackedValue lastUpdated = unsetValue(); - /** * Tracked value for metadata: the metadata map to set. */ private TrackedValue> metadata = unsetValue(); - /** * Tracked value for pages: the page count to set. */ private TrackedValue pages = unsetValue(); - /** * Tracked value for price: the book price to set. */ private TrackedValue price = unsetValue(); - /** * Tracked value for publishDate: the publication date to set. */ private TrackedValue publishDate = unsetValue(); - /** * Tracked value for publisher: the publisher to set. */ private TrackedValue publisher = unsetValue(); - /** * Tracked value for rating: the book rating to set. */ private TrackedValue rating = unsetValue(); - /** * Tracked value for salesCount: the sales count to set. */ private TrackedValue salesCount = unsetValue(); - /** * Tracked value for subtitle: an Optional containing the subtitle to set. */ private TrackedValue> subtitle = unsetValue(); - /** * Tracked value for tags: the list of tags to set. */ private TrackedValue> tags = unsetValue(); - /** * Tracked value for title: the book title to set. */ @@ -127,26 +110,30 @@ public BookDtoBuilder() { /** * Initialisation of builder for {@code org.javahelpers.simple.builders.example.BookDto} by a instance. - * + * * @param instance object instance for initialisiation */ 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"); + 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"); + 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"); + 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"); + 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()); @@ -155,21 +142,25 @@ public BookDtoBuilder(BookDto instance) { 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"); + 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"); + 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"); + 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"); + 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()); @@ -178,7 +169,7 @@ public BookDtoBuilder(BookDto instance) { /** * Creating a new builder for {@code org.javahelpers.simple.builders.example.BookDto}. - * + * * @return builder for {@code org.javahelpers.simple.builders.example.BookDto} */ public static BookDtoBuilder create() { @@ -187,7 +178,7 @@ public static BookDtoBuilder create() { /** * Sets the value for author. - * + * * @param author the book author to set * @return current instance of builder */ @@ -198,7 +189,7 @@ public BookDtoBuilder author(String author) { /** * Sets the value for available. - * + * * @param available true if available, false otherwise * @return current instance of builder */ @@ -209,7 +200,7 @@ public BookDtoBuilder available(boolean available) { /** * Sets the value for category. - * + * * @param category the category code to set * @return current instance of builder */ @@ -220,7 +211,7 @@ public BookDtoBuilder category(char category) { /** * Sets the value for discount. - * + * * @param discount the discount percentage to set * @return current instance of builder */ @@ -231,7 +222,7 @@ public BookDtoBuilder discount(float discount) { /** * Sets the value for edition. - * + * * @param edition the edition number to set * @return current instance of builder */ @@ -242,7 +233,7 @@ public BookDtoBuilder edition(short edition) { /** * Sets the value for exactPrice. - * + * * @param exactPrice the exact book price to set * @return current instance of builder */ @@ -253,7 +244,7 @@ public BookDtoBuilder exactPrice(BigDecimal exactPrice) { /** * Sets the value for genres. - * + * * @param genres the set of genres to set * @return current instance of builder */ @@ -264,7 +255,7 @@ public BookDtoBuilder genres(Set genres) { /** * Sets the value for isbn. - * + * * @param isbn the ISBN to set * @return current instance of builder */ @@ -275,7 +266,7 @@ public BookDtoBuilder isbn(String isbn) { /** * Sets the value for lastUpdated. - * + * * @param lastUpdated the last update timestamp to set * @return current instance of builder */ @@ -286,7 +277,7 @@ public BookDtoBuilder lastUpdated(LocalDateTime lastUpdated) { /** * Sets the value for metadata. - * + * * @param metadata the metadata map to set * @return current instance of builder */ @@ -297,7 +288,7 @@ public BookDtoBuilder metadata(Map metadata) { /** * Sets the value for pages. - * + * * @param pages the page count to set * @return current instance of builder */ @@ -308,7 +299,7 @@ public BookDtoBuilder pages(int pages) { /** * Sets the value for price. - * + * * @param price the book price to set * @return current instance of builder */ @@ -319,7 +310,7 @@ public BookDtoBuilder price(double price) { /** * Sets the value for publishDate. - * + * * @param publishDate the publication date to set * @return current instance of builder */ @@ -330,7 +321,7 @@ public BookDtoBuilder publishDate(LocalDate publishDate) { /** * Sets the value for publisher. - * + * * @param publisher the publisher to set * @return current instance of builder */ @@ -341,7 +332,7 @@ public BookDtoBuilder publisher(PersonDto publisher) { /** * Sets the value for rating. - * + * * @param rating the book rating to set * @return current instance of builder */ @@ -352,7 +343,7 @@ public BookDtoBuilder rating(byte rating) { /** * Sets the value for salesCount. - * + * * @param salesCount the sales count to set * @return current instance of builder */ @@ -363,7 +354,7 @@ public BookDtoBuilder salesCount(long salesCount) { /** * Sets the value for subtitle. - * + * * @param subtitle an Optional containing the subtitle to set * @return current instance of builder */ @@ -374,7 +365,7 @@ public BookDtoBuilder subtitle(Optional subtitle) { /** * Sets the value for tags. - * + * * @param tags the list of tags to set * @return current instance of builder */ @@ -385,7 +376,7 @@ public BookDtoBuilder tags(List tags) { /** * Sets the value for title. - * + * * @param title the book title to set * @return current instance of builder */ @@ -396,39 +387,39 @@ public BookDtoBuilder title(String title) { /** * Validates that the author field is not null or empty. - * + * * @return this builder instance for chaining * @throws IllegalArgumentException if author is null or empty */ BookDtoBuilder validateAuthor() { if (!author.isSet() || author.value().trim().isEmpty()) { - throw new IllegalArgumentException("Author cannot be null or empty"); + throw new IllegalArgumentException("Author cannot be null or empty"); } return this; } /** * Validates that the isbn field is not null or empty. - * + * * @return this builder instance for chaining * @throws IllegalArgumentException if isbn is null or empty */ BookDtoBuilder validateIsbn() { if (!isbn.isSet() || isbn.value().trim().isEmpty()) { - throw new IllegalArgumentException("Isbn cannot be null or empty"); + throw new IllegalArgumentException("Isbn cannot be null or empty"); } return this; } /** * Validates that the title field is not null or empty. - * + * * @return this builder instance for chaining * @throws IllegalArgumentException if title is null or empty */ BookDtoBuilder validateTitle() { if (!title.isSet() || title.value().trim().isEmpty()) { - throw new IllegalArgumentException("Title cannot be null or empty"); + throw new IllegalArgumentException("Title cannot be null or empty"); } return this; } @@ -486,31 +477,30 @@ public BookDto build() { /** * Returns a string representation of this builder, including only fields that have been set. - * + * * @return string representation of the builder */ @Override public String toString() { - return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE) - .append("author", this.author) - .append("available", this.available) - .append("category", this.category) - .append("discount", this.discount) - .append("edition", this.edition) - .append("exactPrice", this.exactPrice) - .append("genres", this.genres) - .append("isbn", this.isbn) - .append("lastUpdated", this.lastUpdated) - .append("metadata", this.metadata) - .append("pages", this.pages) - .append("price", this.price) - .append("publishDate", this.publishDate) - .append("publisher", this.publisher) - .append("rating", this.rating) - .append("salesCount", this.salesCount) - .append("subtitle", this.subtitle) - .append("tags", this.tags) - .append("title", this.title) - .toString(); + return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE).append("author", this.author) + .append("available", this.available) + .append("category", this.category) + .append("discount", this.discount) + .append("edition", this.edition) + .append("exactPrice", this.exactPrice) + .append("genres", this.genres) + .append("isbn", this.isbn) + .append("lastUpdated", this.lastUpdated) + .append("metadata", this.metadata) + .append("pages", this.pages) + .append("price", this.price) + .append("publishDate", this.publishDate) + .append("publisher", this.publisher) + .append("rating", this.rating) + .append("salesCount", this.salesCount) + .append("subtitle", this.subtitle) + .append("tags", this.tags) + .append("title", this.title) + .toString(); } -} +} \ No newline at end of file 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 8e99d43f..f430b042 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 @@ -3,7 +3,6 @@ import static org.javahelpers.simple.builders.core.util.TrackedValue.changedValue; import static org.javahelpers.simple.builders.core.util.TrackedValue.initialValue; import static org.javahelpers.simple.builders.core.util.TrackedValue.unsetValue; - import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; import java.util.function.BooleanSupplier; import java.util.function.Consumer; @@ -18,24 +17,20 @@ /** * Builder for {@code org.javahelpers.simple.builders.example.JacksonIntegrationDto}. *

- * This builder provides a fluent API for creating instances of org.javahelpers.simple.builders.example.JacksonIntegrationDto with - * method chaining and validation. Use the static {@code create()} method - * to obtain a new builder instance, configure the desired properties using - * the setter methods, and then call {@code build()} to create the final DTO. + * This builder provides a fluent API for creating instances of + * org.javahelpers.simple.builders.example.JacksonIntegrationDto with method chaining and validation. Use the static + * {@code create()} method to obtain a new builder instance, configure the desired properties using the setter methods, + * and then call {@code build()} to create the final DTO. */ @Generated("Generated by org.javahelpers.simple.builders.processor.BuilderProcessor") -@BuilderImplementation( - forClass = JacksonIntegrationDto.class -) -@JsonPOJOBuilder( - withPrefix = "" -) +@BuilderImplementation(forClass = JacksonIntegrationDto.class) +@JsonPOJOBuilder(withPrefix = "") public class JacksonIntegrationDtoBuilder implements IBuilderBase { + /** * Tracked value for name: name. */ private TrackedValue name = unsetValue(); - /** * Tracked value for age: age. */ @@ -49,20 +44,21 @@ public JacksonIntegrationDtoBuilder() { /** * Initialisation of builder for {@code org.javahelpers.simple.builders.example.JacksonIntegrationDto} by a instance. - * + * * @param instance object instance for initialisiation */ 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"); + throw new IllegalArgumentException( + "Cannot initialize builder from instance: field 'age' is marked as non-null but source object has null value"); } } /** * Creating a new builder for {@code org.javahelpers.simple.builders.example.JacksonIntegrationDto}. - * + * * @return builder for {@code org.javahelpers.simple.builders.example.JacksonIntegrationDto} */ public static JacksonIntegrationDtoBuilder create() { @@ -71,7 +67,7 @@ public static JacksonIntegrationDtoBuilder create() { /** * Sets the value for age. - * + * * @param age age * @return current instance of builder */ @@ -82,7 +78,7 @@ public JacksonIntegrationDtoBuilder age(int age) { /** * Sets the value for age by invoking the provided supplier. - * + * * @param ageSupplier supplier for age * @return current instance of builder */ @@ -93,7 +89,7 @@ public JacksonIntegrationDtoBuilder age(Supplier ageSupplier) { /** * Sets the value for name. - * + * * @param name name * @return current instance of builder */ @@ -104,7 +100,7 @@ public JacksonIntegrationDtoBuilder name(String name) { /** * Sets the value for name by executing the provided consumer. - * + * * @param nameStringBuilderConsumer consumer providing an instance of name * @return current instance of builder */ @@ -117,7 +113,7 @@ public JacksonIntegrationDtoBuilder name(Consumer nameStringBuild /** * Sets the value for name by invoking the provided supplier. - * + * * @param nameSupplier supplier for name * @return current instance of builder */ @@ -127,9 +123,9 @@ public JacksonIntegrationDtoBuilder name(Supplier nameSupplier) { } /** - * Sets the String value for name by using String.format(format, args). - * See {@link String#format(String, Object...)} for details. - * + * Sets the String value for name by using String.format(format, args). See + * {@link String#format(String, Object...)} for details. + * * @param format A format string * @param args Arguments referenced by the format specifiers in the format string. * @return current instance of builder @@ -141,20 +137,20 @@ public JacksonIntegrationDtoBuilder name(String format, Object... args) { /** * Validates that the name field is not null or empty. - * + * * @return this builder instance for chaining * @throws IllegalArgumentException if name is null or empty */ JacksonIntegrationDtoBuilder validateName() { if (!name.isSet() || name.value().trim().isEmpty()) { - throw new IllegalArgumentException("Name cannot be null or empty"); + throw new IllegalArgumentException("Name cannot be null or empty"); } return this; } /** * Conditionally applies builder modifications if the condition is true. - * + * * @param condition the condition to evaluate * @param yesCondition the consumer to apply if condition is true * @return this builder instance @@ -166,19 +162,18 @@ public JacksonIntegrationDtoBuilder conditional(BooleanSupplier condition, /** * Conditionally applies builder modifications based on a condition evaluation. - * + * * @param condition the condition to evaluate * @param trueCase the consumer to apply if condition is true * @param falseCase the consumer to apply if condition is false (can be null) * @return this builder instance */ public JacksonIntegrationDtoBuilder conditional(BooleanSupplier condition, - Consumer trueCase, - Consumer falseCase) { + 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; } @@ -200,15 +195,14 @@ public JacksonIntegrationDto build() { /** * Returns a string representation of this builder, including only fields that have been set. - * + * * @return string representation of the builder */ @Override public String toString() { - return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE) - .append("name", this.name) - .append("age", this.age) - .toString(); + return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE).append("name", this.name) + .append("age", this.age) + .toString(); } /** @@ -216,8 +210,9 @@ public String toString() { */ public interface With { /** - * Initializes a builder from an instance of this class, using methods of this builder to change values and returns the new built object. - * + * Initializes a builder from an instance of this class, using methods of this builder to change values and returns + * the new built object. + * * @param b the consumer to apply modifications * @return the modified instance */ @@ -226,7 +221,9 @@ default JacksonIntegrationDto with(Consumer b) { try { builder = new JacksonIntegrationDtoBuilder(JacksonIntegrationDto.class.cast(this)); } catch (ClassCastException ex) { - throw new IllegalArgumentException("The interface 'JacksonIntegrationDtoBuilder.With' should only be implemented by classes, which could be casted to 'JacksonIntegrationDto'", ex); + throw new IllegalArgumentException( + "The interface 'JacksonIntegrationDtoBuilder.With' should only be implemented by classes, which could be casted to 'JacksonIntegrationDto'", + ex); } b.accept(builder); return builder.build(); @@ -234,15 +231,17 @@ default JacksonIntegrationDto with(Consumer b) { /** * Creates a builder initialized from this instance. - * + * * @return a builder initialized with this instance's values */ default JacksonIntegrationDtoBuilder with() { try { return new JacksonIntegrationDtoBuilder(JacksonIntegrationDto.class.cast(this)); } catch (ClassCastException ex) { - throw new IllegalArgumentException("The interface 'JacksonIntegrationDtoBuilder.With' should only be implemented by classes, which could be casted to 'JacksonIntegrationDto'", ex); + throw new IllegalArgumentException( + "The interface 'JacksonIntegrationDtoBuilder.With' should only be implemented by classes, which could be casted to 'JacksonIntegrationDto'", + ex); } } } -} +} \ No newline at end of file diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/MannschaftDtoBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/MannschaftDtoBuilder.java index 99357b79..1ad4b485 100644 --- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/MannschaftDtoBuilder.java +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/MannschaftDtoBuilder.java @@ -3,7 +3,6 @@ import static org.javahelpers.simple.builders.core.util.TrackedValue.changedValue; import static org.javahelpers.simple.builders.core.util.TrackedValue.initialValue; import static org.javahelpers.simple.builders.core.util.TrackedValue.unsetValue; - import java.util.HashSet; import java.util.Set; import java.util.function.BooleanSupplier; @@ -20,21 +19,18 @@ /** * Builder for {@code org.javahelpers.simple.builders.example.MannschaftDto}. *

- * This builder provides a fluent API for creating instances of org.javahelpers.simple.builders.example.MannschaftDto with - * method chaining and validation. Use the static {@code create()} method - * to obtain a new builder instance, configure the desired properties using - * the setter methods, and then call {@code build()} to create the final DTO. + * This builder provides a fluent API for creating instances of org.javahelpers.simple.builders.example.MannschaftDto + * with method chaining and validation. Use the static {@code create()} method to obtain a new builder instance, + * configure the desired properties using the setter methods, and then call {@code build()} to create the final DTO. */ @Generated("Generated by org.javahelpers.simple.builders.processor.BuilderProcessor") -@BuilderImplementation( - forClass = MannschaftDto.class -) +@BuilderImplementation(forClass = MannschaftDto.class) public class MannschaftDtoBuilder implements IBuilderBase { + /** * Tracked value for name: name. */ private TrackedValue name = unsetValue(); - /** * Tracked value for sponsoren: sponsoren. */ @@ -48,7 +44,7 @@ public MannschaftDtoBuilder() { /** * Initialisation of builder for {@code org.javahelpers.simple.builders.example.MannschaftDto} by a instance. - * + * * @param instance object instance for initialisiation */ public MannschaftDtoBuilder(MannschaftDto instance) { @@ -58,7 +54,7 @@ public MannschaftDtoBuilder(MannschaftDto instance) { /** * Creating a new builder for {@code org.javahelpers.simple.builders.example.MannschaftDto}. - * + * * @return builder for {@code org.javahelpers.simple.builders.example.MannschaftDto} */ public static MannschaftDtoBuilder create() { @@ -67,7 +63,7 @@ public static MannschaftDtoBuilder create() { /** * Adds a single element to sponsoren. - * + * * @param element the element to add * @return current instance of builder */ @@ -85,7 +81,7 @@ public MannschaftDtoBuilder add2Sponsoren(SponsorDto element) { /** * Sets the value for name. - * + * * @param name name * @return current instance of builder */ @@ -96,7 +92,7 @@ public MannschaftDtoBuilder name(String name) { /** * Sets the value for name by executing the provided consumer. - * + * * @param nameStringBuilderConsumer consumer providing an instance of name * @return current instance of builder */ @@ -109,7 +105,7 @@ public MannschaftDtoBuilder name(Consumer nameStringBuilderConsum /** * Sets the value for name by invoking the provided supplier. - * + * * @param nameSupplier supplier for name * @return current instance of builder */ @@ -119,9 +115,9 @@ public MannschaftDtoBuilder name(Supplier nameSupplier) { } /** - * Sets the String value for name by using String.format(format, args). - * See {@link String#format(String, Object...)} for details. - * + * Sets the String value for name by using String.format(format, args). See + * {@link String#format(String, Object...)} for details. + * * @param format A format string * @param args Arguments referenced by the format specifiers in the format string. * @return current instance of builder @@ -133,7 +129,7 @@ public MannschaftDtoBuilder name(String format, Object... args) { /** * Sets the value for sponsoren. - * + * * @param sponsoren sponsoren * @return current instance of builder */ @@ -144,7 +140,7 @@ public MannschaftDtoBuilder sponsoren(SponsorDto... sponsoren) { /** * Sets the value for sponsoren. - * + * * @param sponsoren sponsoren * @return current instance of builder */ @@ -155,13 +151,16 @@ public MannschaftDtoBuilder sponsoren(Set sponsoren) { /** * Sets the value for sponsoren using a builder consumer that produces the value. - * + * * @param sponsorenBuilderConsumer consumer providing an instance of a builder for sponsoren * @return current instance of builder */ public MannschaftDtoBuilder sponsoren( Consumer> sponsorenBuilderConsumer) { - HashSetBuilderWithElementBuilders builder = this.sponsoren.isSet() ? new HashSetBuilderWithElementBuilders(this.sponsoren.value(), SponsorDtoBuilder::create) : new HashSetBuilderWithElementBuilders(SponsorDtoBuilder::create); + HashSetBuilderWithElementBuilders builder = this.sponsoren.isSet() + ? new HashSetBuilderWithElementBuilders(this.sponsoren.value(), + SponsorDtoBuilder::create) + : new HashSetBuilderWithElementBuilders(SponsorDtoBuilder::create); sponsorenBuilderConsumer.accept(builder); this.sponsoren = changedValue(builder.build()); return this; @@ -169,7 +168,7 @@ public MannschaftDtoBuilder sponsoren( /** * Sets the value for sponsoren by invoking the provided supplier. - * + * * @param sponsorenSupplier supplier for sponsoren * @return current instance of builder */ @@ -180,43 +179,42 @@ public MannschaftDtoBuilder sponsoren(Supplier> sponsorenSupplie /** * Validates that the name field is not null or empty. - * + * * @return this builder instance for chaining * @throws IllegalArgumentException if name is null or empty */ MannschaftDtoBuilder validateName() { if (!name.isSet() || name.value().trim().isEmpty()) { - throw new IllegalArgumentException("Name cannot be null or empty"); + throw new IllegalArgumentException("Name cannot be null or empty"); } return this; } /** * Conditionally applies builder modifications if the condition is true. - * + * * @param condition the condition to evaluate * @param yesCondition the consumer to apply if condition is true * @return this builder instance */ - public MannschaftDtoBuilder conditional(BooleanSupplier condition, - Consumer yesCondition) { + public MannschaftDtoBuilder conditional(BooleanSupplier condition, Consumer yesCondition) { return conditional(condition, yesCondition, null); } /** * Conditionally applies builder modifications based on a condition evaluation. - * + * * @param condition the condition to evaluate * @param trueCase the consumer to apply if condition is true * @param falseCase the consumer to apply if condition is false (can be null) * @return this builder instance */ - public MannschaftDtoBuilder conditional(BooleanSupplier condition, - Consumer trueCase, Consumer falseCase) { + public MannschaftDtoBuilder 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; } @@ -234,15 +232,14 @@ public MannschaftDto build() { /** * Returns a string representation of this builder, including only fields that have been set. - * + * * @return string representation of the builder */ @Override public String toString() { - return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE) - .append("name", this.name) - .append("sponsoren", this.sponsoren) - .toString(); + return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE).append("name", this.name) + .append("sponsoren", this.sponsoren) + .toString(); } /** @@ -250,8 +247,9 @@ public String toString() { */ public interface With { /** - * Initializes a builder from an instance of this class, using methods of this builder to change values and returns the new built object. - * + * Initializes a builder from an instance of this class, using methods of this builder to change values and returns + * the new built object. + * * @param b the consumer to apply modifications * @return the modified instance */ @@ -260,7 +258,9 @@ default MannschaftDto with(Consumer b) { try { builder = new MannschaftDtoBuilder(MannschaftDto.class.cast(this)); } catch (ClassCastException ex) { - throw new IllegalArgumentException("The interface 'MannschaftDtoBuilder.With' should only be implemented by classes, which could be casted to 'MannschaftDto'", ex); + throw new IllegalArgumentException( + "The interface 'MannschaftDtoBuilder.With' should only be implemented by classes, which could be casted to 'MannschaftDto'", + ex); } b.accept(builder); return builder.build(); @@ -268,15 +268,17 @@ default MannschaftDto with(Consumer b) { /** * Creates a builder initialized from this instance. - * + * * @return a builder initialized with this instance's values */ default MannschaftDtoBuilder with() { try { return new MannschaftDtoBuilder(MannschaftDto.class.cast(this)); } catch (ClassCastException ex) { - throw new IllegalArgumentException("The interface 'MannschaftDtoBuilder.With' should only be implemented by classes, which could be casted to 'MannschaftDto'", ex); + throw new IllegalArgumentException( + "The interface 'MannschaftDtoBuilder.With' should only be implemented by classes, which could be casted to 'MannschaftDto'", + ex); } } } -} +} \ No newline at end of file diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/PersonDtoBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/PersonDtoBuilder.java index 767ab9bc..c3f6e7a4 100644 --- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/PersonDtoBuilder.java +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/PersonDtoBuilder.java @@ -3,7 +3,6 @@ import static org.javahelpers.simple.builders.core.util.TrackedValue.changedValue; import static org.javahelpers.simple.builders.core.util.TrackedValue.initialValue; import static org.javahelpers.simple.builders.core.util.TrackedValue.unsetValue; - import java.time.LocalDate; import java.util.ArrayList; import java.util.List; @@ -22,35 +21,29 @@ * Builder for {@code org.javahelpers.simple.builders.example.PersonDto}. *

* This builder provides a fluent API for creating instances of org.javahelpers.simple.builders.example.PersonDto with - * method chaining and validation. Use the static {@code create()} method - * to obtain a new builder instance, configure the desired properties using - * the setter methods, and then call {@code build()} to create the final DTO. + * method chaining and validation. Use the static {@code create()} method to obtain a new builder instance, configure + * the desired properties using the setter methods, and then call {@code build()} to create the final DTO. */ @Generated("Generated by org.javahelpers.simple.builders.processor.BuilderProcessor") -@BuilderImplementation( - forClass = PersonDto.class -) +@BuilderImplementation(forClass = PersonDto.class) public class PersonDtoBuilder implements IBuilderBase { + /** * Tracked value for name: name. */ private TrackedValue name = unsetValue(); - /** * Tracked value for birthdate: birthdate. */ private TrackedValue birthdate = unsetValue(); - /** * Tracked value for mannschaft: mannschaft. */ private TrackedValue mannschaft = unsetValue(); - /** * Tracked value for nickNames: nickNames. */ private TrackedValue> nickNames = unsetValue(); - /** * Tracked value for nickNames2: nickNames2. */ @@ -64,7 +57,7 @@ public PersonDtoBuilder() { /** * Initialisation of builder for {@code org.javahelpers.simple.builders.example.PersonDto} by a instance. - * + * * @param instance object instance for initialisiation */ public PersonDtoBuilder(PersonDto instance) { @@ -76,7 +69,7 @@ public PersonDtoBuilder(PersonDto instance) { /** * Creating a new builder for {@code org.javahelpers.simple.builders.example.PersonDto}. - * + * * @return builder for {@code org.javahelpers.simple.builders.example.PersonDto} */ public static PersonDtoBuilder create() { @@ -85,7 +78,7 @@ public static PersonDtoBuilder create() { /** * Adds a single element to nickNames. - * + * * @param element the element to add * @return current instance of builder */ @@ -103,7 +96,7 @@ public PersonDtoBuilder add2NickNames(String element) { /** * Sets the value for birthdate. - * + * * @param birthdate birthdate * @return current instance of builder */ @@ -114,7 +107,7 @@ public PersonDtoBuilder birthdate(LocalDate birthdate) { /** * Sets the value for birthdate by invoking the provided supplier. - * + * * @param birthdateSupplier supplier for birthdate * @return current instance of builder */ @@ -125,7 +118,7 @@ public PersonDtoBuilder birthdate(Supplier birthdateSupplier) { /** * Sets the value for mannschaft. - * + * * @param mannschaft mannschaft * @return current instance of builder */ @@ -136,12 +129,14 @@ public PersonDtoBuilder mannschaft(MannschaftDto mannschaft) { /** * Sets the value for mannschaft using a builder consumer that produces the value. - * + * * @param mannschaftBuilderConsumer consumer providing an instance of a builder for mannschaft * @return current instance of builder */ public PersonDtoBuilder mannschaft(Consumer mannschaftBuilderConsumer) { - MannschaftDtoBuilder builder = this.mannschaft.isSet() ? new MannschaftDtoBuilder(this.mannschaft.value()) : new MannschaftDtoBuilder(); + MannschaftDtoBuilder builder = this.mannschaft.isSet() + ? new MannschaftDtoBuilder(this.mannschaft.value()) + : new MannschaftDtoBuilder(); mannschaftBuilderConsumer.accept(builder); this.mannschaft = changedValue(builder.build()); return this; @@ -149,7 +144,7 @@ public PersonDtoBuilder mannschaft(Consumer mannschaftBuil /** * Sets the value for mannschaft by invoking the provided supplier. - * + * * @param mannschaftSupplier supplier for mannschaft * @return current instance of builder */ @@ -160,7 +155,7 @@ public PersonDtoBuilder mannschaft(Supplier mannschaftSupplier) { /** * Sets the value for name. - * + * * @param name name * @return current instance of builder */ @@ -171,7 +166,7 @@ public PersonDtoBuilder name(String name) { /** * Sets the value for name by executing the provided consumer. - * + * * @param nameStringBuilderConsumer consumer providing an instance of name * @return current instance of builder */ @@ -184,7 +179,7 @@ public PersonDtoBuilder name(Consumer nameStringBuilderConsumer) /** * Sets the value for name by invoking the provided supplier. - * + * * @param nameSupplier supplier for name * @return current instance of builder */ @@ -194,9 +189,9 @@ public PersonDtoBuilder name(Supplier nameSupplier) { } /** - * Sets the String value for name by using String.format(format, args). - * See {@link String#format(String, Object...)} for details. - * + * Sets the String value for name by using String.format(format, args). See + * {@link String#format(String, Object...)} for details. + * * @param format A format string * @param args Arguments referenced by the format specifiers in the format string. * @return current instance of builder @@ -208,7 +203,7 @@ public PersonDtoBuilder name(String format, Object... args) { /** * Sets the value for nickNames. - * + * * @param nickNames nickNames * @return current instance of builder */ @@ -219,7 +214,7 @@ public PersonDtoBuilder nickNames(String... nickNames) { /** * Sets the value for nickNames. - * + * * @param nickNames nickNames * @return current instance of builder */ @@ -230,12 +225,14 @@ public PersonDtoBuilder nickNames(List nickNames) { /** * Sets the value for nickNames using a builder consumer that produces the value. - * + * * @param nickNamesBuilderConsumer consumer providing an instance of a builder for nickNames * @return current instance of builder */ public PersonDtoBuilder nickNames(Consumer> nickNamesBuilderConsumer) { - ArrayListBuilder builder = this.nickNames.isSet() ? new ArrayListBuilder(this.nickNames.value()) : new ArrayListBuilder(); + ArrayListBuilder builder = this.nickNames.isSet() + ? new ArrayListBuilder(this.nickNames.value()) + : new ArrayListBuilder(); nickNamesBuilderConsumer.accept(builder); this.nickNames = changedValue(builder.build()); return this; @@ -243,7 +240,7 @@ public PersonDtoBuilder nickNames(Consumer> nickNamesBu /** * Sets the value for nickNames by invoking the provided supplier. - * + * * @param nickNamesSupplier supplier for nickNames * @return current instance of builder */ @@ -254,7 +251,7 @@ public PersonDtoBuilder nickNames(Supplier> nickNamesSupplier) { /** * Sets the value for nickNames2. - * + * * @param nickNames2 nickNames2 * @return current instance of builder */ @@ -265,7 +262,7 @@ public PersonDtoBuilder nickNames2(String... nickNames2) { /** * Sets the value for nickNames2. - * + * * @param nickNames2 nickNames2 * @return current instance of builder */ @@ -276,12 +273,14 @@ public PersonDtoBuilder nickNames2(List nickNames2) { /** * Sets the value for nickNames2 using the fluent builder consumer. - * + * * @param nickNames2BuilderConsumer consumer for nickNames2 * @return current instance of builder */ public PersonDtoBuilder nickNames2(Consumer> nickNames2BuilderConsumer) { - ArrayListBuilder builder = this.nickNames2.isSet() ? new ArrayListBuilder(java.util.List.of(this.nickNames2.value())) : new ArrayListBuilder(); + ArrayListBuilder builder = this.nickNames2.isSet() + ? new ArrayListBuilder(java.util.List.of(this.nickNames2.value())) + : new ArrayListBuilder(); nickNames2BuilderConsumer.accept(builder); this.nickNames2 = changedValue(builder.build().toArray(new String[0])); return this; @@ -289,7 +288,7 @@ public PersonDtoBuilder nickNames2(Consumer> nickNames2 /** * Sets the value for nickNames2 by invoking the provided supplier. - * + * * @param nickNames2Supplier supplier for nickNames2 * @return current instance of builder */ @@ -300,43 +299,42 @@ public PersonDtoBuilder nickNames2(Supplier nickNames2Supplier) { /** * Validates that the name field is not null or empty. - * + * * @return this builder instance for chaining * @throws IllegalArgumentException if name is null or empty */ PersonDtoBuilder validateName() { if (!name.isSet() || name.value().trim().isEmpty()) { - throw new IllegalArgumentException("Name cannot be null or empty"); + throw new IllegalArgumentException("Name cannot be null or empty"); } return this; } /** * Conditionally applies builder modifications if the condition is true. - * + * * @param condition the condition to evaluate * @param yesCondition the consumer to apply if condition is true * @return this builder instance */ - public PersonDtoBuilder conditional(BooleanSupplier condition, - Consumer yesCondition) { + public PersonDtoBuilder conditional(BooleanSupplier condition, Consumer yesCondition) { return conditional(condition, yesCondition, null); } /** * Conditionally applies builder modifications based on a condition evaluation. - * + * * @param condition the condition to evaluate * @param trueCase the consumer to apply if condition is true * @param falseCase the consumer to apply if condition is false (can be null) * @return this builder instance */ - 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; } @@ -356,18 +354,17 @@ public PersonDto build() { /** * Returns a string representation of this builder, including only fields that have been set. - * + * * @return string representation of the builder */ @Override public String toString() { - return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE) - .append("name", this.name) - .append("birthdate", this.birthdate) - .append("mannschaft", this.mannschaft) - .append("nickNames", this.nickNames) - .append("nickNames2", this.nickNames2) - .toString(); + return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE).append("name", this.name) + .append("birthdate", this.birthdate) + .append("mannschaft", this.mannschaft) + .append("nickNames", this.nickNames) + .append("nickNames2", this.nickNames2) + .toString(); } /** @@ -375,8 +372,9 @@ public String toString() { */ public interface With { /** - * Initializes a builder from an instance of this class, using methods of this builder to change values and returns the new built object. - * + * Initializes a builder from an instance of this class, using methods of this builder to change values and returns + * the new built object. + * * @param b the consumer to apply modifications * @return the modified instance */ @@ -385,7 +383,9 @@ default PersonDto with(Consumer b) { try { builder = new PersonDtoBuilder(PersonDto.class.cast(this)); } catch (ClassCastException ex) { - throw new IllegalArgumentException("The interface 'PersonDtoBuilder.With' should only be implemented by classes, which could be casted to 'PersonDto'", ex); + throw new IllegalArgumentException( + "The interface 'PersonDtoBuilder.With' should only be implemented by classes, which could be casted to 'PersonDto'", + ex); } b.accept(builder); return builder.build(); @@ -393,15 +393,17 @@ default PersonDto with(Consumer b) { /** * Creates a builder initialized from this instance. - * + * * @return a builder initialized with this instance's values */ default PersonDtoBuilder with() { try { return new PersonDtoBuilder(PersonDto.class.cast(this)); } catch (ClassCastException ex) { - throw new IllegalArgumentException("The interface 'PersonDtoBuilder.With' should only be implemented by classes, which could be casted to 'PersonDto'", ex); + throw new IllegalArgumentException( + "The interface 'PersonDtoBuilder.With' should only be implemented by classes, which could be casted to 'PersonDto'", + ex); } } } -} +} \ No newline at end of file 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 a34f67ec..fc7d430a 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 @@ -3,7 +3,6 @@ import static org.javahelpers.simple.builders.core.util.TrackedValue.changedValue; import static org.javahelpers.simple.builders.core.util.TrackedValue.initialValue; import static org.javahelpers.simple.builders.core.util.TrackedValue.unsetValue; - import java.util.function.BooleanSupplier; import java.util.function.Consumer; import java.util.function.Supplier; @@ -17,26 +16,22 @@ /** * Builder for {@code org.javahelpers.simple.builders.example.ProductRecord}. *

- * This builder provides a fluent API for creating instances of org.javahelpers.simple.builders.example.ProductRecord with - * method chaining and validation. Use the static {@code create()} method - * to obtain a new builder instance, configure the desired properties using - * the setter methods, and then call {@code build()} to create the final DTO. + * This builder provides a fluent API for creating instances of org.javahelpers.simple.builders.example.ProductRecord + * with method chaining and validation. Use the static {@code create()} method to obtain a new builder instance, + * configure the desired properties using the setter methods, and then call {@code build()} to create the final DTO. */ @Generated("Generated by org.javahelpers.simple.builders.processor.BuilderProcessor") -@BuilderImplementation( - forClass = ProductRecord.class -) +@BuilderImplementation(forClass = ProductRecord.class) public class ProductRecordBuilder implements IBuilderBase { + /** * Tracked value for name: name. */ private TrackedValue name = unsetValue(); - /** * Tracked value for price: price. */ private TrackedValue price = unsetValue(); - /** * Tracked value for category: category. */ @@ -50,21 +45,22 @@ public ProductRecordBuilder() { /** * Initialisation of builder for {@code org.javahelpers.simple.builders.example.ProductRecord} by a instance. - * + * * @param instance object instance for initialisiation */ 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"); + 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()); } /** * Creating a new builder for {@code org.javahelpers.simple.builders.example.ProductRecord}. - * + * * @return builder for {@code org.javahelpers.simple.builders.example.ProductRecord} */ public static ProductRecordBuilder create() { @@ -73,7 +69,7 @@ public static ProductRecordBuilder create() { /** * Sets the value for category. - * + * * @param category category * @return current instance of builder */ @@ -84,7 +80,7 @@ public ProductRecordBuilder category(String category) { /** * Sets the value for category by executing the provided consumer. - * + * * @param categoryStringBuilderConsumer consumer providing an instance of category * @return current instance of builder */ @@ -97,7 +93,7 @@ public ProductRecordBuilder category(Consumer categoryStringBuild /** * Sets the value for category by invoking the provided supplier. - * + * * @param categorySupplier supplier for category * @return current instance of builder */ @@ -107,9 +103,9 @@ public ProductRecordBuilder category(Supplier categorySupplier) { } /** - * Sets the String value for category by using String.format(format, args). - * See {@link String#format(String, Object...)} for details. - * + * Sets the String value for category by using String.format(format, args). See + * {@link String#format(String, Object...)} for details. + * * @param format A format string * @param args Arguments referenced by the format specifiers in the format string. * @return current instance of builder @@ -121,7 +117,7 @@ public ProductRecordBuilder category(String format, Object... args) { /** * Sets the value for name. - * + * * @param name name * @return current instance of builder */ @@ -132,7 +128,7 @@ public ProductRecordBuilder name(String name) { /** * Sets the value for name by executing the provided consumer. - * + * * @param nameStringBuilderConsumer consumer providing an instance of name * @return current instance of builder */ @@ -145,7 +141,7 @@ public ProductRecordBuilder name(Consumer nameStringBuilderConsum /** * Sets the value for name by invoking the provided supplier. - * + * * @param nameSupplier supplier for name * @return current instance of builder */ @@ -155,9 +151,9 @@ public ProductRecordBuilder name(Supplier nameSupplier) { } /** - * Sets the String value for name by using String.format(format, args). - * See {@link String#format(String, Object...)} for details. - * + * Sets the String value for name by using String.format(format, args). See + * {@link String#format(String, Object...)} for details. + * * @param format A format string * @param args Arguments referenced by the format specifiers in the format string. * @return current instance of builder @@ -169,7 +165,7 @@ public ProductRecordBuilder name(String format, Object... args) { /** * Sets the value for price. - * + * * @param price price * @return current instance of builder */ @@ -180,7 +176,7 @@ public ProductRecordBuilder price(double price) { /** * Sets the value for price by invoking the provided supplier. - * + * * @param priceSupplier supplier for price * @return current instance of builder */ @@ -191,56 +187,55 @@ public ProductRecordBuilder price(Supplier priceSupplier) { /** * Validates that the category field is not null or empty. - * + * * @return this builder instance for chaining * @throws IllegalArgumentException if category is null or empty */ ProductRecordBuilder validateCategory() { if (!category.isSet() || category.value().trim().isEmpty()) { - throw new IllegalArgumentException("Category cannot be null or empty"); + throw new IllegalArgumentException("Category cannot be null or empty"); } return this; } /** * Validates that the name field is not null or empty. - * + * * @return this builder instance for chaining * @throws IllegalArgumentException if name is null or empty */ ProductRecordBuilder validateName() { if (!name.isSet() || name.value().trim().isEmpty()) { - throw new IllegalArgumentException("Name cannot be null or empty"); + throw new IllegalArgumentException("Name cannot be null or empty"); } return this; } /** * Conditionally applies builder modifications if the condition is true. - * + * * @param condition the condition to evaluate * @param yesCondition the consumer to apply if condition is true * @return this builder instance */ - public ProductRecordBuilder conditional(BooleanSupplier condition, - Consumer yesCondition) { + public ProductRecordBuilder conditional(BooleanSupplier condition, Consumer yesCondition) { return conditional(condition, yesCondition, null); } /** * Conditionally applies builder modifications based on a condition evaluation. - * + * * @param condition the condition to evaluate * @param trueCase the consumer to apply if condition is true * @param falseCase the consumer to apply if condition is false (can be null) * @return this builder instance */ - public ProductRecordBuilder conditional(BooleanSupplier condition, - Consumer trueCase, Consumer falseCase) { + public ProductRecordBuilder 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; } @@ -262,16 +257,15 @@ public ProductRecord build() { /** * Returns a string representation of this builder, including only fields that have been set. - * + * * @return string representation of the builder */ @Override public String toString() { - return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE) - .append("name", this.name) - .append("price", this.price) - .append("category", this.category) - .toString(); + return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE).append("name", this.name) + .append("price", this.price) + .append("category", this.category) + .toString(); } /** @@ -279,8 +273,9 @@ public String toString() { */ public interface With { /** - * Initializes a builder from an instance of this class, using methods of this builder to change values and returns the new built object. - * + * Initializes a builder from an instance of this class, using methods of this builder to change values and returns + * the new built object. + * * @param b the consumer to apply modifications * @return the modified instance */ @@ -289,7 +284,9 @@ default ProductRecord with(Consumer b) { try { builder = new ProductRecordBuilder(ProductRecord.class.cast(this)); } catch (ClassCastException ex) { - throw new IllegalArgumentException("The interface 'ProductRecordBuilder.With' should only be implemented by classes, which could be casted to 'ProductRecord'", ex); + throw new IllegalArgumentException( + "The interface 'ProductRecordBuilder.With' should only be implemented by classes, which could be casted to 'ProductRecord'", + ex); } b.accept(builder); return builder.build(); @@ -297,15 +294,17 @@ default ProductRecord with(Consumer b) { /** * Creates a builder initialized from this instance. - * + * * @return a builder initialized with this instance's values */ default ProductRecordBuilder with() { try { return new ProductRecordBuilder(ProductRecord.class.cast(this)); } catch (ClassCastException ex) { - throw new IllegalArgumentException("The interface 'ProductRecordBuilder.With' should only be implemented by classes, which could be casted to 'ProductRecord'", ex); + throw new IllegalArgumentException( + "The interface 'ProductRecordBuilder.With' should only be implemented by classes, which could be casted to 'ProductRecord'", + ex); } } } -} +} \ No newline at end of file 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 5766b3ae..6e406c1a 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 @@ -4,13 +4,12 @@ import com.fasterxml.jackson.databind.module.SimpleModule; public class SimpleBuildersJacksonModule extends SimpleModule { - public SimpleBuildersJacksonModule() { - setMixInAnnotation(JacksonIntegrationDto.class, JacksonIntegrationDtoMixin.class); - } - @JsonDeserialize( - builder = JacksonIntegrationDtoBuilder.class - ) + @JsonDeserialize(builder = JacksonIntegrationDtoBuilder.class) private interface JacksonIntegrationDtoMixin { } -} + + public SimpleBuildersJacksonModule() { + setMixInAnnotation(JacksonIntegrationDto.class, JacksonIntegrationDtoMixin.class); + } +} \ No newline at end of file diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SponsorDtoBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SponsorDtoBuilder.java index 861c1167..295bcb70 100644 --- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SponsorDtoBuilder.java +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SponsorDtoBuilder.java @@ -3,7 +3,6 @@ import static org.javahelpers.simple.builders.core.util.TrackedValue.changedValue; import static org.javahelpers.simple.builders.core.util.TrackedValue.initialValue; import static org.javahelpers.simple.builders.core.util.TrackedValue.unsetValue; - import java.util.function.BooleanSupplier; import java.util.function.Consumer; import java.util.function.Supplier; @@ -18,15 +17,13 @@ * Builder for {@code org.javahelpers.simple.builders.example.SponsorDto}. *

* This builder provides a fluent API for creating instances of org.javahelpers.simple.builders.example.SponsorDto with - * method chaining and validation. Use the static {@code create()} method - * to obtain a new builder instance, configure the desired properties using - * the setter methods, and then call {@code build()} to create the final DTO. + * method chaining and validation. Use the static {@code create()} method to obtain a new builder instance, configure + * the desired properties using the setter methods, and then call {@code build()} to create the final DTO. */ @Generated("Generated by org.javahelpers.simple.builders.processor.BuilderProcessor") -@BuilderImplementation( - forClass = SponsorDto.class -) +@BuilderImplementation(forClass = SponsorDto.class) public class SponsorDtoBuilder implements IBuilderBase { + /** * Tracked value for name: name. */ @@ -40,7 +37,7 @@ public SponsorDtoBuilder() { /** * Initialisation of builder for {@code org.javahelpers.simple.builders.example.SponsorDto} by a instance. - * + * * @param instance object instance for initialisiation */ public SponsorDtoBuilder(SponsorDto instance) { @@ -49,7 +46,7 @@ public SponsorDtoBuilder(SponsorDto instance) { /** * Creating a new builder for {@code org.javahelpers.simple.builders.example.SponsorDto}. - * + * * @return builder for {@code org.javahelpers.simple.builders.example.SponsorDto} */ public static SponsorDtoBuilder create() { @@ -58,7 +55,7 @@ public static SponsorDtoBuilder create() { /** * Sets the value for name. - * + * * @param name name * @return current instance of builder */ @@ -69,7 +66,7 @@ public SponsorDtoBuilder name(String name) { /** * Sets the value for name by executing the provided consumer. - * + * * @param nameStringBuilderConsumer consumer providing an instance of name * @return current instance of builder */ @@ -82,7 +79,7 @@ public SponsorDtoBuilder name(Consumer nameStringBuilderConsumer) /** * Sets the value for name by invoking the provided supplier. - * + * * @param nameSupplier supplier for name * @return current instance of builder */ @@ -92,9 +89,9 @@ public SponsorDtoBuilder name(Supplier nameSupplier) { } /** - * Sets the String value for name by using String.format(format, args). - * See {@link String#format(String, Object...)} for details. - * + * Sets the String value for name by using String.format(format, args). See + * {@link String#format(String, Object...)} for details. + * * @param format A format string * @param args Arguments referenced by the format specifiers in the format string. * @return current instance of builder @@ -106,43 +103,42 @@ public SponsorDtoBuilder name(String format, Object... args) { /** * Validates that the name field is not null or empty. - * + * * @return this builder instance for chaining * @throws IllegalArgumentException if name is null or empty */ SponsorDtoBuilder validateName() { if (!name.isSet() || name.value().trim().isEmpty()) { - throw new IllegalArgumentException("Name cannot be null or empty"); + throw new IllegalArgumentException("Name cannot be null or empty"); } return this; } /** * Conditionally applies builder modifications if the condition is true. - * + * * @param condition the condition to evaluate * @param yesCondition the consumer to apply if condition is true * @return this builder instance */ - public SponsorDtoBuilder conditional(BooleanSupplier condition, - Consumer yesCondition) { + public SponsorDtoBuilder conditional(BooleanSupplier condition, Consumer yesCondition) { return conditional(condition, yesCondition, null); } /** * Conditionally applies builder modifications based on a condition evaluation. - * + * * @param condition the condition to evaluate * @param trueCase the consumer to apply if condition is true * @param falseCase the consumer to apply if condition is false (can be null) * @return this builder instance */ - public SponsorDtoBuilder conditional(BooleanSupplier condition, - Consumer trueCase, Consumer falseCase) { + public SponsorDtoBuilder 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; } @@ -159,14 +155,12 @@ public SponsorDto build() { /** * Returns a string representation of this builder, including only fields that have been set. - * + * * @return string representation of the builder */ @Override public String toString() { - return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE) - .append("name", this.name) - .toString(); + return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE).append("name", this.name).toString(); } /** @@ -174,8 +168,9 @@ public String toString() { */ public interface With { /** - * Initializes a builder from an instance of this class, using methods of this builder to change values and returns the new built object. - * + * Initializes a builder from an instance of this class, using methods of this builder to change values and returns + * the new built object. + * * @param b the consumer to apply modifications * @return the modified instance */ @@ -184,7 +179,9 @@ default SponsorDto with(Consumer b) { try { builder = new SponsorDtoBuilder(SponsorDto.class.cast(this)); } catch (ClassCastException ex) { - throw new IllegalArgumentException("The interface 'SponsorDtoBuilder.With' should only be implemented by classes, which could be casted to 'SponsorDto'", ex); + throw new IllegalArgumentException( + "The interface 'SponsorDtoBuilder.With' should only be implemented by classes, which could be casted to 'SponsorDto'", + ex); } b.accept(builder); return builder.build(); @@ -192,15 +189,17 @@ default SponsorDto with(Consumer b) { /** * Creates a builder initialized from this instance. - * + * * @return a builder initialized with this instance's values */ default SponsorDtoBuilder with() { try { return new SponsorDtoBuilder(SponsorDto.class.cast(this)); } catch (ClassCastException ex) { - throw new IllegalArgumentException("The interface 'SponsorDtoBuilder.With' should only be implemented by classes, which could be casted to 'SponsorDto'", ex); + throw new IllegalArgumentException( + "The interface 'SponsorDtoBuilder.With' should only be implemented by classes, which could be casted to 'SponsorDto'", + ex); } } } -} +} \ No newline at end of file diff --git a/processor/src/main/resources/eclipse-java-format.xml b/processor/src/main/resources/eclipse-java-format.xml index 62bf76cf..c51be09c 100644 --- a/processor/src/main/resources/eclipse-java-format.xml +++ b/processor/src/main/resources/eclipse-java-format.xml @@ -34,5 +34,8 @@ + + + From 79265b653f3a467b6098d5ba194b6259e356b568 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Wed, 18 Mar 2026 21:19:25 +0100 Subject: [PATCH 14/20] Removing packages from javapoet --- .../classgen/javapoet/JavaCodeGenerator.java | 684 ------------------ .../classgen/javapoet/JavapoetMapper.java | 272 ------- .../exceptions/JavapoetMapperException.java | 59 -- .../javapoet/exceptions/package-info.java | 9 - .../classgen/javapoet/package-info.java | 26 - 5 files changed, 1050 deletions(-) delete mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/javapoet/JavaCodeGenerator.java delete mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/javapoet/JavapoetMapper.java delete mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/javapoet/exceptions/JavapoetMapperException.java delete mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/javapoet/exceptions/package-info.java delete mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/javapoet/package-info.java diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/javapoet/JavaCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/javapoet/JavaCodeGenerator.java deleted file mode 100644 index 5499f44c..00000000 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/javapoet/JavaCodeGenerator.java +++ /dev/null @@ -1,684 +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.classgen.javapoet; - -import static javax.lang.model.element.Modifier.PUBLIC; -import static org.javahelpers.simple.builders.processor.classgen.javapoet.JavapoetMapper.*; - -import com.palantir.javapoet.AnnotationSpec; -import com.palantir.javapoet.ClassName; -import com.palantir.javapoet.CodeBlock; -import com.palantir.javapoet.FieldSpec; -import com.palantir.javapoet.JavaFile; -import com.palantir.javapoet.MethodSpec; -import com.palantir.javapoet.ParameterSpec; -import com.palantir.javapoet.ParameterizedTypeName; -import com.palantir.javapoet.TypeSpec; -import java.io.IOException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import javax.annotation.processing.ProcessingEnvironment; -import javax.lang.model.element.Modifier; -import javax.lang.model.element.TypeElement; -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.FieldDto; -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.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.TypeNameArray; -import org.javahelpers.simple.builders.processor.processing.ProcessingLogger; - -/** JavaCodeGenerator generates with BuilderDefinitionDto JavaCode for the builder. */ -public class JavaCodeGenerator { - /** Processing environment for accessing filer and element utilities. */ - private final ProcessingEnvironment processingEnv; - - /** Logger for debug output during code generation. */ - private final ProcessingLogger logger; - - /** - * Constructor for JavaCodeGenerator. - * - * @param processingEnv Processing environment for accessing filer and element utilities - * @param logger Logger for debug output - */ - public JavaCodeGenerator(ProcessingEnvironment processingEnv, ProcessingLogger logger) { - this.processingEnv = processingEnv; - this.logger = logger; - } - - /** - * Generates a builder class from the given builder definition. - * - * @param builderDef dto of all information to create the builder - * @throws BuilderException if there is an error in source code generation - */ - public void generateBuilder(BuilderDefinitionDto builderDef) throws BuilderException { - logger.debugStartOperation( - "Code generation for builder: %s", builderDef.getBuilderTypeName().getClassName()); - TypeSpec.Builder classBuilder = createClassBuilder(builderDef); - addClassMetadata(classBuilder, builderDef); - addFieldsToBuilder(classBuilder, builderDef); - addMethodsToBuilder(classBuilder, builderDef); - addConstructorsToBuilder(classBuilder, builderDef); - addNestedTypesToBuilder(classBuilder, builderDef); - addAnnotationsToBuilder(classBuilder, builderDef); - - writeBuilderClassToFile(classBuilder.build(), builderDef); - logger.debugEndOperation( - "Successfully generated builder: %s", builderDef.getBuilderTypeName().getClassName()); - } - - private TypeSpec.Builder createClassBuilder(BuilderDefinitionDto builderDef) { - ClassName builderBaseClass = map2ClassName(builderDef.getBuilderTypeName()); - if (CollectionUtils.isNotEmpty(builderDef.getGenerics())) { - logger.debug("Builder has %d generic type parameter(s)", builderDef.getGenerics().size()); - } - - TypeSpec.Builder result = - TypeSpec.classBuilder(builderBaseClass) - .addTypeVariables(map2TypeVariables(builderDef.getGenerics())); - logger.debug("Class builder created"); - return result; - } - - private void addClassMetadata(TypeSpec.Builder classBuilder, BuilderDefinitionDto builderDef) { - // Add class JavaDoc if provided by enhancer - if (builderDef.getClassJavadoc() != null) { - classBuilder.addJavadoc(builderDef.getClassJavadoc()); - } - - // Set builder class access level - Modifier builderAccessModifier = - JavaLangMapper.mapAccessModifier(builderDef.getConfiguration().getBuilderAccess()); - if (builderAccessModifier != null) { - classBuilder.addModifiers(builderAccessModifier); - } - - // Adding interfaces from enhancers - for (InterfaceName interfaceName : builderDef.getInterfaces()) { - com.palantir.javapoet.TypeName interfaceType = - JavapoetMapper.mapInterfaceToTypeName(interfaceName); - classBuilder.addSuperinterface(interfaceType); - } - - logger.debug("Class metadata added"); - } - - private void addFieldsToBuilder(TypeSpec.Builder classBuilder, BuilderDefinitionDto builderDef) { - logger.debugStartOperation( - "Generating %d constructor fields and %d setter fields", - builderDef.getConstructorFieldsForBuilder().size(), - builderDef.getSetterFieldsForBuilder().size()); - - // Generate backing fields for each DTO field (constructor and setter fields) - // Note: Builder field name conflicts are now resolved in BuilderDefinitionCreator - for (FieldDto fieldDto : builderDef.getConstructorFieldsForBuilder()) { - FieldSpec fieldSpec = createFieldMember(fieldDto); - classBuilder.addField(fieldSpec); - } - for (FieldDto fieldDto : builderDef.getSetterFieldsForBuilder()) { - FieldSpec fieldSpec = createFieldMember(fieldDto); - classBuilder.addField(fieldSpec); - } - logger.debugEndOperation("Fields added: %d fields", builderDef.getAllFieldsForBuilder().size()); - } - - private void addMethodsToBuilder(TypeSpec.Builder classBuilder, BuilderDefinitionDto builderDef) { - // Collect all methods from all fields, setting javadoc and tracking field relationship - Map allMethods = collectAllMethods(builderDef); - logger.debugStartOperation("Adding Methods for %d candidates", allMethods.size()); - - // Resolve conflicts and sort by ordering - List resolvedMethods = resolveMethodConflicts(allMethods); - logger.debug("Resolved %d methods after conflict resolution", resolvedMethods.size()); - - // Generate all methods in order - int generatedCnt = 0; - for (MethodDto methodDto : resolvedMethods) { - MethodSpec methodSpec = createMethod(methodDto); - classBuilder.addMethod(methodSpec); - generatedCnt++; - } - logger.debugEndOperation("%d Methods added", generatedCnt); - } - - 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); - } - } - - // Add core methods to the collection - for (MethodDto coreMethod : builderDef.getCoreMethods()) { - allMethods.put(coreMethod, null); // Core methods don't have associated fields - } - - return allMethods; - } - - private void addConstructorsToBuilder( - TypeSpec.Builder classBuilder, BuilderDefinitionDto builderDef) { - generateConstructors(classBuilder, builderDef); - logger.debug("Constructors added"); - } - - private void addNestedTypesToBuilder( - TypeSpec.Builder classBuilder, BuilderDefinitionDto builderDef) { - if (CollectionUtils.isEmpty(builderDef.getNestedTypes())) { - return; - } - // Adding nested types (e.g., With interface) - logger.debugStartOperation("Generating %d nested type(s)", builderDef.getNestedTypes().size()); - for (NestedTypeDto nestedType : builderDef.getNestedTypes()) { - TypeSpec nestedTypeSpec = createNestedType(nestedType); - classBuilder.addType(nestedTypeSpec); - logger.debug("Generated nested type: %s", nestedType.getTypeName()); - } - logger.debugEndOperation("Nested types added"); - } - - private void addAnnotationsToBuilder( - TypeSpec.Builder classBuilder, BuilderDefinitionDto builderDef) { - if (CollectionUtils.isEmpty(builderDef.getClassAnnotations())) { - return; - } - // Adding annotations from enhancers - for (AnnotationDto annotation : builderDef.getClassAnnotations()) { - AnnotationSpec annotationSpec = map2AnnotationSpec(annotation); - classBuilder.addAnnotation(annotationSpec); - } - - logger.debug("Class-level annotations added"); - } - - private void writeBuilderClassToFile(TypeSpec typeSpec, BuilderDefinitionDto builderDef) - throws BuilderException { - logger.debug( - "Writing builder class to file: %s.%s", - builderDef.getBuilderTypeName().getPackageName(), - builderDef.getBuilderTypeName().getClassName()); - - // Extract qualified name from the builder definition - String qualifiedName = builderDef.getBuilderTypeName().getFullQualifiedName(); - - // Check if builder class already exists before attempting to write - if (builderClassAlreadyExists(qualifiedName)) { - throw new BuilderException( - null, - """ - Builder class '%s' already exists. This may be a manually written builder or a previously generated builder. - To resolve this: - 1. If you have a manual builder, consider renaming it or removing @SimpleBuilder from the DTO - 2. If this is from a previous compilation, clean and rebuild the project - 3. Check that you're not trying to generate multiple builders for the same DTO - """ - .formatted(qualifiedName)); - } - - try { - JavaFile.builder(builderDef.getBuilderTypeName().getPackageName(), typeSpec) - .skipJavaLangImports(true) - .addStaticImport(TrackedValue.class, "initialValue") - .addStaticImport(TrackedValue.class, "changedValue") - .addStaticImport(TrackedValue.class, "unsetValue") - .build() - .writeTo(processingEnv.getFiler()); - } catch (IOException ex) { - // Handle file system errors during file write - String message = ex.getMessage(); - String errorMessage = - """ - Unable to create builder class '%s': %s. - Check the build environment and ensure all necessary directories are accessible. - """ - .formatted( - qualifiedName, StringUtils.isNotBlank(message) ? message : "Unknown error"); - throw new BuilderException(null, errorMessage); - } - } - - /** - * Checks if a builder class already exists by attempting to find the type element. - * - * @param qualifiedName the fully qualified name of the class to check - * @return true if the class already exists, false otherwise - */ - private boolean builderClassAlreadyExists(String qualifiedName) { - try { - TypeElement existingType = processingEnv.getElementUtils().getTypeElement(qualifiedName); - return existingType != null; - } catch (Exception e) { - // Log the exception at debug level - this should rarely happen but is useful for - // troubleshooting - logger.debug( - "Error checking if builder class '%s' already exists: %s", - qualifiedName, StringUtils.isNotBlank(e.getMessage()) ? e.getMessage() : "No message"); - // If there's any error checking, assume the class doesn't exist - return false; - } - } - - private void writeSimpleClassToFile(String packageName, TypeSpec typeSpec) - throws BuilderException { - try { - JavaFile.builder(packageName, typeSpec) - .skipJavaLangImports(true) - .build() - .writeTo(processingEnv.getFiler()); - } catch (IOException ex) { - // Handle file system issues for Jackson modules and other simple classes - 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); - } - } - - /** - * Resolves method conflicts by keeping only the highest priority method for each signature and - * sorts all methods by ordering then name. This prevents compilation errors when methods from - * different fields have the same signature and ensures proper method generation order. - * - * @param methodToField mapping from method to its source field (null for core methods) - * @return list of all methods with conflicts resolved, sorted by ordering for proper generation - */ - private List resolveMethodConflicts(Map methodToField) { - MethodDto.MethodComparator comparator = new MethodDto.MethodComparator(); - - // Sort entries using MethodComparator for deterministic conflict resolution - // This ensures consistent behavior when multiple methods have the same signature - List> sortedEntries = - methodToField.entrySet().stream() - .sorted((e1, e2) -> comparator.compare(e1.getKey(), e2.getKey())) - .toList(); - - // Use LinkedHashMap to preserve insertion order from sorted entries - Map signatureToMethod = new java.util.LinkedHashMap<>(); - - // Process all methods and resolve conflicts - for (Map.Entry entry : sortedEntries) { - MethodDto method = entry.getKey(); - FieldDto field = entry.getValue(); - String signature = method.getSignatureKey(); - - MethodDto existing = signatureToMethod.get(signature); - if (existing == null) { - // No conflict, add the method - signatureToMethod.put(signature, method); - } else { - // Conflict detected: keep the higher priority method - String existingSource = getSourceDescription(existing, methodToField.get(existing)); - String newSource = getSourceDescription(method, field); - - if (method.getPriority() > existing.getPriority()) { - // New method wins - 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()); - } else if (method.getPriority() < existing.getPriority()) { - // Existing method wins - logger.warning( - " Method conflict: '%s' from %s (priority %d) dropped in favor of %s (priority %d)", - signature, newSource, method.getPriority(), existingSource, existing.getPriority()); - } else { - // Equal priority - keep first - 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()); - } - } - } - - // Return methods in insertion order (already sorted from conflict resolution) - return new java.util.ArrayList<>(signatureToMethod.values()); - } - - /** - * Gets a description of the method source for logging purposes. - * - * @param method the method - * @param field the associated field (null for core methods) - * @return description of the method source - */ - private String getSourceDescription(MethodDto method, FieldDto field) { - if (field == null) { - return "core method '" + method.getMethodName() + "'"; - } else { - return "field '" + field.getFieldNameInBuilder() + "'"; - } - } - - /** - * Generates constructors for the builder class. - * - * @param classBuilder the TypeSpec.Builder to add constructors to - * @param builderDef the builder definition containing field information - */ - private void generateConstructors( - TypeSpec.Builder classBuilder, BuilderDefinitionDto builderDef) { - // Get access modifiers from configuration - Modifier constructorAccessModifier = - JavaLangMapper.mapAccessModifier( - builderDef.getConfiguration().getBuilderConstructorAccess()); - ClassName dtoBaseClass = map2ClassName(builderDef.getBuildingTargetTypeName()); - - // Generate empty constructor - MethodSpec emptyConstructor = createEmptyConstructor(dtoBaseClass, constructorAccessModifier); - classBuilder.addMethod(emptyConstructor); - - // Generate constructor with instance - com.palantir.javapoet.TypeName dtoTypeName = - map2ParameterType(builderDef.getBuildingTargetTypeName()); - MethodSpec instanceConstructor = - createConstructorWithInstance( - dtoBaseClass, - dtoTypeName, - builderDef.getAllFieldsForBuilder(), - constructorAccessModifier); - classBuilder.addMethod(instanceConstructor); - } - - private MethodSpec createEmptyConstructor(ClassName dtoClass, Modifier accessModifier) { - MethodSpec.Builder constructorBuilder = - MethodSpec.constructorBuilder() - .addModifiers(accessModifier) - .addJavadoc( - """ - Empty constructor of builder for {@code $1N.$2T}. - """, - dtoClass.packageName(), - dtoClass); - return constructorBuilder.build(); - } - - private MethodSpec createConstructorWithInstance( - ClassName dtoBaseClass, - com.palantir.javapoet.TypeName dtoType, - List fields, - Modifier accessModifier) { - MethodSpec.Builder cb = - MethodSpec.constructorBuilder() - .addModifiers(accessModifier) - .addParameter(dtoType, "instance") - .addJavadoc( - """ - Initialisation of builder for {@code $1N.$2T} by a instance. - - @param instance object instance for initialisiation - """, - dtoBaseClass.packageName(), - dtoBaseClass); - - for (FieldDto f : fields) { - f.getGetterName().ifPresent(getter -> addFieldInitializationWithValidation(cb, f, getter)); - } - return cb.build(); - } - - private void addFieldInitializationWithValidation( - MethodSpec.Builder cb, FieldDto field, String getter) { - // Initialize field from source instance - cb.addStatement( - "this.$N = $T.initialValue(instance.$N())", - field.getFieldNameInBuilder(), - ClassName.get(TrackedValue.class), - getter); - - // Validate non-nullable fields immediately - fail fast if source object is invalid - if (field.isNonNullable()) { - cb.beginControlFlow("if (this.$N.value() == null)", field.getFieldNameInBuilder()) - .addStatement( - "throw new $T($S)", - IllegalArgumentException.class, - "Cannot initialize builder from instance: field '" - + field.getFieldNameInBuilder() - + "' is marked as non-null but source object has null value") - .endControlFlow(); - } - } - - private FieldSpec createFieldMember(FieldDto fieldDto) { - com.palantir.javapoet.TypeName fieldType = map2ParameterType(fieldDto.getFieldType()); - if (fieldType.isPrimitive()) { - fieldType = fieldType.box(); - } - // Wrap all fields in TrackedValue - ClassName builderFieldWrapper = ClassName.get(TrackedValue.class); - ParameterizedTypeName wrappedFieldType = - ParameterizedTypeName.get(builderFieldWrapper, fieldType); - - return FieldSpec.builder(wrappedFieldType, fieldDto.getFieldNameInBuilder(), Modifier.PRIVATE) - .addJavadoc( - "Tracked value for $L: $L.\n", - fieldDto.getFieldNameInBuilder(), - fieldDto.getJavaDoc()) - .initializer("$T.unsetValue()", builderFieldWrapper) - .build(); - } - - private MethodSpec createMethod(MethodDto methodDto) { - com.palantir.javapoet.TypeName returnType = map2ParameterType(methodDto.getReturnType()); - MethodSpec.Builder methodBuilder = - MethodSpec.methodBuilder(methodDto.getMethodName()).returns(returnType); - - // Use modifier from MethodDto if present - methodDto.getModifier().ifPresent(methodBuilder::addModifiers); - - // Add static modifier if method is static - if (methodDto.isStatic()) { - methodBuilder.addModifiers(javax.lang.model.element.Modifier.STATIC); - } - - // Add generic type parameters if any - if (CollectionUtils.isNotEmpty(methodDto.getGenericParameters())) { - List typeVariables = - methodDto.getGenericParameters().stream() - .map(param -> com.palantir.javapoet.TypeVariableName.get(param.getName())) - .toList(); - methodBuilder.addTypeVariables(typeVariables); - } - - // Use javadoc from MethodDto if available - if (StringUtils.isNoneBlank(methodDto.getJavadoc())) { - methodBuilder.addJavadoc(methodDto.getJavadoc()); - } - - // Add annotations from MethodDto - if (!methodDto.getAnnotations().isEmpty()) { - methodBuilder.addAnnotations(map2AnnotationSpecs(methodDto.getAnnotations())); - } - - // Add parameters - for (MethodParameterDto paramDto : methodDto.getParameters()) { - methodBuilder.addParameter(createParameter(paramDto)); - if (paramDto.getParameterType() instanceof TypeNameArray) { - methodBuilder.varargs(); // Arrays should be mapped to be generics - } - } - - CodeBlock codeBlock = map2CodeBlock(methodDto.getMethodCodeDto()); - methodBuilder.addCode(codeBlock); - return methodBuilder.build(); - } - - private ParameterSpec createParameter(MethodParameterDto paramDto) { - com.palantir.javapoet.TypeName parameterType = map2ParameterType(paramDto.getParameterType()); - ParameterSpec.Builder paramBuilder = - ParameterSpec.builder(parameterType, paramDto.getParameterName()); - if (!paramDto.getAnnotations().isEmpty()) { - paramBuilder.addAnnotations(map2AnnotationSpecs(paramDto.getAnnotations())); - } - return paramBuilder.build(); - } - - private TypeSpec createNestedType(NestedTypeDto nestedType) { - TypeSpec.Builder typeBuilder; - boolean isInterface = nestedType.getKind() == NestedTypeDto.NestedTypeKind.INTERFACE; - if (isInterface) { - typeBuilder = TypeSpec.interfaceBuilder(nestedType.getTypeName()); - } else { - typeBuilder = TypeSpec.classBuilder(nestedType.getTypeName()); - } - - if (nestedType.isPublic()) { - typeBuilder.addModifiers(PUBLIC); - } - - if (nestedType.getJavadoc() != null) { - typeBuilder.addJavadoc(nestedType.getJavadoc()); - } - - for (MethodDto methodDto : nestedType.getMethods()) { - MethodSpec methodSpec = createNestedTypeMethod(methodDto, isInterface); - typeBuilder.addMethod(methodSpec); - } - - return typeBuilder.build(); - } - - /** - * Creates a method specification from a MethodDto for nested types (e.g., With interface - * methods). - * - * @param methodDto the method definition - * @param isInterface whether the containing type is an interface - * @return the generated MethodSpec - */ - private MethodSpec createNestedTypeMethod(MethodDto methodDto, boolean isInterface) { - MethodSpec.Builder methodBuilder = - MethodSpec.methodBuilder(methodDto.getMethodName()).addModifiers(PUBLIC); - - // Set return type using mapper - methodBuilder.returns(JavapoetMapper.map2ParameterType(methodDto.getReturnType())); - - // Add parameters using mapper - for (MethodParameterDto paramDto : methodDto.getParameters()) { - methodBuilder.addParameter(createParameter(paramDto)); - } - - if (methodDto.getJavadoc() != null) { - methodBuilder.addJavadoc(methodDto.getJavadoc()); - } - - // Add code only if method has implementation (even for interfaces with default methods) - if (methodDto.getMethodCodeDto() != null) { - if (isInterface) { - methodBuilder.addModifiers(javax.lang.model.element.Modifier.DEFAULT); - } - - methodBuilder.addCode(map2CodeBlock(methodDto.getMethodCodeDto())); - } - - return methodBuilder.build(); - } - - /** - * 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); - - ClassName simpleModuleClass = - ClassName.get("com.fasterxml.jackson.databind.module", "SimpleModule"); - ClassName jsonDeserializeClass = - ClassName.get("com.fasterxml.jackson.databind.annotation", "JsonDeserialize"); - - // Create the constructor - MethodSpec.Builder constructorBuilder = - MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC); - - // Create the class - TypeSpec.Builder classBuilder = - TypeSpec.classBuilder(moduleClassName) - .addModifiers(Modifier.PUBLIC) - .superclass(simpleModuleClass); - - for (JacksonModuleEntryDto entry : moduleDef.getEntries()) { - ClassName dtoClass = - ClassName.get(entry.dtoType().getPackageName(), entry.dtoType().getClassName()); - ClassName builderClass = - ClassName.get(entry.builderType().getPackageName(), entry.builderType().getClassName()); - - // Create MixIn interface name: DtoNameMixin - String mixinName = entry.dtoType().getClassName() + "Mixin"; - - // Create MixIn interface with @JsonDeserialize(builder = Builder.class) - TypeSpec mixinInterface = - TypeSpec.interfaceBuilder(mixinName) - .addModifiers(Modifier.PRIVATE) - .addAnnotation( - AnnotationSpec.builder(jsonDeserializeClass) - .addMember("builder", "$T.class", builderClass) - .build()) - .build(); - - classBuilder.addType(mixinInterface); - - // Add registration to constructor: setMixInAnnotation(Dto.class, Mixin.class) - constructorBuilder.addStatement( - "setMixInAnnotation($T.class, $N.class)", dtoClass, mixinName); - } - - classBuilder.addMethod(constructorBuilder.build()); - - // Write file - try { - writeSimpleClassToFile(packageName, classBuilder.build()); - } 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())); - } - } -} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/javapoet/JavapoetMapper.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/javapoet/JavapoetMapper.java deleted file mode 100644 index c8cbb2f0..00000000 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/javapoet/JavapoetMapper.java +++ /dev/null @@ -1,272 +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.classgen.javapoet; - -import com.palantir.javapoet.AnnotationSpec; -import com.palantir.javapoet.ArrayTypeName; -import com.palantir.javapoet.ClassName; -import com.palantir.javapoet.CodeBlock; -import com.palantir.javapoet.ParameterizedTypeName; -import com.palantir.javapoet.TypeName; -import com.palantir.javapoet.TypeVariableName; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; -import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.StringUtils; -import org.javahelpers.simple.builders.processor.classgen.javapoet.exceptions.JavapoetMapperException; -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.method.MethodCodePlaceholder; -import org.javahelpers.simple.builders.processor.model.method.MethodCodeStringPlaceholder; -import org.javahelpers.simple.builders.processor.model.method.MethodCodeTypePlaceholder; -import org.javahelpers.simple.builders.processor.model.type.GenericParameterDto; -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; - -/** Helper functions to create JavaPoet types from DTOs of simple builder. */ -public final class JavapoetMapper { - - private JavapoetMapper() {} - - /** - * Maps a list of simple-builder DTO type names to an array of JavaPoet {@code TypeName}s. - * Primitives are boxed because JavaPoet requires reference types for type arguments. - * - * @param typeArguments the list of type arguments to map - * @return array of JavaPoet TypeName instances - */ - public static TypeName[] map2TypeArgumentsArray( - List typeArguments) { - java.util.List args = new java.util.ArrayList<>(typeArguments.size()); - for (org.javahelpers.simple.builders.processor.model.type.TypeName tn : typeArguments) { - TypeName mapped = map2ParameterType(tn); - if (mapped.isPrimitive()) { - mapped = mapped.box(); - } - args.add(mapped); - } - return args.toArray(new TypeName[0]); - } - - /** - * Mapper for parameterType. Maps into javapoet classes. - * - * @param parameterType simple-builder dto to be mapped - * @return javapoet TypeName - */ - public static TypeName map2ParameterType( - org.javahelpers.simple.builders.processor.model.type.TypeName parameterType) { - TypeName typeName; - if (parameterType - instanceof - org.javahelpers.simple.builders.processor.model.type.TypeNameVariable typeVariable) { - typeName = TypeVariableName.get(typeVariable.getClassName()); - } else if (parameterType instanceof TypeNamePrimitive parameterTypePrim) { - typeName = mapPrimitive(parameterTypePrim); - } else if (parameterType instanceof TypeNameArray parameterTypeArray) { - typeName = ArrayTypeName.of(map2ParameterType(parameterTypeArray.getTypeOfArray())); - } else if (parameterType instanceof TypeNameGeneric parameterTypeGeneric) { - typeName = mapGeneric(parameterTypeGeneric); - } else { - typeName = ClassName.get(parameterType.getPackageName(), parameterType.getClassName()); - } - - if (typeName != null && CollectionUtils.isNotEmpty(parameterType.getAnnotations())) { - typeName = typeName.annotated(map2AnnotationSpecs(parameterType.getAnnotations())); - } - return typeName; - } - - private static TypeName mapPrimitive(TypeNamePrimitive parameterTypePrim) { - return switch (parameterTypePrim.getType()) { - case BOOLEAN -> TypeName.BOOLEAN; - case BYTE -> TypeName.BYTE; - case CHAR -> TypeName.CHAR; - case DOUBLE -> TypeName.DOUBLE; - case FLOAT -> TypeName.FLOAT; - case INT -> TypeName.INT; - case LONG -> TypeName.LONG; - case SHORT -> TypeName.SHORT; - default -> null; - }; - } - - private static TypeName mapGeneric(TypeNameGeneric param) { - ClassName classNameParameter = ClassName.get(param.getPackageName(), param.getClassName()); - if (param.getInnerTypeArguments().isEmpty()) { - return classNameParameter; - } - TypeName[] typeArgs = map2TypeArgumentsArray(param.getInnerTypeArguments()); - return ParameterizedTypeName.get(classNameParameter, typeArgs); - } - - /** - * Mapper for typename. Maps into javapoet classes. - * - * @param typeName simple-builder dto to be mapped - * @return javapoet TypeName - */ - public static ClassName map2ClassName( - org.javahelpers.simple.builders.processor.model.type.TypeName typeName) { - if (StringUtils.isNoneEmpty(typeName.getPackageName())) { - return ClassName.get(typeName.getPackageName(), typeName.getClassName()); - } else { - return ClassName.bestGuess(typeName.getClassName()); - } - } - - /** - * Maps a base type and generic parameters to a JavaPoet ParameterizedTypeName. - * - * @param baseType the base type to parameterize - * @param builderGenerics the list of generic parameters - * @return a ParameterizedTypeName with the given type parameters - */ - public static ParameterizedTypeName map2ParameterizedTypeName( - org.javahelpers.simple.builders.processor.model.type.TypeName baseType, - List builderGenerics) { - ClassName baseTypeClassName = map2ClassName(baseType); - return ParameterizedTypeName.get( - baseTypeClassName, map2TypeVariables(builderGenerics).toArray(new TypeVariableName[0])); - } - - /** - * Maps a list of GenericParameterDto to JavaPoet TypeVariableName instances. - * - * @param builderGenerics the list of generic parameters to map - * @return list of TypeVariableName representing the generic parameters - */ - public static List map2TypeVariables( - List builderGenerics) { - List javapoetGenerics = new ArrayList<>(); - for (GenericParameterDto g : builderGenerics) { - List bounds = new ArrayList<>(); - for (org.javahelpers.simple.builders.processor.model.type.TypeName b : g.getUpperBounds()) { - bounds.add(map2ParameterType(b)); - } - TypeVariableName tv = - bounds.isEmpty() - ? TypeVariableName.get(g.getName()) - : TypeVariableName.get(g.getName(), bounds.toArray(new TypeName[0])); - javapoetGenerics.add(tv); - } - return javapoetGenerics; - } - - /** - * CodeBlock creating by definition in {@code MethodCodeDto}. - * - * @param codeDto code definition - * @return {@code CodeBlock} of javapoet - */ - public static CodeBlock map2CodeBlock( - org.javahelpers.simple.builders.processor.model.method.MethodCodeDto codeDto) { - Map arguments = - codeDto.getCodeArguments().stream() - .collect( - Collectors.toMap( - MethodCodePlaceholder::getLabel, JavapoetMapper::toCodeblockValue)); - return CodeBlock.builder().addNamed(codeDto.getCodeFormat(), arguments).build(); - } - - private static Object toCodeblockValue(MethodCodePlaceholder placeHolderValue) { - if (placeHolderValue instanceof MethodCodeStringPlaceholder stringPlaceholder) { - return stringPlaceholder.getValue(); - } else if (placeHolderValue instanceof MethodCodeTypePlaceholder typePlaceholder) { - return map2ParameterType(typePlaceholder.getValue()); - } else { - throw new UnsupportedOperationException( - "Unsupported placeholder type: " + placeHolderValue.getClass()); - } - } - - /** - * Maps an AnnotationDto to a JavaPoet AnnotationSpec. - * - * @param annotationDto the annotation DTO to map - * @return the mapped JavaPoet AnnotationSpec, always not null - * @throws JavapoetMapperException if mapping fails - */ - public static AnnotationSpec map2AnnotationSpec(AnnotationDto annotationDto) { - try { - ClassName annotationType = map2ClassName(annotationDto.getAnnotationType()); - AnnotationSpec.Builder builder = AnnotationSpec.builder(annotationType); - - for (Map.Entry member : annotationDto.getMembers().entrySet()) { - builder.addMember(member.getKey(), "$L", member.getValue()); - } - - return builder.build(); - } catch (Exception e) { - throw new JavapoetMapperException( - e, - "Failed to map annotation %s: %s", - annotationDto.getAnnotationType().getClassName(), - e.getMessage()); - } - } - - /** - * Maps a list of AnnotationDto to JavaPoet AnnotationSpec instances. - * - * @param annotations the list of annotations to map - * @return list of AnnotationSpec for all annotations - * @throws JavapoetMapperException if any annotation mapping fails - */ - public static List map2AnnotationSpecs(List annotations) { - return annotations.stream().map(JavapoetMapper::map2AnnotationSpec).toList(); - } - - /** - * Maps an InterfaceName to a JavaPoet TypeName. - * - * @param interfaceName the interface name to map - * @return the mapped JavaPoet TypeName, always not null - * @throws JavapoetMapperException if mapping fails - */ - public static TypeName mapInterfaceToTypeName(InterfaceName interfaceName) { - try { - TypeName interfaceType = - ClassName.get(interfaceName.getPackageName(), interfaceName.getSimpleName()); - - // Add type parameters if present - if (interfaceName.hasTypeParameters()) { - TypeName[] typeArgs = - interfaceName.getTypeParameters().stream() - .map(JavapoetMapper::map2ParameterType) - .toArray(TypeName[]::new); - interfaceType = ParameterizedTypeName.get((ClassName) interfaceType, typeArgs); - } - - return interfaceType; - } catch (Exception e) { - throw new JavapoetMapperException( - e, "Failed to map interface %s: %s", interfaceName.toString(), e.getMessage()); - } - } -} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/javapoet/exceptions/JavapoetMapperException.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/javapoet/exceptions/JavapoetMapperException.java deleted file mode 100644 index 75cef175..00000000 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/javapoet/exceptions/JavapoetMapperException.java +++ /dev/null @@ -1,59 +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.classgen.javapoet.exceptions; - -/** Special exception for errors in mapping to Javapoet classes. */ -public class JavapoetMapperException extends RuntimeException { - - /** - * Creating an exception with message and parameters. - * - * @param cause root cause of current exception, containing stacktrace - */ - public JavapoetMapperException(Throwable cause) { - super(cause.getMessage(), cause); - } - - /** - * Creating an exception with message and parameters. - * - * @param message A specific message, supports String.format arguments - * @param args Arguments for Stringlformat on message - */ - public JavapoetMapperException(String message, Object... args) { - super(String.format(message, args)); - } - - /** - * Creating an exception with message and parameters. - * - * @param cause root cause of current exception, containing stacktrace - * @param message A specific message, supports String.format arguments - * @param args Arguments for Stringlformat on message - */ - public JavapoetMapperException(Throwable cause, String message, Object... args) { - super(String.format(message, args), cause); - } -} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/javapoet/exceptions/package-info.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/javapoet/exceptions/package-info.java deleted file mode 100644 index 76bcbff7..00000000 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/javapoet/exceptions/package-info.java +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Package org.javahelpers.simple.builders.processor.classgen.javapoet.exceptions - * - *

JavaPoet-specific exception classes. - * - *

This package contains exceptions specific to JavaPoet code generation, providing error - * handling for mapping and code generation issues within the JavaPoet layer. - */ -package org.javahelpers.simple.builders.processor.classgen.javapoet.exceptions; diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/javapoet/package-info.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/javapoet/package-info.java deleted file mode 100644 index 0574a8c5..00000000 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/javapoet/package-info.java +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Package org.javahelpers.simple.builders.processor.classgen.javapoet - * - *

JavaPoet-based code generation for simple-builders processor. - * - *

This package contains the core code generation components: - * - *

    - *
  • {@link JavaCodeGenerator} - Main code generator that creates builder classes - *
  • {@link JavapoetMapper} - Utility for mapping DTOs to JavaPoet types - *
- * - *

Exception classes are organized in separate packages: - * - *

    - *
  • {@link org.javahelpers.simple.builders.processor.exceptions.BuilderException} - Exception - * for code generation errors - *
  • {@link - * org.javahelpers.simple.builders.processor.classgen.javapoet.exceptions.JavapoetMapperException} - * - Exception for mapping errors - *
- * - *

This package isolates JavaPoet-specific code, making it easier to replace the code generation - * implementation if needed. - */ -package org.javahelpers.simple.builders.processor.classgen.javapoet; From 94e10da76b3c2825a8eed8887ea68e5cc64f0199 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Wed, 18 Mar 2026 21:26:28 +0100 Subject: [PATCH 15/20] Solving possible regex attack vector --- .../processor/classgen/roaster/RoasterCodeGenerator.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 b7357101..ecde4f16 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 @@ -503,7 +503,10 @@ private void applyJavadoc( if (StringUtils.isBlank(javadoc)) { return; } - String normalized = javadoc.replace("\r\n", "\n").replace("\r", "\n").replaceFirst("\\n+$", ""); + // Normalize line endings without regex to avoid ReDoS vulnerability + String normalized = javadoc.replace("\r\n", "\n").replace("\r", "\n"); + // Remove trailing newlines using StringUtils + normalized = StringUtils.stripEnd(normalized, "\n"); String[] lines = normalized.split("\n", -1); StringBuilder text = new StringBuilder(); source.getJavaDoc().removeAllTags(); From 6049ba128ead088a23e15c6ced252d9d8d65812a Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Wed, 18 Mar 2026 21:40:53 +0100 Subject: [PATCH 16/20] Improving code quality --- .../roaster/RoasterCodeGenerator.java | 43 +++++++++++-------- 1 file changed, 24 insertions(+), 19 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 ecde4f16..c4b6d91f 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 @@ -127,7 +127,7 @@ private String createBuilderSource(BuilderDefinitionDto builderDef) { appendMethods(source, builderDef); appendNestedTypes(source, builderDef); applyClassAnnotations(source, builderDef); - return renderClassSource(source, builderDef); + return renderClassSource(source); } private void applyClassAnnotations(JavaClassSource source, BuilderDefinitionDto builderDef) { @@ -175,7 +175,7 @@ private void addClassMetadata(JavaClassSource source, BuilderDefinitionDto build logger.debug("Class metadata added"); } - private String renderClassSource(JavaClassSource source, BuilderDefinitionDto builderDef) { + private String renderClassSource(JavaClassSource source) { String rendered = source.toUnformattedString(); return formatSource(rendered); } @@ -398,7 +398,7 @@ private void appendMethod( MethodDto methodDto, boolean nestedTypeMethod, boolean interfaceMethod) { - MethodSource method = source.addMethod(); + MethodSource method = source.addMethod(); configureMethod(method, methodDto, nestedTypeMethod, interfaceMethod); String body = methodDto.getMethodCodeDto() != null @@ -439,9 +439,9 @@ private void appendNestedType(JavaClassSource source, NestedTypeDto nestedType) } private void appendNestedMethod(JavaSource source, MethodDto methodDto, boolean isInterface) { - org.jboss.forge.roaster.model.source.MethodHolderSource methodHolder = - (org.jboss.forge.roaster.model.source.MethodHolderSource) source; - MethodSource method = methodHolder.addMethod(); + org.jboss.forge.roaster.model.source.MethodHolderSource methodHolder = + (org.jboss.forge.roaster.model.source.MethodHolderSource) source; + MethodSource method = methodHolder.addMethod(); configureMethod(method, methodDto, true, isInterface); if (methodDto.getMethodCodeDto() != null && StringUtils.isNotBlank(methodDto.getMethodCodeDto().getCodeFormat())) { @@ -516,16 +516,9 @@ private void applyJavadoc( inTags = true; } if (inTags && line.startsWith("@")) { - int firstSpace = line.indexOf(' '); - if (firstSpace > 1) { - source - .getJavaDoc() - .addTagValue(line.substring(0, firstSpace), line.substring(firstSpace + 1)); - } else if (line.length() > 1) { - source.getJavaDoc().addTagValue(line, ""); - } + processJavadocTag(source, line); } else { - if (text.length() > 0) { + if (!text.isEmpty()) { text.append('\n'); } text.append(line); @@ -534,6 +527,18 @@ private void applyJavadoc( source.getJavaDoc().setText(text.toString()); } + private void processJavadocTag( + org.jboss.forge.roaster.model.source.JavaDocCapableSource source, String line) { + int firstSpace = line.indexOf(' '); + if (firstSpace > 1) { + source + .getJavaDoc() + .addTagValue(line.substring(0, firstSpace), line.substring(firstSpace + 1)); + } else if (line.length() > 1) { + source.getJavaDoc().addTagValue(line, ""); + } + } + private void applyAnnotations( org.jboss.forge.roaster.model.source.AnnotationTargetSource source, java.util.Collection annotations) { @@ -767,7 +772,7 @@ private void addBodyImports(Set imports, String currentPackage, MethodDt } private String formatSource(String rawSource) { - if (formatterProperties == null) { + if (formatterProperties.isEmpty()) { return rawSource; } try { @@ -789,7 +794,7 @@ private Properties loadFormatterProperties() { logger.warning( "simple-builders: Bundled Eclipse formatter profile '%s' was not found on the processor classpath.", FORMATTER_PROFILE_RESOURCE); - return null; + return new Properties(); } FormatterProfileReader profileReader = FormatterProfileReader.fromEclipseXml(inputStream); return profileReader.getDefaultProperties(); @@ -798,7 +803,7 @@ private Properties loadFormatterProperties() { "simple-builders: Failed to load bundled Eclipse formatter profile '%s': %s", FORMATTER_PROFILE_RESOURCE, StringUtils.defaultIfBlank(ex.getMessage(), ex.getClass().getSimpleName())); - return null; + return new Properties(); } } @@ -923,7 +928,7 @@ public void generateJacksonModule(JacksonModuleDefinitionDto moduleDef) { for (JacksonModuleEntryDto entry : moduleDef.getEntries()) { String mixinName = entry.dtoType().getClassName() + "Mixin"; constructorBody.append( - "setMixInAnnotation(%s.class, %s.class);\n" + "setMixInAnnotation(%s.class, %s.class);%n" .formatted(entry.dtoType().getClassName(), mixinName)); } constructor.setBody(constructorBody.toString()); From fb52db1c1aae869aea3a58f6516053fcff74b460 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Wed, 18 Mar 2026 21:45:27 +0100 Subject: [PATCH 17/20] Removing further unused parameters --- .../classgen/roaster/RoasterCodeGenerator.java | 15 +++------------ 1 file changed, 3 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 c4b6d91f..bd86c0f0 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 @@ -220,22 +220,14 @@ private void appendConstructors(JavaClassSource source, BuilderDefinitionDto bui builderDef.getConfiguration().getBuilderConstructorAccess()); TypeName dtoBaseClass = builderDef.getBuildingTargetTypeName(); - appendEmptyConstructor( - source, - dtoBaseClass, - builderDef.getBuilderTypeName().getClassName(), - constructorAccessModifier); + appendEmptyConstructor(source, dtoBaseClass, constructorAccessModifier); appendConstructorWithInstance( - source, - dtoBaseClass, - builderDef.getBuilderTypeName().getClassName(), - builderDef.getAllFieldsForBuilder(), - constructorAccessModifier); + source, dtoBaseClass, builderDef.getAllFieldsForBuilder(), constructorAccessModifier); logger.debug("Constructors added"); } private void appendEmptyConstructor( - JavaClassSource source, TypeName dtoClass, String builderClassName, Modifier accessModifier) { + JavaClassSource source, TypeName dtoClass, Modifier accessModifier) { MethodSource constructor = source.addMethod(); constructor.setConstructor(true); applyVisibility(constructor, accessModifier); @@ -248,7 +240,6 @@ private void appendEmptyConstructor( private void appendConstructorWithInstance( JavaClassSource source, TypeName dtoBaseClass, - String builderClassName, List fields, Modifier accessModifier) { MethodSource constructor = source.addMethod(); From 2d15907a20f53f6b5b580e746b67c6cbda80dc78 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Thu, 19 Mar 2026 21:46:01 +0100 Subject: [PATCH 18/20] Adding tests for edgecases in CodeGenerator --- .../RoasterCodeGeneratorEdgeCasesTest.java | 236 ++++++++++++++++++ 1 file changed, 236 insertions(+) create mode 100644 processor/src/test/java/org/javahelpers/simple/builders/processor/RoasterCodeGeneratorEdgeCasesTest.java diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/RoasterCodeGeneratorEdgeCasesTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/RoasterCodeGeneratorEdgeCasesTest.java new file mode 100644 index 00000000..bd658c1e --- /dev/null +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/RoasterCodeGeneratorEdgeCasesTest.java @@ -0,0 +1,236 @@ +/* + * 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; + +import static org.javahelpers.simple.builders.processor.testing.ProcessorAsserts.assertContaining; +import static org.javahelpers.simple.builders.processor.testing.ProcessorAsserts.assertGenerationSucceeded; +import static org.javahelpers.simple.builders.processor.testing.ProcessorAsserts.assertNotContaining; +import static org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils.loadGeneratedSource; + +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import javax.tools.JavaFileObject; +import org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils; +import org.junit.jupiter.api.Test; + +/** + * Integration tests for RoasterCodeGenerator edge cases to improve code coverage. + * + *

These tests exercise specific code paths in RoasterCodeGenerator that are typically not + * covered by standard integration tests, such as empty collections, null checks, and edge cases. + */ +class RoasterCodeGeneratorEdgeCasesTest { + + /** + * Tests that builders can be generated for DTOs in the default package (no package declaration). + * + *

This edge case ensures the code generator handles the absence of a package name correctly + * and generates valid builder code without package declarations. + */ + @Test + void shouldHandleBuilderInDefaultPackage() { + JavaFileObject sourceFile = + JavaFileObjects.forSourceString( + "SimpleDto", + """ + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + + @SimpleBuilder + public class SimpleDto { + private String name; + public String getName() { return name; } + public void setName(String name) { this.name = name; } + } + """); + + Compilation compilation = ProcessorTestUtils.createCompiler().compile(sourceFile); + + String generatedCode = loadGeneratedSource(compilation, "SimpleDtoBuilder"); + assertGenerationSucceeded(compilation, "SimpleDtoBuilder", generatedCode); + assertContaining(generatedCode, "public class SimpleDtoBuilder"); + assertNotContaining("package"); + } + + /** + * Tests that builders are generated correctly when class-level annotations are disabled. + * + *

This edge case verifies that the code generator correctly handles the empty annotation list + * when {@code usingGeneratedAnnotation} and {@code usingBuilderImplementationAnnotation} are both + * disabled, ensuring the annotation copying logic skips processing when no annotations should be + * added. + */ + @Test + void shouldHandleBuilderWithNoClassAnnotations() { + JavaFileObject sourceFile = + ProcessorTestUtils.simpleBuilderClass( + "test", + "NoAnnotationsDto", + """ + private String value; + public String getValue() { return value; } + public void setValue(String value) { this.value = value; } + """); + + Compilation compilation = + ProcessorTestUtils.createCompiler() + .withOptions( + "-Asimplebuilder.usingGeneratedAnnotation=false", + "-Asimplebuilder.usingBuilderImplementationAnnotation=false") + .compile(sourceFile); + + String generatedCode = loadGeneratedSource(compilation, "NoAnnotationsDtoBuilder"); + assertGenerationSucceeded(compilation, "NoAnnotationsDtoBuilder", generatedCode); + assertContaining(generatedCode, "public class NoAnnotationsDtoBuilder"); + // Verify no class-level annotations are present + // (check for annotations before the class declaration) + assertNotContaining(generatedCode, "@Generated", "@BuilderImplementation"); + } + + /** + * Tests that builders are generated correctly when the With interface is disabled. + * + *

This edge case ensures the code generator handles builders with no nested types (the With + * interface is the only nested type typically generated in builders). When {@code + * generateWithInterface=false}, the builder should have no inner interfaces, and the nested type + * generation logic should correctly skip processing when the nested type list is empty. + */ + @Test + void shouldHandleBuilderWithNoNestedTypes() { + JavaFileObject sourceFile = + ProcessorTestUtils.simpleBuilderClass( + "test", + "NoNestedTypesDto", + """ + private Integer count; + public Integer getCount() { return count; } + public void setCount(Integer count) { this.count = count; } + """); + + Compilation compilation = + ProcessorTestUtils.createCompiler() + .withOptions("-Asimplebuilder.generateWithInterface=false") + .compile(sourceFile); + + String generatedCode = loadGeneratedSource(compilation, "NoNestedTypesDtoBuilder"); + assertGenerationSucceeded(compilation, "NoNestedTypesDtoBuilder", generatedCode); + assertContaining(generatedCode, "public class NoNestedTypesDtoBuilder"); + // Verify no With interface is generated + assertNotContaining(generatedCode, "public interface With"); + } + + /** + * Tests that fields without JavaDoc documentation are handled correctly. + * + *

This edge case verifies that the code generator handles blank or missing JavaDoc strings and + * skips JavaDoc generation when documentation is not provided. + */ + @Test + void shouldHandleFieldWithNoJavadoc() { + JavaFileObject sourceFile = + ProcessorTestUtils.simpleBuilderClass( + "test", + "NoJavadocDto", + """ + private String field; + public String getField() { return field; } + public void setField(String field) { this.field = field; } + """); + + Compilation compilation = ProcessorTestUtils.createCompiler().compile(sourceFile); + + String generatedCode = loadGeneratedSource(compilation, "NoJavadocDtoBuilder"); + assertGenerationSucceeded(compilation, "NoJavadocDtoBuilder", generatedCode); + assertContaining(generatedCode, "private TrackedValue field"); + } + + /** + * Tests that primitive types (int, boolean, double) are handled correctly in builders. + * + *

This edge case ensures primitive types are properly boxed in TrackedValue and that primitive + * type imports are skipped (since primitives don't require imports). + */ + @Test + void shouldHandlePrimitiveTypes() { + JavaFileObject sourceFile = + ProcessorTestUtils.simpleBuilderClass( + "test", + "PrimitiveDto", + """ + private int count; + private boolean active; + private double value; + public int getCount() { return count; } + public void setCount(int count) { this.count = count; } + public boolean isActive() { return active; } + public void setActive(boolean active) { this.active = active; } + public double getValue() { return value; } + public void setValue(double value) { this.value = value; } + """); + + Compilation compilation = ProcessorTestUtils.createCompiler().compile(sourceFile); + + String generatedCode = loadGeneratedSource(compilation, "PrimitiveDtoBuilder"); + assertGenerationSucceeded(compilation, "PrimitiveDtoBuilder", generatedCode); + assertContaining( + generatedCode, + "TrackedValue count", + "TrackedValue active", + "TrackedValue value"); + } + + /** + * Tests that generic type variables (T, K, V) are handled correctly in builders. + * + *

This edge case verifies that type variables are properly preserved in the generated builder + * and that type variable imports are skipped (since they're not actual classes). + */ + @Test + void shouldHandleGenericTypeVariables() { + JavaFileObject sourceFile = + JavaFileObjects.forSourceString( + "test.GenericDto", + """ + package test; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + + @SimpleBuilder + public class GenericDto { + private T value; + public T getValue() { return value; } + public void setValue(T value) { this.value = value; } + } + """); + + Compilation compilation = ProcessorTestUtils.createCompiler().compile(sourceFile); + + String generatedCode = loadGeneratedSource(compilation, "GenericDtoBuilder"); + assertContaining( + generatedCode, + "public class GenericDtoBuilder", + "TrackedValue value", + "public GenericDto build()", + "public static GenericDtoBuilder create()"); + } +} From f40814b65b6f881d0380bef8dbdc34b67c5b4b64 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Thu, 19 Mar 2026 21:52:00 +0100 Subject: [PATCH 19/20] Adding test for already existing Builder --- .../RoasterCodeGeneratorEdgeCasesTest.java | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/RoasterCodeGeneratorEdgeCasesTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/RoasterCodeGeneratorEdgeCasesTest.java index bd658c1e..805028a5 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/RoasterCodeGeneratorEdgeCasesTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/RoasterCodeGeneratorEdgeCasesTest.java @@ -24,6 +24,7 @@ package org.javahelpers.simple.builders.processor; +import static com.google.testing.compile.CompilationSubject.assertThat; import static org.javahelpers.simple.builders.processor.testing.ProcessorAsserts.assertContaining; import static org.javahelpers.simple.builders.processor.testing.ProcessorAsserts.assertGenerationSucceeded; import static org.javahelpers.simple.builders.processor.testing.ProcessorAsserts.assertNotContaining; @@ -140,6 +141,51 @@ void shouldHandleBuilderWithNoNestedTypes() { assertNotContaining(generatedCode, "public interface With"); } + /** + * Tests that the code generator detects and reports when a builder class already exists. + * + *

This edge case verifies that if a builder class with the same name already exists (either + * manually written or from a previous compilation), the processor detects the conflict and issues + * a warning, allowing compilation to succeed gracefully. + */ + @Test + void shouldDetectExistingBuilderClass() { + JavaFileObject dto = + JavaFileObjects.forSourceString( + "test.PersonDto", + """ + package test; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + + @SimpleBuilder + public class PersonDto { + private String name; + public String getName() { return name; } + public void setName(String name) { this.name = name; } + } + """); + + // Create a builder class that already exists + JavaFileObject existingBuilder = + JavaFileObjects.forSourceString( + "test.PersonDtoBuilder", + """ + package test; + + public class PersonDtoBuilder { + // Manually written or previously generated builder + } + """); + + Compilation compilation = ProcessorTestUtils.createCompiler().compile(dto, existingBuilder); + + // Verify compilation succeeds with a warning about the existing builder + assertThat(compilation).succeeded(); + assertThat(compilation) + .hadWarningContaining( + "Failed to generate builder - Builder class 'test.PersonDtoBuilder' already exists"); + } + /** * Tests that fields without JavaDoc documentation are handled correctly. * From 15816d224ba544ad4948742c14a248a644b63172 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Thu, 19 Mar 2026 22:10:35 +0100 Subject: [PATCH 20/20] Removing javapoet --- processor/pom.xml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/processor/pom.xml b/processor/pom.xml index 9bf0d7e1..38788390 100644 --- a/processor/pom.xml +++ b/processor/pom.xml @@ -54,7 +54,6 @@ 1.1.1 - 0.12.0 2.31.0.Final 3.20.0 4.5.0 @@ -111,11 +110,6 @@ true - - com.palantir.javapoet - javapoet - ${javapoet.version} - org.jboss.forge.roaster roaster-api