diff --git a/DEBUG_LOGGING.md b/DEBUG_LOGGING.md new file mode 100644 index 00000000..2c3fe3c4 --- /dev/null +++ b/DEBUG_LOGGING.md @@ -0,0 +1,114 @@ +# Debug Logging for Simple Builders + +The Simple Builders annotation processor supports conditional debug logging that provides detailed information about the builder generation process. + +## Logging Levels + +- **INFO**: Always visible - Shows success messages for each builder generated +- **WARNING**: Always visible - Shows when a builder cannot be generated (e.g., wrong annotation target, generation errors). Other builders will continue to be generated. +- **ERROR**: Always visible - Shows fatal configuration errors (e.g., unsupported JDK version). Stops compilation completely. +- **DEBUG**: Conditional - Shows detailed tracing of field discovery, method analysis, and code generation steps + +## Enabling Debug Logging + +Debug logging uses `Diagnostic.Kind.OTHER` but is only activated when explicitly enabled via the compiler argument `-Averbose=true`. + +### Option 1: Via Maven Property (Recommended) + +Add a property in your `pom.xml`: + +```xml + + false + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + + -Averbose=${simplebuilder.verbose} + + + + + +``` + +Then enable debug logging from command line: +```bash +mvn clean compile -Dsimplebuilder.verbose=true +``` + +### Option 2: Static Configuration + +Set it permanently in your `pom.xml`: + +```xml + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + + -Averbose=true + + + + + +``` + +## Example Debug Output + +When debug logging is enabled, you'll see detailed output with visual separators: + +``` +[INFO] [DEBUG] =============================== +[INFO] simple-builders: PROCESSING ROUND START +[INFO] [DEBUG] =============================== +[INFO] [DEBUG] simple-builders: Processing round started. Found 3 annotated elements. +[INFO] [DEBUG] ------------------------------------ +[INFO] [DEBUG] simple-builders: Processing element: PersonDto +[INFO] [DEBUG] ------------------------------------ +[INFO] [DEBUG] Extracting builder definition from: org.example.PersonDto +[INFO] [DEBUG] Builder will be generated as: org.example.PersonDtoBuilder +[INFO] [DEBUG] Analyzing method: setName with 1 parameter(s) +[INFO] [DEBUG] -> Adding field: name (type: java.lang.String) +[INFO] [DEBUG] Analyzing method: setAge with 1 parameter(s) +[INFO] [DEBUG] -> Adding field: age (type: int) +[INFO] [DEBUG] Processed 2 possible setters: added 2 fields, skipped 0 +[INFO] [DEBUG] Starting code generation for builder: PersonDtoBuilder +[INFO] [DEBUG] Generating 0 constructor fields and 2 setter fields +[INFO] [DEBUG] Generated 2 methods for field: name +[INFO] [DEBUG] Generated 2 methods for field: age +[INFO] [DEBUG] Writing builder class to file: org.example.PersonDtoBuilder +[INFO] [DEBUG] Successfully generated builder: PersonDtoBuilder +[INFO] simple-builders: Successfully generated builder for: PersonDto +``` + +**Note**: Debug messages are prefixed with `[DEBUG]` and use `Diagnostic.Kind.OTHER` which appears as `[INFO]` in Maven output. + +## Normal Output (Without Debug) + +Without debug logging enabled, you only see the INFO-level messages: + +``` +[INFO] simple-builders: Successfully generated builder for: PersonDto +[INFO] simple-builders: Successfully generated builder for: OrderDto +[INFO] simple-builders: Successfully generated builder for: CustomerDto +``` + +## Troubleshooting + +If debug logging is not appearing: + +1. Verify the compiler plugin configuration includes the debug or verbose argument +2. Check that you're using Maven 3.6+ +3. Ensure the annotation processor is actually running (check for generated files in `target/generated-sources/annotations`) +4. Try with `-X` flag to see all Maven debug output and verify the compiler arguments are being passed diff --git a/README.md b/README.md index 6b74fc7e..cc5823a8 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,11 @@ - [Usage](#usage) - [Basic Usage](#basic-usage) - [Collections and Nested Objects](#collections-and-nested-objects) + - [Debug Logging](#debug-logging) - [Building from Source](#building-from-source) - [Contributing](#contributing) - [License](#license) +- [Links](#links) ## What is Simple Builders? @@ -169,6 +171,12 @@ Project project = ProjectBuilder.create() .build(); ``` +### Debug Logging + +Simple Builders supports detailed debug logging to trace the builder generation process. Enable it with the `-Averbose=true` compiler argument for detailed insights into field discovery, method analysis, and code generation. + +For complete documentation on enabling and using debug logging, see [DEBUG_LOGGING.md](DEBUG_LOGGING.md). + ## Building from Source 1. Clone the repository: @@ -210,7 +218,3 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file * [Downloads](https://github.com/java-helpers/simple-builders/releases) * [Issue tracker](https://github.com/java-helpers/simple-builders/issues) * [CI build](https://github.com/java-helpers/simple-builders/actions/) - -## Licensing - -SimpleBuilders is licensed under the MIT License; you may not use this project except in compliance with the License. You may obtain a copy of the License at http://www.opensource.org/licenses/mit-license.php. diff --git a/example/pom.xml b/example/pom.xml index 1aba9198..e7c618db 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -24,6 +24,9 @@ ${java.version} ${java.version} ${java.version} + + + false @@ -77,6 +80,9 @@ ${project.version} + + -Averbose=${simplebuilder.verbose} + diff --git a/example/src/main/java/org/javahelpers/simple/builders/example/MannschaftDto.java b/example/src/main/java/org/javahelpers/simple/builders/example/MannschaftDto.java index 93d170e5..185f3cc6 100644 --- a/example/src/main/java/org/javahelpers/simple/builders/example/MannschaftDto.java +++ b/example/src/main/java/org/javahelpers/simple/builders/example/MannschaftDto.java @@ -25,6 +25,9 @@ package org.javahelpers.simple.builders.example; import java.util.Set; +import java.util.function.Supplier; + +import org.javahelpers.simple.builders.core.annotations.IgnoreInBuilder; import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; @SimpleBuilder @@ -40,6 +43,11 @@ public void setName(String name) { this.name = name; } + @IgnoreInBuilder + public void setName(Supplier nameSupplier) { + this.name = nameSupplier.get(); + } + public Set getSponsoren() { return sponsoren; } diff --git a/example/src/main/java/org/javahelpers/simple/builders/example/PersonDto.java b/example/src/main/java/org/javahelpers/simple/builders/example/PersonDto.java index 16c4bd31..77faf11a 100644 --- a/example/src/main/java/org/javahelpers/simple/builders/example/PersonDto.java +++ b/example/src/main/java/org/javahelpers/simple/builders/example/PersonDto.java @@ -30,18 +30,18 @@ @SimpleBuilder public class PersonDto { - private String name; + private final String name; private int groesse; private List nickNames; private java.time.LocalDate birthdate; private MannschaftDto mannschaft; - public String getName() { - return name; + public PersonDto(String name) { + this.name = name; } - public void setName(String name) { - this.name = name; + public String getName() { + return name; } public int getGroesse() { diff --git a/example/src/main/java/org/javahelpers/simple/builders/example/goal/PersonDtoBuilder.java b/example/src/main/java/org/javahelpers/simple/builders/example/goal/PersonDtoBuilder.java index e339a500..4fa03f54 100644 --- a/example/src/main/java/org/javahelpers/simple/builders/example/goal/PersonDtoBuilder.java +++ b/example/src/main/java/org/javahelpers/simple/builders/example/goal/PersonDtoBuilder.java @@ -33,14 +33,13 @@ @Generated("org.javahelpers.simple.builders.processor") @BuilderImplementation(forClass = PersonDto.class) public class PersonDtoBuilder { - private final PersonDto instance = new PersonDto(); + private final PersonDto instance = new PersonDto("test"); public static PersonDtoBuilder create() { return new PersonDtoBuilder(); } public PersonDtoBuilder name(String name) { - instance.setName(name); return this; } 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 d1b38b24..50c63021 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 @@ -33,6 +33,7 @@ import javax.annotation.processing.Processor; import javax.annotation.processing.RoundEnvironment; import javax.annotation.processing.SupportedAnnotationTypes; +import javax.annotation.processing.SupportedOptions; import javax.lang.model.SourceVersion; import javax.lang.model.element.Element; import javax.lang.model.element.TypeElement; @@ -49,6 +50,7 @@ */ @AutoService(Processor.class) @SupportedAnnotationTypes("org.javahelpers.simple.builders.core.annotations.SimpleBuilder") +@SupportedOptions("verbose") public class BuilderProcessor extends AbstractProcessor { private ProcessingContext context; private JavaCodeGenerator codeGenerator; @@ -57,20 +59,17 @@ public class BuilderProcessor extends AbstractProcessor { @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); - ProcessingLogger logger = new ProcessingLogger(processingEnv.getMessager()); + ProcessingLogger logger = new ProcessingLogger(processingEnv); this.context = new ProcessingContext( processingEnv.getElementUtils(), processingEnv.getTypeUtils(), logger); - this.codeGenerator = new JavaCodeGenerator(processingEnv.getFiler()); - - // Enforce minimum Java version (17+) for the processor + this.codeGenerator = new JavaCodeGenerator(processingEnv.getFiler(), logger); SourceVersion current = processingEnv.getSourceVersion(); this.supportedJdk = isAtLeastJava17(current); if (!this.supportedJdk) { context.error( - "simple-builders requires Java 17 or higher for annotation processing. Detected: " - + current - + ". Please upgrade to JDK 17+ or disable the processor."); + "simple-builders requires Java 17 or higher for annotation processing. Detected: '%s'. Please upgrade to JDK 17+ or disable the processor.", + current); } } @@ -89,13 +88,30 @@ public boolean process(Set annotations, RoundEnvironment if (simpleBuilderAnnotation == null) { context.error( "Annotation org.javahelpers.simple.builders.core.annotations.SimpleBuilder is not on classpath. So nothing to do here."); - return false; } - for (Element annotatedElement : roundEnv.getElementsAnnotatedWith(simpleBuilderAnnotation)) { + + Set annotatedElements = + roundEnv.getElementsAnnotatedWith(simpleBuilderAnnotation); + context.debug("==============================="); + context.info("simple-builders: PROCESSING ROUND START"); + context.debug("==============================="); + context.debug( + "simple-builders: Processing round started. Found %d annotated elements.", + annotatedElements.size()); + + for (Element annotatedElement : annotatedElements) { try { + context.debug("------------------------------------"); + context.debug("simple-builders: Processing element: %s", annotatedElement.getSimpleName()); + context.debug("------------------------------------"); process(annotatedElement); + context.info( + "simple-builders: Successfully generated builder for: %s", + annotatedElement.getSimpleName()); } catch (BuilderException ex) { - context.error(annotatedElement, "Failed to process annotated element: " + ex.getMessage()); + // All builder generation failures are warnings to allow other builders to be generated + context.warning( + annotatedElement, "simple-builders: Failed to generate builder - %s", ex.getMessage()); } } return true; diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderDefinitionDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderDefinitionDto.java index f8d54a84..05d8f767 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderDefinitionDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderDefinitionDto.java @@ -103,6 +103,16 @@ public void addFieldInConstructor(FieldDto field) { fieldsInConstructor.add(field); } + /** Adds multiple field definitions to be provided via the DTO constructor. */ + public void addAllFieldsInConstructor(List fields) { + fieldsInConstructor.addAll(fields); + } + + /** Adds multiple field definitions to the builder. */ + public void addAllFields(List fields) { + this.fields.addAll(fields); + } + /** * Getting all definied fields in builder definition. * diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java index 6df46c67..d6b78afa 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java @@ -32,6 +32,7 @@ import static org.javahelpers.simple.builders.processor.util.JavaLangMapper.map2TypeName; import static org.javahelpers.simple.builders.processor.util.TypeNameAnalyser.*; +import java.util.LinkedList; import java.util.List; import java.util.Optional; import java.util.Set; @@ -39,7 +40,6 @@ import java.util.function.Supplier; import javax.lang.model.element.*; import javax.lang.model.type.TypeMirror; -import javax.lang.model.util.ElementFilter; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Strings; @@ -72,11 +72,11 @@ private BuilderDefinitionCreator() { } /** - * Extracting definition for a builder from annotated element. + * Extracts a BuilderDefinition from the annotated element. * - * @param annotatedElement annotated elment which is target of builder creation - * @param context processing context with element and type utilities - * @return definition of builder + * @param annotatedElement the annotated type element to extract the builder definition from + * @param context the processing context for annotations processing + * @return the builder definition * @throws BuilderException if validation or generation failed */ public static BuilderDefinitionDto extractFromElement( @@ -84,28 +84,73 @@ public static BuilderDefinitionDto extractFromElement( validateAnnotatedElement(annotatedElement); TypeElement annotatedType = (TypeElement) annotatedElement; + context.debug("Extracting builder definition from: %s", annotatedType.getQualifiedName()); + + BuilderDefinitionDto result = initializeBuilderDefinition(annotatedType, context); + + List constructorFields = extractConstructorFields(annotatedType, context); + result.addAllFieldsInConstructor(constructorFields); + + List setterFields = extractSetterFields(annotatedType, result, context); + result.addAllFields(setterFields); + + return result; + } + + /** Initializes the builder definition with package, class name, and generics. */ + private static BuilderDefinitionDto initializeBuilderDefinition( + TypeElement annotatedType, ProcessingContext context) { BuilderDefinitionDto result = new BuilderDefinitionDto(); String packageName = context.getPackageName(annotatedType); String simpleClassName = annotatedType.getSimpleName().toString(); result.setBuilderTypeName(new TypeName(packageName, simpleClassName + BUILDER_SUFFIX)); result.setBuildingTargetTypeName(new TypeName(packageName, simpleClassName)); + context.debug( + "Builder will be generated as: %s.%s", packageName, simpleClassName + BUILDER_SUFFIX); + // Extract generics from the annotated type via mapper (stream-based) JavaLangMapper.map2GenericParameterDtos(annotatedType, context).forEach(result::addGeneric); - // Extract constructors and their parameters (treat as constructor fields) + return result; + } + + /** + * Extracts fields from the constructor parameters. + * + * @return list of fields extracted from constructor parameters + */ + private static List extractConstructorFields( + TypeElement annotatedType, ProcessingContext context) { + List constructorFields = new LinkedList<>(); Optional constructorOpt = findConstructorForBuilder(annotatedType, context); if (constructorOpt.isPresent()) { ExecutableElement ctor = constructorOpt.get(); + context.debug( + "Analyzing constructor: %s with %d parameter(s)", + ctor.getSimpleName(), ctor.getParameters().size()); for (VariableElement param : ctor.getParameters()) { Optional fieldFromCtor = createFieldFromConstructor(annotatedType, param, context); - fieldFromCtor.ifPresent(result::addFieldInConstructor); + if (fieldFromCtor.isPresent()) { + FieldDto field = fieldFromCtor.get(); + logFieldAddition(field, context); + constructorFields.add(field); + } } } + return constructorFields; + } - List allMembers = context.getAllMembers(annotatedType); - List methods = ElementFilter.methodsIn(allMembers); + /** + * Extracts fields from setter methods, avoiding duplicates from constructor fields. + * + * @param result the builder definition containing constructor fields to check for duplicates + * @return list of fields extracted from setter methods + */ + private static List extractSetterFields( + TypeElement annotatedType, BuilderDefinitionDto result, ProcessingContext context) { + List setterFields = new LinkedList<>(); // Build a set of constructor field names to avoid duplicates from setters Set ctorFieldNames = @@ -113,31 +158,74 @@ public static BuilderDefinitionDto extractFromElement( .map(FieldDto::getFieldName) .collect(toSet()); + List methods = findAllPossibleSettersOfClass(annotatedType, context); + int processedCount = 0; + int addedCount = 0; + int skippedCount = 0; + for (ExecutableElement mth : methods) { - // nur public - if (isMethodRelevantForBuilder(mth)) { - // Avoid adding setter-derived fields that duplicate constructor params + context.debug( + "Analyzing method: %s with %d parameter(s)", + mth.getSimpleName(), mth.getParameters().size()); + + if (isMethodRelevantForBuilder(mth, context)) { Optional maybeField = createFieldFromSetter(mth, context); if (maybeField.isPresent()) { + processedCount++; FieldDto field = maybeField.get(); if (!ctorFieldNames.contains(field.getFieldName())) { - result.addField(field); + addedCount++; + logFieldAddition(field, context); + setterFields.add(field); + continue; } + } else { + context.debug(" -> Skipping method (already in constructor): %s", mth.getSimpleName()); } } + skippedCount++; } - return result; + context.debug( + "Processed %d possible setters: added %d fields, skipped %d", + processedCount, addedCount, skippedCount); + + return setterFields; } - private static boolean isMethodRelevantForBuilder(ExecutableElement mth) { - return isSetterForField(mth) - && isNoMethodOfObjectClass(mth) - && hasNoThrowablesDeclared(mth) - && hasNoReturnValue(mth) - && hasNotAnnotation(IgnoreInBuilder.class, mth) - && isNotPrivate(mth) - && isNotStatic(mth); + /** Logs the addition of a field with its type information. */ + private static void logFieldAddition(FieldDto field, ProcessingContext context) { + String fieldTypeName = field.getFieldType().getClassName(); + if (field.getFieldType().getPackageName() != null + && !field.getFieldType().getPackageName().isEmpty()) { + fieldTypeName = field.getFieldType().getPackageName() + "." + fieldTypeName; + } + context.debug(" -> Adding field: %s (type: %s)", field.getFieldName(), fieldTypeName); + } + + private static boolean isMethodRelevantForBuilder( + ExecutableElement mth, ProcessingContext context) { + if (!hasNoThrowablesDeclared(mth)) { + context.debug(" -> Skipping: declares throwables"); + return false; + } + if (!hasNoReturnValue(mth)) { + context.debug(" -> Skipping: has return value"); + return false; + } + if (!hasNotAnnotation(IgnoreInBuilder.class, mth)) { + context.debug(" -> Skipping: has @IgnoreInBuilder annotation"); + return false; + } + if (!isNotPrivate(mth)) { + context.debug(" -> Skipping: is private"); + return false; + } + if (!isNotStatic(mth)) { + context.debug(" -> Skipping: is static"); + return false; + } + return true; } private static void addAdditionalHelperMethodsForField( @@ -239,43 +327,12 @@ private static Optional createFieldFromSetter( String methodName = mth.getSimpleName().toString(); String fieldName = StringUtils.uncapitalize(Strings.CI.removeStart(methodName, "set")); - FieldDto result = new FieldDto(); - result.setFieldName(fieldName); List parameters = mth.getParameters(); if (parameters.size() != 1) { // Should never happen, just to be sure here - context.warning( - mth.getEnclosingElement(), - "Setter method " + methodName + " has " + parameters.size() + " parameters, expected 1"); + context.warning(mth, "Unexpected state of method."); return Optional.empty(); } - VariableElement fieldParameter = parameters.get(0); - TypeMirror fieldTypeMirror = fieldParameter.asType(); - Element rawElement = context.asElement(fieldTypeMirror); - TypeElement fieldTypeElement = rawElement instanceof TypeElement te ? te : null; - - // Extract only the @param Javadoc for the single setter parameter (if present) - String fullJavaDoc = context.getDocComment(mth); - String parameterJavaDocExtracted = - JavaLangAnalyser.extractParamJavaDoc(fullJavaDoc, fieldParameter); - String parameterJavaDoc = - parameterJavaDocExtracted == null ? fieldName : parameterJavaDocExtracted; - result.setJavaDoc(parameterJavaDoc); - - // extracting type of field - MethodParameterDto fieldParameterDto = map2MethodParameter(fieldParameter, context); - if (fieldParameterDto == null) { - context.warning(mth.getEnclosingElement(), "Could not extract type of field " + fieldName); - return Optional.empty(); - } - TypeName fieldType = fieldParameterDto.getParameterType(); - result.setFieldType(fieldType); - - // Determine if a corresponding getter exists on the DTO and record its name - TypeElement dtoType = (TypeElement) mth.getEnclosingElement(); - JavaLangAnalyser.findGetterForField(dtoType, fieldName, fieldTypeMirror, context) - .ifPresent( - getterExecutable -> result.setGetterName(getterExecutable.getSimpleName().toString())); // Finding generics declared on the setter itself (field-specific), e.g., @@ -284,20 +341,22 @@ private static Optional createFieldFromSetter( if (CollectionUtils.isNotEmpty(mth.getTypeParameters())) { context.warning( mth.getEnclosingElement(), - "Field " + fieldName + " has field-specific generics, so it will be ignored"); + "Field '%s' has field-specific generics, so it will be ignored", + fieldName); return Optional.empty(); } - // simple setter - result.addMethod(createFieldSetterWithTransform(fieldName, null, fieldType)); + VariableElement fieldParameter = parameters.get(0); + TypeElement dtoType = (TypeElement) mth.getEnclosingElement(); - // add consumer/supplier generation via helpers - addConsumerMethodsForField( - result, fieldName, fieldType, fieldParameter, fieldTypeElement, context); - addSupplierMethodsForField(result, fieldName, fieldType, fieldTypeElement); - addAdditionalHelperMethodsForField(result, fieldName, fieldType); + // Extract only the @param Javadoc for the single setter parameter (if present) + String fullJavaDoc = context.getDocComment(mth); + String javaDoc = JavaLangAnalyser.extractParamJavaDoc(fullJavaDoc, fieldParameter); + if (javaDoc == null) { + javaDoc = fieldName; + } - return Optional.of(result); + return createFieldDto(fieldName, javaDoc, fieldParameter, dtoType, context); } /** @@ -306,23 +365,60 @@ private static Optional createFieldFromSetter( */ private static Optional createFieldFromConstructor( TypeElement dtoType, VariableElement param, ProcessingContext context) { + String fieldName = param.getSimpleName().toString(); + // Set javadoc (default to field name if no javadoc found) + String javaDoc = JavaLangAnalyser.extractParamJavaDoc(context.getDocComment(dtoType), param); + if (javaDoc == null) { + javaDoc = fieldName; + } + return createFieldDto(fieldName, javaDoc, param, dtoType, context); + } + + /** + * Common method to create a FieldDto with all builder methods (setter, supplier, consumer, + * helpers). + * + * @param fieldName the name of the field + * @param javaDoc the javadoc for the field + * @param param the parameter element (from constructor or setter) + * @param dtoType the DTO type containing this field + * @param context processing context + * @return Optional containing the FieldDto, or empty if field cannot be created + */ + private static Optional createFieldDto( + String fieldName, + String javaDoc, + VariableElement param, + TypeElement dtoType, + ProcessingContext context) { MethodParameterDto paramDto = map2MethodParameter(param, context); if (paramDto == null) { return Optional.empty(); } - String fieldName = param.getSimpleName().toString(); + TypeName fieldType = paramDto.getParameterType(); - FieldDto ctorField = new FieldDto(); - ctorField.setFieldName(fieldName); - ctorField.setFieldType(fieldType); - // Set javadoc (default to field name if no javadoc found) - ctorField.setJavaDoc(fieldName); + TypeMirror fieldTypeMirror = param.asType(); + Element rawElement = context.asElement(fieldTypeMirror); + TypeElement fieldTypeElement = rawElement instanceof TypeElement te ? te : null; + + FieldDto field = new FieldDto(); + field.setFieldName(fieldName); + field.setFieldType(fieldType); + field.setJavaDoc(javaDoc); + // Find matching getter on the DTO type - JavaLangAnalyser.findGetterForField(dtoType, fieldName, param.asType(), context) - .ifPresent(getter -> ctorField.setGetterName(getter.getSimpleName().toString())); - // Provide a simple setter in builder to set the constructor argument - ctorField.addMethod(createFieldSetterWithTransform(fieldName, null, fieldType)); - return Optional.of(ctorField); + JavaLangAnalyser.findGetterForField(dtoType, fieldName, fieldTypeMirror, context) + .ifPresent(getter -> field.setGetterName(getter.getSimpleName().toString())); + + // Add basic setter method + field.addMethod(createFieldSetterWithTransform(fieldName, null, fieldType)); + + // Add consumer/supplier/helper methods + addConsumerMethodsForField(field, fieldName, fieldType, param, fieldTypeElement, context); + addSupplierMethodsForField(field, fieldName, fieldType, fieldTypeElement); + addAdditionalHelperMethodsForField(field, fieldName, fieldType); + + return Optional.of(field); } private static MethodDto createFieldSetterWithTransform( diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java index 59620ea4..6ae80bbe 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java @@ -52,24 +52,31 @@ public class JavaCodeGenerator { /** Util class for source code generation of type {@code javax.annotation.processing.Filer}. */ private final Filer filer; + /** Logger for debug output during code generation. */ + private final ProcessingLogger logger; + /** * Constructor for JavaCodeGenerator. * * @param filer Util class for source code generation of type {@code * javax.annotation.processing.Filer} + * @param logger Logger for debug output */ - public JavaCodeGenerator(Filer filer) { + public JavaCodeGenerator(Filer filer, ProcessingLogger logger) { this.filer = filer; + this.logger = logger; } /** * Generating source code file for builder. Creation of source code files is done by {@code - * javax.annotation.processing.Filer}. * * @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.debug( + "Starting code generation for builder: %s", builderDef.getBuilderTypeName().getClassName()); + ClassName builderBaseClass = map2ClassName(builderDef.getBuilderTypeName()); ClassName dtoBaseClass = map2ClassName(builderDef.getBuildingTargetTypeName()); com.palantir.javapoet.TypeName builderTypeName; @@ -83,6 +90,7 @@ public void generateBuilder(BuilderDefinitionDto builderDef) throws BuilderExcep dtoTypeName = map2ParameterizedTypeName( builderDef.getBuildingTargetTypeName(), builderDef.getGenerics()); + logger.debug("Builder has %d generic type parameter(s)", builderDef.getGenerics().size()); } TypeSpec.Builder classBuilder = @@ -97,6 +105,11 @@ public void generateBuilder(BuilderDefinitionDto builderDef) throws BuilderExcep dtoBaseClass, dtoTypeName, builderDef.getAllFieldsForBuilder())); classBuilder.addMethod(createEmptyConstructor(dtoBaseClass)); + logger.debug( + "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) for (FieldDto fieldDto : builderDef.getConstructorFieldsForBuilder()) { FieldSpec fieldSpec = createFieldMember(fieldDto); @@ -110,10 +123,14 @@ public void generateBuilder(BuilderDefinitionDto builderDef) throws BuilderExcep // Generate field-specific functions in Builder (constructor fields first, then setter fields) for (FieldDto fieldDto : builderDef.getConstructorFieldsForBuilder()) { List methodSpecs = createFieldMethods(fieldDto, builderTypeName); + logger.debug( + " Generated %d methods for field: %s", methodSpecs.size(), fieldDto.getFieldName()); classBuilder.addMethods(methodSpecs); } for (FieldDto fieldDto : builderDef.getSetterFieldsForBuilder()) { List methodSpecs = createFieldMethods(fieldDto, builderTypeName); + logger.debug( + " Generated %d methods for field: %s", methodSpecs.size(), fieldDto.getFieldName()); classBuilder.addMethods(methodSpecs); } @@ -133,7 +150,13 @@ public void generateBuilder(BuilderDefinitionDto builderDef) throws BuilderExcep classBuilder.addAnnotation(createAnnotationGenerated()); classBuilder.addAnnotation(createAnnotationBuilderImplementation(dtoBaseClass)); + logger.debug( + "Writing builder class to file: %s.%s", + builderDef.getBuilderTypeName().getPackageName(), + builderDef.getBuilderTypeName().getClassName()); writeClassToFile(builderDef.getBuilderTypeName().getPackageName(), classBuilder.build()); + logger.debug( + "Successfully generated builder: %s", builderDef.getBuilderTypeName().getClassName()); } private void writeClassToFile(String packageName, TypeSpec typeSpec) throws BuilderException { diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaLangAnalyser.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaLangAnalyser.java index 7e6259b5..c198ec2f 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaLangAnalyser.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaLangAnalyser.java @@ -61,6 +61,22 @@ public static boolean isNoMethodOfObjectClass(ExecutableElement mth) { || Strings.CS.equals("Object", simpleNameOfParent)); } + /** + * Gets all methods of a class, including inherited methods, excluding methods from {@code + * java.lang.Object}. + * + * @param context the processing context + * @param typeElement the type element to get methods from + * @return filtered list excluding Object class methods + */ + public static List findAllPossibleSettersOfClass( + TypeElement typeElement, ProcessingContext context) { + return ElementFilter.methodsIn(context.getAllMembers(typeElement)).stream() + .filter(JavaLangAnalyser::isNoMethodOfObjectClass) + .filter(JavaLangAnalyser::isSetterForField) + .toList(); + } + /** * Helper function to filter methods with specific annotations. * diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ProcessingContext.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ProcessingContext.java index 6fe18222..7a348d07 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ProcessingContext.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ProcessingContext.java @@ -122,40 +122,61 @@ public boolean isSameType(TypeMirror type1, TypeMirror type2) { } /** - * Reports an error at the location of the given element. + * Logs an info-level message that appears in normal Maven output. * - * @param element the element where the error occurred - * @param message the error message + * @param message the info message to log */ - public void error(Element element, String message) { - logger.error(element, message); + public void info(String message) { + logger.info(message); } /** - * Reports an error message. + * Logs an info-level message with a formatted string. * - * @param message the error message + * @param format the format string + * @param args arguments referenced by the format specifiers in the format string */ - public void error(String message) { - logger.error(message); + public void info(String format, Object... args) { + logger.info(format, args); } /** - * Logs an informational message. + * Logs a debug message visible when Maven is run with -X flag. * - * @param message the message to log + * @param message the debug message to log */ - public void log(String message) { - logger.log(message); + public void debug(String message) { + logger.debug(message); } /** - * Reports a warning at the location of the given element with the specified message. + * Logs a debug message with a formatted string. + * + * @param format the format string + * @param args arguments referenced by the format specifiers in the format string + */ + public void debug(String format, Object... args) { + logger.debug(format, args); + } + + /** + * Reports a warning at the location of the given element with a formatted message. * * @param element the element where the warning occurred, used for location information - * @param message the warning message to be reported + * @param format the format string + * @param args arguments referenced by the format specifiers in the format string + */ + public void warning(Element element, String format, Object... args) { + logger.warning(element, format, args); + } + + /** + * Reports an error with a formatted message. + * + * @param format the format string + * @param args arguments referenced by the format specifiers in the format string */ - public void warning(Element element, String message) { - logger.warning(element, message); + public void error(String format, Object... args) { + logger.error(format, args); } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ProcessingLogger.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ProcessingLogger.java index 65d8c417..613d3243 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ProcessingLogger.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ProcessingLogger.java @@ -25,6 +25,7 @@ package org.javahelpers.simple.builders.processor.util; import javax.annotation.processing.Messager; +import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.Element; import javax.tools.Diagnostic; @@ -37,55 +38,85 @@ public class ProcessingLogger { /** Util class for exposing messages of type {@code javax.annotation.processing.Messager}. */ private final Messager messager; + /** Flag indicating if debug logging is enabled. */ + private final boolean debugEnabled; + /** - * Constructs a new ProcessingLogger with the specified Messager instance. The Messager is used to - * report errors, warnings, and other notices during annotation processing. + * Constructs a new ProcessingLogger with the specified ProcessingEnvironment. The Messager is + * used to report errors, warnings, and other notices during annotation processing. Debug logging + * is enabled by setting the compiler argument: -Averbose=true * - * @param messager the Messager instance to use for reporting messages, must not be null + * @param processingEnv the processing environment providing messager and options */ - public ProcessingLogger(Messager messager) { - this.messager = messager; + public ProcessingLogger(ProcessingEnvironment processingEnv) { + this.messager = processingEnv.getMessager(); + this.debugEnabled = "true".equalsIgnoreCase(processingEnv.getOptions().get("verbose")); } /** - * Reports an error at the location of the given element with the specified message. The error - * will be reported to the underlying Messager instance. + * Reports an error with a formatted message. * - * @param e the element where the error occurred, used for location information - * @param message the error message to be reported + * @param format the format string + * @param args arguments referenced by the format specifiers in the format string */ - public void error(Element e, String message) { - messager.printMessage(Diagnostic.Kind.ERROR, message, e); + public void error(String format, Object... args) { + messager.printMessage(Diagnostic.Kind.ERROR, String.format(format, args)); } /** - * Reports an error with the specified message. The error will be reported to the underlying - * Messager instance. + * Posts an info-level message (NOTE level in Maven). Used for important status information about + * builder generation. * - * @param message the error message to be reported + * @param message the info message to be posted */ - public void error(String message) { - messager.printMessage(Diagnostic.Kind.ERROR, message); + public void info(String message) { + messager.printMessage(Diagnostic.Kind.NOTE, message); } /** - * Posts an informational note message to the underlying Messager. This is typically used for - * non-critical information during processing. + * Posts an info-level message with a formatted string. * - * @param message the informational message to be posted + * @param format the format string + * @param args arguments referenced by the format specifiers in the format string */ - public void log(String message) { - messager.printMessage(Diagnostic.Kind.NOTE, message); + public void info(String format, Object... args) { + messager.printMessage(Diagnostic.Kind.NOTE, String.format(format, args)); + } + + /** + * Posts a debug message with OTHER level. Used for detailed tracing of the builder generation + * process. Only visible when enabled via -Averbose=true compiler argument. + * + * @param message the debug message to be posted + */ + public void debug(String message) { + if (debugEnabled) { + String formatWithDebug = "[DEBUG] " + message; + messager.printMessage(Diagnostic.Kind.OTHER, formatWithDebug); + } + } + + /** + * Posts a debug message with a formatted string. Only visible when enabled via -Averbose=true. + * + * @param format the format string + * @param args arguments referenced by the format specifiers in the format string + */ + public void debug(String format, Object... args) { + if (debugEnabled) { + String formatWithDebug = "[DEBUG] " + format; + messager.printMessage(Diagnostic.Kind.OTHER, String.format(formatWithDebug, args)); + } } /** - * Reports a warning at the location of the given element with the specified message. The warning - * will be reported to the underlying Messager instance. + * Reports a warning at the location of the given element with a formatted message. * * @param e the element where the warning occurred, used for location information - * @param message the warning message to be reported + * @param format the format string + * @param args arguments referenced by the format specifiers in the format string */ - public void warning(Element e, String message) { - messager.printMessage(Diagnostic.Kind.WARNING, message, e); + public void warning(Element e, String format, Object... args) { + messager.printMessage(Diagnostic.Kind.WARNING, String.format(format, args), e); } } diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java index 836776af..ec4673b2 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java @@ -18,6 +18,65 @@ /** Tests for the {@link BuilderProcessor} class. */ class BuilderProcessorTest { + @Test + void shouldGenerateBasicBuilder() { + // Given: A simple class with one field + String className = "SimpleDto"; + String builderClassName = className + "Builder"; + + JavaFileObject sourceFile = + ProcessorTestUtils.simpleBuilderClass( + "test", + className, + """ + private String value; + public String getValue() { return value; } + public void setValue(String value) { this.value = value; } + """); + + // When + Compilation compilation = compile(sourceFile); + + // Then: Builder should be generated successfully + String generatedCode = loadGeneratedSource(compilation, builderClassName); + assertGenerationSucceeded(compilation, builderClassName, generatedCode); + ProcessorAsserts.assertContaining( + generatedCode, "public SimpleDtoBuilder value(String value)", "public SimpleDto build()"); + } + + @Test + void shouldLogDebugMessagesWhenVerboseModeEnabled() { + // Given + JavaFileObject sourceFile = + ProcessorTestUtils.simpleBuilderClass( + "test", + "VerboseTest", + """ + private String name; + public String getName() { return name; } + public void setName(String name) { this.name = name; } + """); + + // When: Compile with verbose=true to enable debug logging + Compilation compilation = + javac() + .withProcessors(new BuilderProcessor()) + .withOptions("-Averbose=true") + .compile(sourceFile); + + // Then: Compilation succeeds and debug messages are present + assertThat(compilation).succeeded(); + + // Verify key debug messages are logged + ProcessorAsserts.assertHadNoteContaining( + compilation, + "simple-builders: PROCESSING ROUND START", + "[DEBUG] simple-builders: Processing element: VerboseTest", + "[DEBUG] Extracting builder definition from: test.VerboseTest", + "[DEBUG] -> Adding field: name", + "[DEBUG] Successfully generated builder: VerboseTestBuilder"); + } + @Test void shouldFailWhenAnnotationPlacedOnInterface() { // Given: @SimpleBuilder used on an interface instead of a class @@ -33,10 +92,10 @@ public interface WrongTargetInterface { void x(); } // When Compilation compilation = compile(source); - // Then: compilation should fail with appropriate error message - assertThat(compilation).failed(); + // Then: compilation should succeed but with a warning + assertThat(compilation).succeeded(); assertThat(compilation) - .hadErrorContaining("The SimpleBuilder should be annotated on a class or record"); + .hadWarningContaining("The SimpleBuilder should be annotated on a class or record"); } @Test @@ -54,9 +113,9 @@ public abstract class AbstractAnnotated { } // When Compilation compilation = compile(source); - // Then: compilation should fail with appropriate error message - assertThat(compilation).failed(); - assertThat(compilation).hadErrorContaining("The SimpleBuilder should not be abstract"); + // Then: compilation should succeed but with a warning + assertThat(compilation).succeeded(); + assertThat(compilation).hadWarningContaining("The SimpleBuilder should not be abstract"); } @Test @@ -138,7 +197,9 @@ void shouldHandleFieldsOfConstructorAndSetters() { generatedCode, "public CtorAndSetterBuilder()", // empty constructor for builder "public CtorAndSetterBuilder name(String name)", // helpermethod for setter + "public CtorAndSetterBuilder name(Supplier nameSupplier)", // supplier for setter "public CtorAndSetterBuilder a(int a)", // helpermethod for constructor param + "public CtorAndSetterBuilder a(Supplier aSupplier)", // supplier for constructor "CtorAndSetter result = new CtorAndSetter(this.a.value());", "this.name.ifChanged(result::setName);"); } @@ -291,6 +352,110 @@ public void notAConstructor(String name) { generatedCode, "public WrongMethodAnnotationBuilder name(String name)"); } + @Test + void shouldGenerateSupplierMethodsForConstructorFields() { + // Given + String packageName = "test"; + String className = "RequiredFieldViaConstructor"; + String builderClassName = className + "Builder"; + + JavaFileObject sourceFile = + ProcessorTestUtils.simpleBuilderClass( + packageName, + className, + """ + private final String name; + private final int age; + private String description; + + public RequiredFieldViaConstructor(String name, int age) { + this.name = name; + this.age = age; + } + + public String getName() { return name; } + public int getAge() { return age; } + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + """); + + // When + Compilation compilation = compile(sourceFile); + + // Then + String generatedCode = loadGeneratedSource(compilation, builderClassName); + assertGenerationSucceeded(compilation, builderClassName, generatedCode); + + // Constructor fields should have both basic setter AND supplier methods + ProcessorAsserts.assertContaining( + generatedCode, + // Basic setters for constructor fields + "public RequiredFieldViaConstructorBuilder name(String name)", + "public RequiredFieldViaConstructorBuilder age(int age)", + // Supplier methods for constructor fields (the bug we fixed!) + "public RequiredFieldViaConstructorBuilder name(Supplier nameSupplier)", + "public RequiredFieldViaConstructorBuilder age(Supplier ageSupplier)", + // Setter field should also have supplier + "public RequiredFieldViaConstructorBuilder description(String description)", + "public RequiredFieldViaConstructorBuilder description(Supplier descriptionSupplier)"); + } + + @Test + void shouldGenerateConsumerMethodsForConstructorFieldsWithCustomTypes() { + // Given + String packageName = "test"; + String className = "RequiredCustomTypeViaConstructor"; + String builderClassName = className + "Builder"; + + JavaFileObject dto = + ProcessorTestUtils.simpleBuilderClass( + packageName, + className, + """ + private final Address address; + private String notes; + + public RequiredCustomTypeViaConstructor(Address address) { + this.address = address; + } + + public Address getAddress() { return address; } + public String getNotes() { return notes; } + public void setNotes(String notes) { this.notes = notes; } + """); + + JavaFileObject helper = + ProcessorTestUtils.forSource( + """ + package test; + public class Address { + private String street; + public Address() {} + public String getStreet() { return street; } + public void setStreet(String street) { this.street = street; } + } + """); + + // When + Compilation compilation = compile(dto, helper); + + // Then + String generatedCode = loadGeneratedSource(compilation, builderClassName); + assertGenerationSucceeded(compilation, builderClassName, generatedCode); + + // Constructor field with custom type should have setter, supplier AND consumer methods + ProcessorAsserts.assertContaining( + generatedCode, + // Basic setter for constructor field + "public RequiredCustomTypeViaConstructorBuilder address(Address address)", + // Supplier method for constructor field + "public RequiredCustomTypeViaConstructorBuilder address(Supplier
addressSupplier)", + // Consumer method for constructor field (the key coverage addition!) + "public RequiredCustomTypeViaConstructorBuilder address(Consumer
addressConsumer)", + "Address consumer = new Address();", + "addressConsumer.accept(consumer);"); + } + @Test void shouldHandleRecordWithoutGeneratingBuilder() { // Given @@ -1237,6 +1402,7 @@ public void setOk(int ok) {} // should be filtered public void nonSetterMethods() {} public void settingChanged(String nonSetterValue) {} + public int setSetterWithReturnValue(int returnValue) { return returnValue; } private void setHidden(int hidden) {} public static void setUtil(int util) {} public void setRisky(int risk) throws Exception {} @@ -1257,6 +1423,7 @@ public void setRisky(int risk) throws Exception {} contains("this.ok = changedValue(ok);"), notContains("nonSetterMethods"), notContains("settingChanged"), + notContains("setterWithReturnValue"), notContains("hidden"), notContains("util(int util)"), notContains("risky(int risk)"), @@ -2183,10 +2350,10 @@ public void setFieldWithOwnGeneric(F fieldWithOwnGeneri assertThat(compilation).succeeded(); assertThat(compilation) .hadWarningContaining( - "Field fieldWithOwnGeneric has field-specific generics, so it will be ignored"); + "Field 'fieldWithOwnGeneric' has field-specific generics, so it will be ignored"); assertThat(compilation) .hadWarningContaining( - "Field fieldWithExtendedClassGeneric has field-specific generics, so it will be ignored"); + "Field 'fieldWithExtendedClassGeneric' has field-specific generics, so it will be ignored"); ProcessorAsserts.assertingResult( generatedCode, // name field should be present @@ -2223,10 +2390,10 @@ public class InnerClass { // When Compilation compilation = compile(source); - // Then: compilation should fail with appropriate error message - assertThat(compilation).failed(); + // Then: compilation should succeed but with a warning + assertThat(compilation).succeeded(); assertThat(compilation) - .hadErrorContaining("The SimpleBuilder should be declared on a top-level class only"); + .hadWarningContaining("The SimpleBuilder should be declared on a top-level class only"); } @Test diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/testing/ProcessorAsserts.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/testing/ProcessorAsserts.java index 8d721199..ec219347 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/testing/ProcessorAsserts.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/testing/ProcessorAsserts.java @@ -109,4 +109,17 @@ public static void assertContaining(String generatedCode, String... searches) { .toArray(AssertRecord[]::new); assertingResult(generatedCode, checks); } + + /** + * Asserts that the compilation contains all the specified note messages. Useful for verifying + * debug or info logging output. + * + * @param compilation the compilation result + * @param noteMessages the note messages to check for + */ + public static void assertHadNoteContaining(Compilation compilation, String... noteMessages) { + for (String noteMessage : noteMessages) { + assertThat(compilation).hadNoteContaining(noteMessage); + } + } }