diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 68ff57e7..a3034987 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,6 +4,8 @@ updates: directories: - "/core" - "/processor" + - "/example-custom-generator" + - "/example" schedule: interval: "weekly" labels: diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 45cc9122..77ff56e0 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -230,11 +230,19 @@ For complete documentation, see [DEBUG_LOGGING.md](DEBUG_LOGGING.md). ``` ========== Compilation Diagnostics ========== --- NOTES --- -[DEBUG] simple-builders: Processing element: Project -[DEBUG] Extracting builder definition from: test.Project -[DEBUG] Analyzing method: setName with 1 parameter(s) -[DEBUG] -> Adding field: name (type: java.lang.String) -[DEBUG] Generated 4 methods for field: name +[DEBUG] simple-builders: Processing round started. Found 1 annotated elements. +[DEBUG] Processing element: Project +[DEBUG] ├─ Extracting builder definition from: test.Project +[DEBUG] │ ├─ Builder will be generated as: test.ProjectBuilder +[DEBUG] │ ├─ Analysing setters for finding fields +[DEBUG] │ │ ├─ Analyzing method: setName with 1 parameter(s) +[DEBUG] │ │ │ └─ Adding field: name (type: java.lang.String) +[DEBUG] │ └─ Processed 1 possible setters: added 1 fields, skipped 0 +[DEBUG] ├─ Code generation for builder: ProjectBuilder +[DEBUG] │ ├─ Adding Methods for 4 candidates +[DEBUG] │ │ └─ 4 Methods added +[DEBUG] │ └─ Successfully generated builder: ProjectBuilder +simple-builders: Successfully generated 1 builder(s) in this processing round ============================================= ========== Generated Source Files ========== diff --git a/docs/DEBUG_LOGGING.md b/docs/DEBUG_LOGGING.md index 2c3fe3c4..a236e123 100644 --- a/docs/DEBUG_LOGGING.md +++ b/docs/DEBUG_LOGGING.md @@ -4,10 +4,10 @@ The Simple Builders annotation processor supports conditional debug logging that ## 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. +- **INFO**: Always visible - Shows success messages for each builder generated. When debug mode is enabled, INFO messages use hierarchical indentation. +- **WARNING**: Always visible - Shows when a builder cannot be generated (e.g., wrong annotation target, generation errors). When debug mode is enabled, WARNING messages use hierarchical indentation. - **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 +- **DEBUG**: Conditional - Shows detailed tracing of field discovery, method analysis, and code generation steps with hierarchical indentation ## Enabling Debug Logging @@ -64,32 +64,63 @@ Set it permanently in your `pom.xml`: ``` +## Conditional Hierarchical Logging + +The Simple Builders processor provides **conditional hierarchical logging** to balance readability in production with detailed debugging when needed: + +### **When Debug Mode is DISABLED (Default):** +- INFO and WARNING messages appear flat without indentation +- Suitable for production systems where log noise should be minimized +- Example: `simple-builders: Successfully generated 3 builder(s) in this processing round` + +### **When Debug Mode is ENABLED (-Averbose=true):** +- INFO and WARNING messages have no extra prefix, DEBUG messages are posted on `[INFO]` chanel so they have the prefix `[INFO] [DEBUG]` +- Provides full visibility into the processing hierarchy +- Example: `[INFO] │ └─ simple-builders: Successfully generated 3 builder(s) in this processing round` +- Example: `[WARNING] │ │ ├─ Builder field conflict: field 'name'...` +- Example: `[INFO] [DEBUG] │ │ │ └─ Processing method: setName` +- All message types align vertically despite different prefix lengths + +This approach ensures clean production logs while maintaining full debugging capabilities when needed. + ## 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 +[INFO] [DEBUG] Processing element: PersonDto +[INFO] [DEBUG] ├─ Extracting builder definition from: org.example.PersonDto +[INFO] [DEBUG] │ ├─ Builder will be generated as: org.example.PersonDtoBuilder +[INFO] [DEBUG] │ ├─ Analysing setters for finding fields +[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] ├─ Code generation for builder: PersonDtoBuilder +[INFO] [DEBUG] │ ├─ Class builder created +[INFO] [DEBUG] │ ├─ Generating 0 constructor fields and 2 setter fields +[INFO] [DEBUG] │ │ └─ Fields added: 2 fields +[INFO] [DEBUG] │ ├─ Adding Methods for 4 candidates +[INFO] [DEBUG] │ │ └─ 4 Methods added +[INFO] [DEBUG] │ ├─ Writing builder class to file: org.example.PersonDtoBuilder +[INFO] [DEBUG] │ └─ Successfully generated builder: PersonDtoBuilder +[INFO] [DEBUG] Processing element: OrderDto +[INFO] [DEBUG] ├─ Extracting builder definition from: org.example.OrderDto +[INFO] [DEBUG] │ └─ Builder will be generated as: org.example.OrderDtoBuilder +[INFO] [DEBUG] │ └─ Processed 1 possible setters: added 1 fields, skipped 0 +[INFO] [DEBUG] ├─ Code generation for builder: OrderDtoBuilder +[INFO] [DEBUG] │ └─ Successfully generated builder: OrderDtoBuilder +[INFO] [DEBUG] Processing element: CustomerDto (with conflicts) +[INFO] [DEBUG] ├─ Extracting builder definition from: org.example.CustomerDto +[INFO] [DEBUG] │ └─ Builder will be generated as: org.example.CustomerDtoBuilder +[INFO] [DEBUG] │ └─ Processed 2 possible setters: added 2 fields, skipped 0 +[WARNING] │ │ └─ Builder field conflict: field 'name' (type Optional) renamed to 'nameOptional' to avoid conflict +[INFO] [DEBUG] ├─ Code generation for builder: CustomerDtoBuilder +[INFO] [DEBUG] │ └─ Successfully generated builder: CustomerDtoBuilder +[INFO] simple-builders: Successfully generated 3 builder(s) in this processing round ``` **Note**: Debug messages are prefixed with `[DEBUG]` and use `Diagnostic.Kind.OTHER` which appears as `[INFO]` in Maven output. @@ -99,9 +130,8 @@ When debug logging is enabled, you'll see detailed output with visual separators 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 +[INFO] simple-builders: PROCESSING ROUND START +[INFO] simple-builders: Successfully generated 3 builder(s) in this processing round ``` ## Troubleshooting diff --git a/example-custom-generator/pom.xml b/example-custom-generator/pom.xml index 2c29cb5e..69fff13b 100644 --- a/example-custom-generator/pom.xml +++ b/example-custom-generator/pom.xml @@ -17,7 +17,7 @@ 2.25 3.18.0 - 3.13.0 + 3.15.0 ${java.version} ${java.version} ${java.version} @@ -48,6 +48,7 @@ ${java.version} ${java.version} ${java.version} + none diff --git a/example/src/main/java/org/javahelpers/simple/builders/example/FieldConflictExample.java b/example/src/main/java/org/javahelpers/simple/builders/example/FieldConflictExample.java new file mode 100644 index 00000000..31a76631 --- /dev/null +++ b/example/src/main/java/org/javahelpers/simple/builders/example/FieldConflictExample.java @@ -0,0 +1,66 @@ +package org.javahelpers.simple.builders.example; + +import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + +/** + * Example DTO demonstrating field conflict resolution in Simple Builders. + * + *

This class intentionally creates a field conflict scenario by having two setters + * with the same name but different parameter types. This is a common user error that + * Simple Builders handles gracefully by renaming conflicting fields in the generated + * builder.

+ * + *

Conflict Scenario

+ * + * + *

Expected Behavior

+ *

When debug logging is enabled ({@code -Dsimplebuilder.verbose=true}), you will see + * warning messages showing how Simple Builders resolves this conflict:

+ * + * + *

How to Fix the Conflict

+ *

The proper solution is to give the setters different names:

+ *
{@code
+ * public void setFirstName(String name) {
+ *     this.firstName = name;
+ * }
+ * 
+ * public void setLastName(Optional name) {
+ *     this.lastName = name;
+ * }
+ * }
+ * + *

Alternatively, use {@code @IgnoreInBuilder} on one of the setters if the conflict + * is intentional.

+ * + * @see org.javahelpers.simple.builders.core.annotations.IgnoreInBuilder + */ +@SimpleBuilder +public class FieldConflictExample { + private String firstName; + private java.util.Optional lastName; + + public void setName(String name) { + this.firstName = name; + } + + public void setName(java.util.Optional name) { + this.lastName = name; + } + + public String getFirstName() { + return firstName; + } + + public String getLastName() { + return lastName.orElse(null); + } +} diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/FieldConflictExampleBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/FieldConflictExampleBuilder.java new file mode 100644 index 00000000..7a1defe7 --- /dev/null +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/FieldConflictExampleBuilder.java @@ -0,0 +1,239 @@ +package org.javahelpers.simple.builders.example; + +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.Optional; +import java.util.function.BooleanSupplier; +import java.util.function.Consumer; +import java.util.function.Supplier; +import javax.annotation.processing.Generated; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.javahelpers.simple.builders.core.annotations.BuilderImplementation; +import org.javahelpers.simple.builders.core.interfaces.IBuilderBase; +import org.javahelpers.simple.builders.core.util.BuilderToStringStyle; +import org.javahelpers.simple.builders.core.util.TrackedValue; + +/** + * Builder for {@code org.javahelpers.simple.builders.example.FieldConflictExample}. + *

+ * This builder provides a fluent API for creating instances of org.javahelpers.simple.builders.example.FieldConflictExample 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 = FieldConflictExample.class +) +public class FieldConflictExampleBuilder implements IBuilderBase { + /** + * Tracked value for name: name. + */ + private TrackedValue name = unsetValue(); + + /** + * Tracked value for nameOptional: name. + */ + private TrackedValue> nameOptional = unsetValue(); + + /** + * Empty constructor of builder for {@code org.javahelpers.simple.builders.example.FieldConflictExample}. + */ + public FieldConflictExampleBuilder() { + } + + /** + * Initialisation of builder for {@code org.javahelpers.simple.builders.example.FieldConflictExample} by a instance. + * + * @param instance object instance for initialisiation + */ + public FieldConflictExampleBuilder(FieldConflictExample instance) { + } + + /** + * Creating a new builder for {@code org.javahelpers.simple.builders.example.FieldConflictExample}. + * + * @return builder for {@code org.javahelpers.simple.builders.example.FieldConflictExample} + */ + public static FieldConflictExampleBuilder create() { + return new FieldConflictExampleBuilder(); + } + + /** + * Sets the value for name. + * + * @param name name + * @return current instance of builder + */ + public FieldConflictExampleBuilder name(String name) { + this.name = changedValue(name); + return this; + } + + /** + * Sets the value for name. + * + * @param name name + * @return current instance of builder + */ + public FieldConflictExampleBuilder name(Optional name) { + this.nameOptional = changedValue(name); + return this; + } + + /** + * Sets the value for name by executing the provided consumer. + * + * @param nameStringBuilderConsumer consumer providing an instance of name + * @return current instance of builder + */ + public FieldConflictExampleBuilder name(Consumer nameStringBuilderConsumer) { + StringBuilder builder = new StringBuilder(); + nameStringBuilderConsumer.accept(builder); + this.name = changedValue(builder.toString()); + return this; + } + + /** + * Sets the value for name by invoking the provided supplier. + * + * @param nameSupplier supplier for name + * @return current instance of builder + */ + public FieldConflictExampleBuilder name(Supplier nameSupplier) { + this.name = changedValue(nameSupplier.get()); + return this; + } + + /** + * 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 + */ + public FieldConflictExampleBuilder name(String format, Object... args) { + this.nameOptional = changedValue(Optional.of(String.format(format, args))); + return this; + } + + /** + * Sets the value for nameOptional by executing the provided consumer. + * + * @param nameOptionalStringBuilderConsumer consumer providing an instance of name + * @return current instance of builder + */ + public FieldConflictExampleBuilder nameOptional( + Consumer nameOptionalStringBuilderConsumer) { + StringBuilder builder = new StringBuilder(); + nameOptionalStringBuilderConsumer.accept(builder); + this.nameOptional = changedValue(Optional.of(builder.toString())); + 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 + */ + FieldConflictExampleBuilder validateName() { + if (!name.isSet() || name.value().trim().isEmpty()) { + 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 FieldConflictExampleBuilder 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 FieldConflictExampleBuilder conditional(BooleanSupplier condition, + Consumer trueCase, + Consumer falseCase) { + if (condition.getAsBoolean()) { + trueCase.accept(this); + } else if (falseCase != null) { + falseCase.accept(this); + } + return this; + } + + /** + * Builds the configured DTO instance. + */ + @Override + public FieldConflictExample build() { + FieldConflictExample result = new FieldConflictExample(); + this.name.ifSet(result::setName); + this.nameOptional.ifSet(result::setName); + return result; + } + + /** + * 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("nameOptional", this.nameOptional) + .toString(); + } + + /** + * Interface that can be implemented by the DTO to provide fluent modification methods. + */ + 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. + * + * @param b the consumer to apply modifications + * @return the modified instance + */ + default FieldConflictExample with(Consumer b) { + FieldConflictExampleBuilder builder; + try { + builder = new FieldConflictExampleBuilder(FieldConflictExample.class.cast(this)); + } catch (ClassCastException ex) { + throw new IllegalArgumentException("The interface 'FieldConflictExampleBuilder.With' should only be implemented by classes, which could be casted to 'FieldConflictExample'", ex); + } + b.accept(builder); + return builder.build(); + } + + /** + * Creates a builder initialized from this instance. + * + * @return a builder initialized with this instance's values + */ + default FieldConflictExampleBuilder with() { + try { + return new FieldConflictExampleBuilder(FieldConflictExample.class.cast(this)); + } catch (ClassCastException ex) { + throw new IllegalArgumentException("The interface 'FieldConflictExampleBuilder.With' should only be implemented by classes, which could be casted to 'FieldConflictExample'", ex); + } + } + } +} 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 dbfc3939..1b111fa7 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 @@ -70,6 +70,7 @@ public class BuilderProcessor extends AbstractProcessor { public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); ProcessingLogger logger = new ProcessingLogger(processingEnv); + logger.debug("Starting BuilderProcessor..."); // Read global configuration from compiler arguments CompilerArgumentsReader reader = new CompilerArgumentsReader(processingEnv); @@ -80,6 +81,14 @@ public synchronized void init(ProcessingEnvironment processingEnv) { this.codeGenerator = new JavaCodeGenerator(processingEnv, logger); this.jacksonModuleGenerator = new JacksonModuleGenerator(processingEnv, logger); + // Initialize GeneratorRegistry once during processor initialization + context.debugStartOperation("Initializing generator registry"); + try { + context.getGeneratorRegistry(); + } finally { + context.debugEndOperation(); + } + SourceVersion current = processingEnv.getSourceVersion(); this.supportedJdk = isAtLeastJava17(current); if (!this.supportedJdk) { @@ -102,6 +111,8 @@ public boolean process(Set annotations, RoundEnvironment for (var module : modules) { codeGenerator.generateJacksonModule(module); } + // Reset indentation after Jackson module generation as well + context.resetIndentation(); return false; } @@ -126,9 +137,7 @@ public boolean process(Set annotations, RoundEnvironment elementsToProcess.addAll(roundEnv.getElementsAnnotatedWith(annotation)); } - context.debug("==============================="); context.info("simple-builders: PROCESSING ROUND START"); - context.debug("==============================="); context.debug( "simple-builders: Processing round started. Found %d annotated elements.", elementsToProcess.size()); @@ -139,24 +148,35 @@ public boolean process(Set annotations, RoundEnvironment .sorted(Comparator.comparing(element -> element.getSimpleName().toString())) .toList(); + int successfulGenerations = 0; for (Element annotatedElement : sortedElements) { + context.debugStartOperation("Processing element: " + annotatedElement.getSimpleName()); try { - context.debug("------------------------------------"); - context.debug("simple-builders: Processing element: %s", annotatedElement.getSimpleName()); - context.debug("------------------------------------"); // Resolve configuration per-element to handle all layers // (defaults, global, template, inline) BuilderConfiguration config = reader.resolveConfiguration(annotatedElement); + context.debug("Configuration resolved: %s", config); process(annotatedElement, config); - context.info( - "simple-builders: Successfully generated builder for: %s", - annotatedElement.getSimpleName()); + successfulGenerations++; } catch (BuilderException ex) { - // All builder generation failures are warnings to allow other builders to be generated + // 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()); + } finally { + context.debugEndOperation(); } } + + // Log summary of builder generation + if (successfulGenerations > 0) { + context.info( + "simple-builders: Successfully generated %d builder(s) in this processing round", + successfulGenerations); + } + + // Reset indentation level at the end of each processing round to prevent cascading errors + context.resetIndentation(); return true; } @@ -183,6 +203,14 @@ private void process(Element annotatedElement, BuilderConfiguration config) // Collect info for Jackson Module if enabled jacksonModuleGenerator.addEntry(builderDef, annotatedElement); + context.debug("Jackson module entry added"); + + // Add summary of what was generated + context.debugEndOperation( + "Generated builder with %d fields and %d methods for %s", + builderDef.getAllFieldsForBuilder().size(), + builderDef.getCoreMethods().size(), + builderDef.getBuilderTypeName().getClassName()); } /** diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/ClassJavaDocEnhancer.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/ClassJavaDocEnhancer.java index 76bd40ea..e47fab5a 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/ClassJavaDocEnhancer.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/ClassJavaDocEnhancer.java @@ -93,9 +93,6 @@ public void enhanceBuilder(BuilderDefinitionDto builderDto, ProcessingContext co CodeBlock javadoc = createClassJavadoc(dtoClass); builderDto.setClassJavadoc(javadoc.toString()); - - context.debug( - "Added class JavaDoc to builder %s", builderDto.getBuilderTypeName().getClassName()); } /** diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/CoreMethodsEnhancer.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/CoreMethodsEnhancer.java index 5e6a33d4..394f784e 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/CoreMethodsEnhancer.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/CoreMethodsEnhancer.java @@ -107,9 +107,6 @@ public void enhanceBuilder(BuilderDefinitionDto builderDto, ProcessingContext co // Add toString() method MethodDto toStringMethod = createToStringMethod(builderDto); builderDto.addCoreMethod(toStringMethod); - - context.debug( - "Added core methods to builder %s", builderDto.getBuilderTypeName().getClassName()); } /** Creates the build() method. */ diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/GeneratedAnnotationEnhancer.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/GeneratedAnnotationEnhancer.java index 7fa470db..865fc7ae 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/GeneratedAnnotationEnhancer.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/GeneratedAnnotationEnhancer.java @@ -81,10 +81,6 @@ public boolean appliesTo( public void enhanceBuilder(BuilderDefinitionDto builderDto, ProcessingContext context) { AnnotationDto generatedAnnotation = createGeneratedAnnotation(); builderDto.addClassAnnotation(generatedAnnotation); - - context.debug( - "Added @Generated annotation to builder %s", - builderDto.getBuilderTypeName().getClassName()); } /** diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/GeneratorRegistry.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/GeneratorRegistry.java index eed36cad..b3d46955 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/GeneratorRegistry.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/GeneratorRegistry.java @@ -74,7 +74,8 @@ public GeneratorRegistry(ProcessingContext context, ProcessingEnvironment proces loadAllGenerators(); sortGeneratorsByPriority(); - context.debug( + // Log the closing message with └─ without changing indentation level + context.debugEndOperation( "Initialized GeneratorRegistry with %d method generators and %d builder enhancers", methodGenerators.size(), builderEnhancers.size()); } @@ -91,11 +92,12 @@ public List generateAllMethods( FieldDto field, TypeName dtoType, TypeName builderType) { List allMethods = new ArrayList<>(); + context.debugStartOperation("Processing method generators"); for (MethodGenerator generator : methodGenerators) { if (generator.appliesTo(field, dtoType, context)) { try { context.debug( - " -> Applying method generator: %s (priority: %d)", + "Applying: %s (priority: %d)", generator.getClass().getSimpleName(), generator.getPriority()); List generatedMethods = generator.generateMethods(field, builderType, context); @@ -110,6 +112,7 @@ public List generateAllMethods( } } } + context.debugEndOperation("Generated %d methods", allMethods.size()); return allMethods; } @@ -121,18 +124,17 @@ public List generateAllMethods( * @param dtoType the DTO type the builder is for */ public void enhanceBuilder(BuilderDefinitionDto builderDto, TypeName dtoType) { + int appliedEnhancers = 0; + context.debugStartOperation("Processing class based enhancer"); for (BuilderEnhancer enhancer : builderEnhancers) { if (enhancer.appliesTo(builderDto, dtoType, context)) { try { context.debug( - " -> Applying builder enhancer: %s (priority: %d)", + "Applying: %s (priority: %d)", enhancer.getClass().getSimpleName(), enhancer.getPriority()); enhancer.enhanceBuilder(builderDto, context); - - context.debug( - " Enhanced builder %s with %s", - builderDto.getBuilderTypeName().getClassName(), enhancer.getClass().getSimpleName()); + appliedEnhancers++; } catch (Exception e) { context.error( "Failed to apply enhancer %s to builder %s: %s", @@ -142,19 +144,23 @@ public void enhanceBuilder(BuilderDefinitionDto builderDto, TypeName dtoType) { } } } + + if (appliedEnhancers > 0) { + context.debugEndOperation("Applied %d builder enhancers", appliedEnhancers); + } else { + context.debugEndOperation(); + } } /** - * Loads all generators (built-in and custom) via ServiceLoader. + * Loads all available generators from the service loader and separates them into method + * generators and builder enhancers. * *

Generators are discovered by looking for implementations of {@link Generator} declared in * {@code META-INF/services/org.javahelpers.simple.builders.processor.generators.Generator} files. * *

The loaded generators are separated into method generators and builder enhancers based on - * their type (using the sealed interface hierarchy). - * - *

If loading fails for any generator, a warning is logged but processing continues with the - * remaining generators. + * their type. */ private void loadAllGenerators() { int methodGenCount = 0; @@ -178,21 +184,16 @@ private void loadAllGenerators() { if (generator instanceof MethodGenerator methodGen) { methodGenerators.add(methodGen); methodGenCount++; - context.debug( - "Loaded method generator: %s (priority: %d)", - generatorClassName, methodGen.getPriority()); } else if (generator instanceof BuilderEnhancer enhancer) { builderEnhancers.add(enhancer); enhancerCount++; - context.debug( - "Loaded builder enhancer: %s (priority: %d)", - generatorClassName, enhancer.getPriority()); } } } catch (Exception e) { context.error("Failed to load generators: %s", e.getMessage()); } + // Only log summary, not individual generators (too verbose) context.debug( "Loaded %d method generators and %d builder enhancers total", methodGenCount, enhancerCount); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/InterfaceEnhancer.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/InterfaceEnhancer.java index 09dfb12d..aa51495b 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/InterfaceEnhancer.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/InterfaceEnhancer.java @@ -92,10 +92,6 @@ public void enhanceBuilder(BuilderDefinitionDto builderDto, ProcessingContext co // Add the IBuilderBase interface to the builder InterfaceName builderBaseInterface = createBuilderBaseInterface(builderDto); builderDto.addInterface(builderBaseInterface); - - context.debug( - "Added IBuilderBase interface to builder %s", - builderDto.getBuilderTypeName().getClassName()); } /** diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/JacksonAnnotationEnhancer.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/JacksonAnnotationEnhancer.java index d3ef7ad2..9e058abe 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/JacksonAnnotationEnhancer.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/JacksonAnnotationEnhancer.java @@ -92,10 +92,6 @@ public boolean appliesTo( public void enhanceBuilder(BuilderDefinitionDto builderDto, ProcessingContext context) { AnnotationDto jacksonAnnotation = createJsonPOJOBuilderAnnotation(builderDto); builderDto.addClassAnnotation(jacksonAnnotation); - - context.debug( - "Added @JsonPOJOBuilder annotation to builder %s", - builderDto.getBuilderTypeName().getClassName()); } /** diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/WithInterfaceEnhancer.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/WithInterfaceEnhancer.java index ce3cedab..1440336e 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/WithInterfaceEnhancer.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/WithInterfaceEnhancer.java @@ -96,25 +96,17 @@ public boolean appliesTo( @Override public void enhanceBuilder(BuilderDefinitionDto builderDto, ProcessingContext context) { - NestedTypeDto withInterface = createWithInterface(builderDto, context); + NestedTypeDto withInterface = createWithInterface(builderDto); builderDto.addNestedType(withInterface); - - context.debug( - "Added With interface to builder %s", builderDto.getBuilderTypeName().getClassName()); } /** * Creates the "With" interface for the builder. * * @param builderDto the builder definition - * @param context the processing context * @return the nested type DTO for the With interface */ - private NestedTypeDto createWithInterface( - BuilderDefinitionDto builderDto, ProcessingContext context) { - context.debug( - "Creating With interface for: %s", builderDto.getBuilderTypeName().getClassName()); - + private NestedTypeDto createWithInterface(BuilderDefinitionDto builderDto) { NestedTypeDto withInterface = new NestedTypeDto(); withInterface.setTypeName("With"); withInterface.setKind(NestedTypeDto.NestedTypeKind.INTERFACE); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java index 4d5cbe1a..2e41d05f 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java @@ -231,8 +231,7 @@ public BuilderConfiguration readFromTemplate(Element element) { // If @SimpleBuilder is present, ignore template annotations if (hasSimpleBuilderAnnotation(element)) { logger.debug( - "Template annotations ignored for '%s' (direct @SimpleBuilder present)", - element.getSimpleName()); + "Template annotations ignored because @SimpleBuilder present", element.getSimpleName()); return null; } @@ -240,6 +239,7 @@ public BuilderConfiguration readFromTemplate(Element element) { for (AnnotationMirror mirror : element.getAnnotationMirrors()) { BuilderConfiguration templateConfig = checkForTemplateAnnotation(mirror, element); if (templateConfig != null) { + logger.debug("Annotation based Configuration: %s", templateConfig.toString()); return templateConfig; } } @@ -350,7 +350,8 @@ private BuilderConfiguration extractOptionsFromTemplateMirror(AnnotationMirror t * @return the fully resolved configuration with all sources merged */ public BuilderConfiguration resolveConfiguration(Element element) throws BuilderException { - logger.debug("Resolving configuration for element: %s", element.getSimpleName()); + String elementName = element.getSimpleName().toString(); + logger.debugStartOperation("Resolving configuration for element: %s", elementName); BuilderConfiguration templateConfig = readFromTemplate(element); BuilderConfiguration inlineConfig = readFromInlineOptions(element); @@ -364,8 +365,7 @@ public BuilderConfiguration resolveConfiguration(Element element) throws Builder // Validate access modifiers and warn about problematic configurations validateAccessModifiers(element, result); - logger.debug("Configuration resolved for '%s': %s", element.getSimpleName(), result.toString()); - + logger.debugEndOperation("Resulting configuration resolved: %s", result.toString()); return result; } 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 1f3dc538..5123ac6f 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 @@ -64,7 +64,8 @@ public static BuilderDefinitionDto extractFromElement( validateAnnotatedElement(annotatedElement); TypeElement annotatedType = (TypeElement) annotatedElement; - context.debug("Extracting builder definition from: %s", annotatedType.getQualifiedName()); + context.debugStartOperation( + "Extracting builder definition from: %s", annotatedType.getQualifiedName()); BuilderDefinitionDto result = initializeBuilderDefinition(annotatedType, context); @@ -82,6 +83,11 @@ public static BuilderDefinitionDto extractFromElement( // Apply builder enhancers (including With interface generation) context.getGeneratorRegistry().enhanceBuilder(result, result.getBuildingTargetTypeName()); + context.debug("Builder will be generated as: %s", result.getBuilderTypeName().getClassName()); + + context.debugEndOperation( + "Builder definition extracted: %s", result.getBuilderTypeName().getClassName()); + return result; } @@ -123,14 +129,15 @@ private static List extractConstructorFields( 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()); + context.debugStartOperation( + "Analyzing constructor with %d parameter(s)", ctor.getParameters().size()); + TypeName builderType = MethodGeneratorUtil.createGenericTypeName( builderDef.getBuilderTypeName(), builderDef.getGenerics()); for (VariableElement param : ctor.getParameters()) { + context.debugStartOperation("Analyzing parameter: %s", param.getSimpleName()); Optional fieldFromCtor = createFieldFromConstructor( annotatedType, param, builderType, context, fieldNameRegistry); @@ -140,6 +147,8 @@ private static List extractConstructorFields( constructorFields.add(field); } } + + context.debugEndOperation(); } return constructorFields; } @@ -155,6 +164,7 @@ private static List extractSetterFields( BuilderDefinitionDto result, ProcessingContext context, Map fieldNameRegistry) { + context.debugStartOperation("Analysing setters for finding fields"); List setterFields = new LinkedList<>(); // Build a set of constructor field names to avoid duplicates from setters @@ -173,7 +183,7 @@ private static List extractSetterFields( result.getBuilderTypeName(), result.getGenerics()); for (ExecutableElement mth : methods) { - context.debug( + context.debugStartOperation( "Analyzing method: %s with %d parameter(s)", mth.getSimpleName(), mth.getParameters().size()); @@ -207,9 +217,13 @@ private static List extractSetterFields( } } - context.debug( - "Processed %d possible setters: added %d fields, skipped %d", - processedCount, addedCount, skippedCount); + if (addedCount != 0 || skippedCount != 0) { + context.debugEndOperation( + "Processed %d possible setters: added %d fields, skipped %d", + processedCount, addedCount, skippedCount); + } else { + context.debugEndOperation("No setters found"); + } return setterFields; } @@ -221,29 +235,29 @@ private static void logFieldAddition(FieldDto field, ProcessingContext context) && !field.getFieldType().getPackageName().isEmpty()) { fieldTypeName = field.getFieldType().getPackageName() + "." + fieldTypeName; } - context.debug(" -> Adding field: %s (type: %s)", field.getFieldName(), fieldTypeName); + context.debugEndOperation("Adding field: %s (type: %s)", field.getFieldName(), fieldTypeName); } private static boolean isMethodRelevantForBuilder( ExecutableElement mth, ProcessingContext context) { if (!hasNoThrowablesDeclared(mth)) { - context.debug(" -> Skipping: declares throwables"); + context.debug("Skipping: declares throwables"); return false; } if (!hasNoReturnValue(mth)) { - context.debug(" -> Skipping: has return value"); + context.debug("Skipping: has return value"); return false; } if (!hasNotAnnotation(IgnoreInBuilder.class, mth)) { - context.debug(" -> Skipping: has @IgnoreInBuilder annotation"); + context.debug("Skipping: has @IgnoreInBuilder annotation"); return false; } if (!isNotPrivate(mth)) { - context.debug(" -> Skipping: is private"); + context.debug("Skipping: is private"); return false; } if (!isNotStatic(mth)) { - context.debug(" -> Skipping: is static"); + context.debug("Skipping: is static"); return false; } return true; diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/FieldAnnotationExtractor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/FieldAnnotationExtractor.java index 777cd2c7..013151aa 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/FieldAnnotationExtractor.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/FieldAnnotationExtractor.java @@ -80,10 +80,6 @@ public static List extractAnnotations( List annotations = new ArrayList<>(); List annotationMirrors = param.getAnnotationMirrors(); - context.debug( - " -> Extracting %d annotation(s) from field %s", - annotationMirrors.size(), param.getSimpleName()); - for (AnnotationMirror mirror : annotationMirrors) { extractAnnotation(mirror, context).ifPresent(annotations::add); } @@ -104,10 +100,6 @@ public static List extractAnnotations( List annotations = new ArrayList<>(); List annotationMirrors = typeMirror.getAnnotationMirrors(); - context.debug( - " -> Extracting %d annotation(s) from type %s", - annotationMirrors.size(), typeMirror.toString()); - for (AnnotationMirror mirror : annotationMirrors) { extractAnnotation(mirror, context).ifPresent(annotations::add); } 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 e452c436..75cfbf5a 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 @@ -75,9 +75,8 @@ public JavaCodeGenerator(ProcessingEnvironment processingEnv, ProcessingLogger l * @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()); - + logger.debugStartOperation( + "Code generation for builder: %s", builderDef.getBuilderTypeName().getClassName()); TypeSpec.Builder classBuilder = createClassBuilder(builderDef); addClassMetadata(classBuilder, builderDef); addFieldsToBuilder(classBuilder, builderDef); @@ -86,12 +85,8 @@ public void generateBuilder(BuilderDefinitionDto builderDef) throws BuilderExcep addNestedTypesToBuilder(classBuilder, builderDef); addAnnotationsToBuilder(classBuilder, builderDef); - logger.debug( - "Writing builder class to file: %s.%s", - builderDef.getBuilderTypeName().getPackageName(), - builderDef.getBuilderTypeName().getClassName()); writeBuilderClassToFile(classBuilder.build(), builderDef); - logger.debug( + logger.debugEndOperation( "Successfully generated builder: %s", builderDef.getBuilderTypeName().getClassName()); } @@ -101,8 +96,11 @@ private TypeSpec.Builder createClassBuilder(BuilderDefinitionDto builderDef) { logger.debug("Builder has %d generic type parameter(s)", builderDef.getGenerics().size()); } - return TypeSpec.classBuilder(builderBaseClass) - .addTypeVariables(map2TypeVariables(builderDef.getGenerics())); + 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) { @@ -123,10 +121,12 @@ private void addClassMetadata(TypeSpec.Builder classBuilder, BuilderDefinitionDt JavapoetMapper.mapInterfaceToTypeName(interfaceName); classBuilder.addSuperinterface(interfaceType); } + + logger.debug("Class metadata added"); } private void addFieldsToBuilder(TypeSpec.Builder classBuilder, BuilderDefinitionDto builderDef) { - logger.debug( + logger.debugStartOperation( "Generating %d constructor fields and %d setter fields", builderDef.getConstructorFieldsForBuilder().size(), builderDef.getSetterFieldsForBuilder().size()); @@ -141,22 +141,26 @@ private void addFieldsToBuilder(TypeSpec.Builder classBuilder, BuilderDefinition 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()); + logger.debug("Resolved %d methods after conflict resolution", resolvedMethods.size()); // Generate all methods in order + int generatedCnt = 0; for (MethodDto methodDto : resolvedMethods) { - logger.debug(" Generating method: %s", methodDto); MethodSpec methodSpec = createMethod(methodDto); classBuilder.addMethod(methodSpec); + generatedCnt++; } + logger.debugEndOperation("%d Methods added", generatedCnt); } private Map collectAllMethods(BuilderDefinitionDto builderDef) { @@ -184,29 +188,45 @@ private Map collectAllMethods(BuilderDefinitionDto builderD 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.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(); @@ -296,10 +316,20 @@ private void writeSimpleClassToFile(String packageName, TypeSpec typeSpec) * @return list of all methods with conflicts resolved, sorted by ordering for proper generation */ private List resolveMethodConflicts(Map methodToField) { - Map signatureToMethod = new HashMap<>(); + 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 : methodToField.entrySet()) { + for (Map.Entry entry : sortedEntries) { MethodDto method = entry.getKey(); FieldDto field = entry.getValue(); String signature = method.getSignatureKey(); @@ -333,8 +363,8 @@ private List resolveMethodConflicts(Map methodTo } } - // Sort methods using enhanced sorting logic - return signatureToMethod.values().stream().sorted(new MethodDto.MethodComparator()).toList(); + // Return methods in insertion order (already sorted from conflict resolution) + return new java.util.ArrayList<>(signatureToMethod.values()); } /** @@ -379,8 +409,6 @@ private void generateConstructors( builderDef.getAllFieldsForBuilder(), constructorAccessModifier); classBuilder.addMethod(instanceConstructor); - - logger.debug(" Generated constructors for builder"); } private MethodSpec createEmptyConstructor(ClassName dtoClass, Modifier accessModifier) { 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 1ee82151..80b0f326 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 @@ -258,7 +258,8 @@ public void debug(String message) { } /** - * Logs a debug message with a formatted string. + * Logs a debug message with a formatted string. Only visible when enabled via -Averbose=true or + * -Asimplebuilder.verbose=true. * * @param format the format string * @param args arguments referenced by the format specifiers in the format string @@ -267,6 +268,31 @@ public void debug(String format, Object... args) { logger.debug(format, args); } + /** + * Starts a new hierarchical operation context for logging with formatted message. + * + * @param format the format string for the operation message + * @param args arguments referenced by the format specifiers + */ + public void debugStartOperation(String format, Object... args) { + logger.debugStartOperation(format, args); + } + + /** Ends the current hierarchical operation context for logging. */ + public void debugEndOperation() { + logger.debugEndOperation(); + } + + /** Ends the current hierarchical operation context with a closing message for logging. */ + public void debugEndOperation(String format, Object... args) { + logger.debugEndOperation(format, args); + } + + /** Resets the indentation level to prevent cascading errors between processing runs. */ + public void resetIndentation() { + logger.resetIndentation(); + } + /** * Logs a warning message without requiring a specific element context. * 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 9d1fc307..409cfbc4 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 @@ -42,6 +42,9 @@ public class ProcessingLogger { /** Flag indicating if debug logging is enabled. */ private final boolean debugEnabled; + /** Thread-local indentation level for hierarchical logging. */ + private static final ThreadLocal indentationLevel = ThreadLocal.withInitial(() -> 0); + /** * Constructs a new ProcessingLogger with the specified ProcessingEnvironment. The Messager is * used to report errors, warnings, and other notices during annotation processing. Debug logging @@ -50,11 +53,19 @@ public class ProcessingLogger { * @param processingEnv the processing environment providing messager and options */ public ProcessingLogger(ProcessingEnvironment processingEnv) { + // Reset ThreadLocal state to ensure clean state between test runs + resetThreadLocalState(); + this.messager = processingEnv.getMessager(); CompilerArgumentsReader reader = new CompilerArgumentsReader(processingEnv); this.debugEnabled = reader.readBooleanValue(CompilerArgumentsEnum.VERBOSE); } + /** Resets ThreadLocal state to ensure clean state between test runs. */ + private void resetThreadLocalState() { + indentationLevel.set(0); + } + /** * Reports an error with a formatted message. * @@ -75,6 +86,20 @@ public void info(String message) { messager.printMessage(Diagnostic.Kind.NOTE, message); } + /** + * Formats a message with hierarchical indentation and proper spacing for alignment. Used by info + * and warning methods when debug mode is enabled. + * + * @param message the message to format + * @param spaces the number of spaces to add before the hierarchy for alignment + * @return the formatted message with proper indentation and spacing + */ + private String formatHierarchicalMessage(String message, int spaces) { + String prefix = getCurrentIndentationLevel() > 0 ? "└─ " : ""; + String spacing = " ".repeat(spaces); + return String.format("%s%s", spacing, formatWithIndentationNoDebug(message, prefix)); + } + /** * Posts an info-level message with a formatted string. * @@ -82,7 +107,16 @@ public void info(String message) { * @param args arguments referenced by the format specifiers in the format string */ public void info(String format, Object... args) { - messager.printMessage(Diagnostic.Kind.NOTE, String.format(format, args)); + String message = String.format(format, args); + if (debugEnabled) { + // When debug is enabled, add spaces to align with [DEBUG] prefix (which is 6 characters + // longer) + String indentedMessage = formatHierarchicalMessage(message, 8); // Add 8 spaces for alignment + messager.printMessage(Diagnostic.Kind.NOTE, indentedMessage); + } else { + // When debug is disabled, use flat formatting (current behavior) + messager.printMessage(Diagnostic.Kind.NOTE, message); + } } /** @@ -94,8 +128,8 @@ public void info(String format, Object... args) { */ public void debug(String message) { if (debugEnabled) { - String formatWithDebug = "[DEBUG] " + message; - messager.printMessage(Diagnostic.Kind.OTHER, formatWithDebug); + String indentedMessage = formatWithIndentation(message); + messager.printMessage(Diagnostic.Kind.OTHER, indentedMessage); } } @@ -108,8 +142,9 @@ public void debug(String message) { */ public void debug(String format, Object... args) { if (debugEnabled) { - String formatWithDebug = "[DEBUG] " + format; - messager.printMessage(Diagnostic.Kind.OTHER, String.format(formatWithDebug, args)); + String message = String.format(format, args); + String indentedMessage = formatWithIndentation(message); + messager.printMessage(Diagnostic.Kind.OTHER, indentedMessage); } } @@ -121,7 +156,16 @@ public void debug(String format, Object... args) { * @param args arguments referenced by the format specifiers in the format string */ public void warning(String format, Object... args) { - messager.printMessage(Diagnostic.Kind.WARNING, String.format(format, args)); + String message = String.format(format, args); + if (debugEnabled) { + // When debug is enabled, add spaces to align with [DEBUG] prefix (which is 5 characters + // longer) + String indentedMessage = formatHierarchicalMessage(message, 5); // Add 5 spaces for alignment + messager.printMessage(Diagnostic.Kind.WARNING, indentedMessage); + } else { + // When debug is disabled, print flat + messager.printMessage(Diagnostic.Kind.WARNING, message); + } } /** @@ -132,6 +176,144 @@ public void warning(String format, Object... args) { * @param args arguments referenced by the format specifiers in the format string */ public void warning(Element e, String format, Object... args) { - messager.printMessage(Diagnostic.Kind.WARNING, String.format(format, args), e); + String message = String.format(format, args); + if (debugEnabled) { + // When debug is enabled, add spaces to align with [DEBUG] prefix (which is 5 characters + // longer) + String indentedMessage = formatHierarchicalMessage(message, 5); // Add 5 spaces for alignment + messager.printMessage(Diagnostic.Kind.WARNING, indentedMessage, e); + } else { + // When debug is disabled, print flat + messager.printMessage(Diagnostic.Kind.WARNING, message, e); + } + } + + /** + * Formats a message with appropriate indentation based on current context. + * + * @param message the message to format + * @param prefix the prefix to add (e.g., "├─ ", "└─ ") or empty string for no prefix + * @return the formatted message with indentation + */ + private String formatWithIndentation(String message, String prefix) { + int level = indentationLevel.get(); + if (level == 0) { + // At level 0, no indentation or prefix + return "[DEBUG] " + message; + } + + // Use │ characters with proper spacing for better visual connection between hierarchical levels + StringBuilder indent = new StringBuilder(); + for (int i = 0; i < level - 1; i++) { + indent.append("│ "); + } + // Add the specified prefix + indent.append(prefix); + return "[DEBUG] " + indent + message; + } + + /** + * Formats a message with indentation but without [DEBUG] prefix for INFO/WARNING messages. + * + * @param message the message to format + * @param prefix the prefix to add (e.g., "├─ ", "└─ ") or empty string for no prefix + * @return the formatted message with indentation but without [DEBUG] prefix + */ + private String formatWithIndentationNoDebug(String message, String prefix) { + int level = indentationLevel.get(); + if (level == 0) { + // At level 0, no indentation or prefix + return message; + } + + // Use │ characters with proper spacing for better visual connection between hierarchical levels + StringBuilder indent = new StringBuilder(); + for (int i = 0; i < level - 1; i++) { + indent.append("│ "); + } + // Add the specified prefix + indent.append(prefix); + return indent + message; + } + + /** + * Formats a message with ├─ prefix for debug messages. + * + * @param message the message to format + * @return the formatted message with indentation and ├─ prefix + */ + private String formatWithIndentation(String message) { + return formatWithIndentation(message, "├─ "); + } + + /** + * Starts a new hierarchical operation context, increasing indentation for subsequent debug + * messages. This should be called before starting a major operation that has sub-operations. + * + * @param formatClosingMessage the format string for the operation message + * @param args arguments referenced by the format specifiers + */ + public void debugStartOperation(String formatClosingMessage, Object... args) { + int currentLevel = indentationLevel.get(); + + // Log the operation start message with proper prefix handling + if (debugEnabled) { + String operationMessage = formatWithIndentation(String.format(formatClosingMessage, args)); + messager.printMessage(Diagnostic.Kind.NOTE, operationMessage); + } + + // Increase indentation for subsequent messages + indentationLevel.set(currentLevel + 1); + } + + /** + * Ends the current hierarchical operation context, decreasing indentation. This should be called + * after completing an operation started with startOperation. + */ + public void debugEndOperation() { + int currentLevel = indentationLevel.get(); + if (currentLevel > 0) { + indentationLevel.set(currentLevel - 1); + // No need to log "Operation completed" - it's redundant and adds noise + } + } + + /** + * Ends the current hierarchical operation context with a closing message, decreasing indentation. + * This should be called after completing an operation started with startOperation when you want + * to log a closing message with the proper tree structure (using └─ for the last operation). + * + * @param formatClosingMessage the format string for the closing message + * @param args arguments referenced by the format specifiers + */ + public void debugEndOperation(String formatClosingMessage, Object... args) { + int currentLevel = indentationLevel.get(); + + // Log the closing message with └─ to indicate it's the last operation at this level + if (debugEnabled) { + String closingMessage = String.format(formatClosingMessage, args); + String operationMessage = formatWithIndentation(closingMessage, "└─ "); + messager.printMessage(Diagnostic.Kind.NOTE, operationMessage); + } + + // Decrease indentation level + if (currentLevel > 0) { + indentationLevel.set(currentLevel - 1); + } + } + + /** + * Resets the indentation level to zero to prevent cascading errors between processing runs. This + * should be called at the end of each processing round. + */ + public void resetIndentation() { + indentationLevel.set(0); + // Clean up ThreadLocal to prevent memory leaks + indentationLevel.remove(); + } + + /** Gets the current indentation level for debugging purposes. */ + public int getCurrentIndentationLevel() { + return indentationLevel.get(); } } 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 b704d01c..1a707c56 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 @@ -1,3 +1,27 @@ +/* + * MIT License + * + * Copyright (c) 2025 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + package org.javahelpers.simple.builders.processor; import static com.google.testing.compile.CompilationSubject.assertThat; @@ -62,14 +86,61 @@ void shouldLogDebugMessagesWhenVerboseModeEnabled() { // Then: Compilation succeeds and debug messages are present assertThat(compilation).succeeded(); - // Verify key debug messages are logged + // Verify key debug messages are logged with hierarchical format ProcessorAsserts.assertHadNoteContaining( compilation, + "[DEBUG] Starting BuilderProcessor...", + "[DEBUG] Loaded global configuration from compiler arguments: BuilderConfiguration[]", + "[DEBUG] Initializing generator registry", + "[DEBUG] ├─ Loaded 14 method generators and 8 builder enhancers total", + "[DEBUG] └─ Initialized GeneratorRegistry with 14 method generators and 8 builder", "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"); + "[DEBUG] simple-builders: Processing round started. Found 1 annotated elements.", + "[DEBUG] Processing element: VerboseTest", + "[DEBUG] ├─ Resolving configuration for element: VerboseTest", + "[DEBUG] │ ├─ Template annotations ignored because @SimpleBuilder present", + "[DEBUG] │ └─ Resulting configuration resolved: BuilderConfiguration[", + "[DEBUG] ├─ Extracting builder definition from: test.VerboseTest", + "[DEBUG] │ ├─ Builder will be generated as: test.VerboseTestBuilder", + "[DEBUG] │ ├─ Analysing setters for finding fields", + "[DEBUG] │ │ ├─ Analyzing method: setName with 1 parameter(s)", + "[DEBUG] │ │ │ ├─ Processing method generators", + "[DEBUG] │ │ │ │ ├─ Applying: BasicSetterGenerator (priority: 100)", + "[DEBUG] │ │ │ │ ├─ Applying: StringFormatHelperGenerator (priority: 80)", + "[DEBUG] │ │ │ │ ├─ Applying: SupplierMethodGenerator (priority: 60)", + "[DEBUG] │ │ │ │ ├─ Applying: StringBuilderConsumerGenerator (priority: 45)", + "[DEBUG] │ │ │ │ └─ Generated 4 methods", + "[DEBUG] │ │ │ └─ Adding field: name (type: java.lang.String)", + "[DEBUG] │ │ └─ Processed 1 possible setters: added 1 fields, skipped 0", + "[DEBUG] │ ├─ Processing class based enhancer", + "[DEBUG] │ │ ├─ Applying: ClassJavaDocEnhancer (priority: 200)", + "[DEBUG] │ │ ├─ Applying: GeneratedAnnotationEnhancer (priority: 120)", + "[DEBUG] │ │ ├─ Applying: BuilderImplementationAnnotationEnhancer (priority: 115)", + "[DEBUG] │ │ ├─ Applying: CoreMethodsEnhancer (priority: 100)", + "[DEBUG] │ │ ├─ Applying: WithInterfaceEnhancer (priority: 95)", + "[DEBUG] │ │ ├─ Applying: InterfaceEnhancer (priority: 90)", + "[DEBUG] │ │ ├─ Applying: ConditionalEnhancer (priority: 80)", + "[DEBUG] │ │ └─ Applied 7 builder enhancers", + "[DEBUG] │ └─ Builder definition extracted: VerboseTestBuilder", + "[DEBUG] ├─ Code generation for builder: VerboseTestBuilder", + "[DEBUG] │ ├─ Class builder created", + "[DEBUG] │ ├─ Class metadata added", + "[DEBUG] │ ├─ Generating 0 constructor fields and 1 setter fields", + "[DEBUG] │ │ └─ Fields added: 1 fields", + "[DEBUG] │ ├─ Adding Methods for 9 candidates", + "[DEBUG] │ │ ├─ Resolved 9 methods after conflict resolution", + "[DEBUG] │ │ └─ 9 Methods added", + "[DEBUG] │ ├─ Constructors added", + "[DEBUG] │ ├─ Generating 1 nested type(s)", + "[DEBUG] │ │ ├─ Generated nested type: With", + "[DEBUG] │ │ └─ Nested types added", + "[DEBUG] │ ├─ Class-level annotations added", + "[DEBUG] │ ├─ Writing builder class to file: test.VerboseTestBuilder", + "[DEBUG] │ └─ Successfully generated builder: VerboseTestBuilder", + "[DEBUG] ├─ Jackson module entry added", + "[DEBUG] └─ Generated builder with 1 fields and 5 methods for VerboseTestBuilder", + "simple-builders: Successfully generated 1 builder(s) in this processing round", + ""); } @Test @@ -2641,7 +2712,7 @@ void shouldHandleOverloadedSettersForSameFieldWithoutConflicts() { generatedCode, "public OverloadedNamesBuilder names(List names)", "public OverloadedNamesBuilder names(String... names)", - "public OverloadedNamesBuilder names(Supplier> namesSupplier)"); + "public OverloadedNamesBuilder names(Supplier namesSupplier)"); } @Test 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 f846668d..45012b2f 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 @@ -137,11 +137,6 @@ public PersonDto(String name, int age, Optional email, String generatedCode = loadGeneratedSource(compilation, "PersonDtoBuilder"); - // Debug output: Print generated code for comparison when test fails - System.out.println("=== Generated PersonDtoBuilder ==="); - System.out.println(generatedCode); - System.out.println("=== End of Generated Code ==="); - // This test uses full code comparison to ensure ALL features are generated. // When a new feature is added, this expected code MUST be updated or the test will fail. // This will catch when new features like add2FieldName are added but not included here. diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/MethodConflictResolutionTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/MethodConflictResolutionTest.java index 6880d6c0..e8811508 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/MethodConflictResolutionTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/MethodConflictResolutionTest.java @@ -1,3 +1,27 @@ +/* + * MIT License + * + * Copyright (c) 2025 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + package org.javahelpers.simple.builders.processor; import static com.google.testing.compile.CompilationSubject.assertThat;