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 16363245..c3a1a19b 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 @@ -25,11 +25,11 @@ package org.javahelpers.simple.builders.processor; import static org.javahelpers.simple.builders.processor.processing.BuilderDefinitionCreator.extractFromElement; -import static org.javahelpers.simple.builders.processor.processing.logging.PerformanceTracker.PHASE_BUILDER_DEFINITION_EXTRACTION; -import static org.javahelpers.simple.builders.processor.processing.logging.PerformanceTracker.PHASE_CODE_GENERATION; -import static org.javahelpers.simple.builders.processor.processing.logging.PerformanceTracker.PHASE_CONFIGURATION_RESOLUTION; -import static org.javahelpers.simple.builders.processor.processing.logging.PerformanceTracker.PHASE_DTO_MAPPING; -import static org.javahelpers.simple.builders.processor.processing.logging.PerformanceTracker.PHASE_ELEMENT_COLLECTION; +import static org.javahelpers.simple.builders.processor.processing.ProcessingPhases.PHASE_BUILDER_DEFINITION_EXTRACTION; +import static org.javahelpers.simple.builders.processor.processing.ProcessingPhases.PHASE_CODE_GENERATION; +import static org.javahelpers.simple.builders.processor.processing.ProcessingPhases.PHASE_CONFIGURATION_RESOLUTION; +import static org.javahelpers.simple.builders.processor.processing.ProcessingPhases.PHASE_DTO_MAPPING; +import static org.javahelpers.simple.builders.processor.processing.ProcessingPhases.PHASE_ELEMENT_COLLECTION; import com.google.auto.service.AutoService; import java.util.ArrayList; @@ -63,6 +63,8 @@ import org.javahelpers.simple.builders.processor.analysis.BuilderScopeResolver; import org.javahelpers.simple.builders.processor.analysis.JavaLangAnalyser; import org.javahelpers.simple.builders.processor.analysis.JavaLangMapper; +import org.javahelpers.simple.builders.processor.classgen.ClassCodeGenerator; +import org.javahelpers.simple.builders.processor.classgen.GenerationEnvironment; import org.javahelpers.simple.builders.processor.classgen.roaster.RoasterCodeGenerator; import org.javahelpers.simple.builders.processor.exceptions.BuilderException; import org.javahelpers.simple.builders.processor.generators.integration.JacksonModuleGenerator; @@ -95,25 +97,36 @@ public class BuilderProcessor extends AbstractProcessor { private ProcessingContext context; private ProcessingLogger logger; - private RoasterCodeGenerator codeGenerator; + private ClassCodeGenerator codeGenerator; private JacksonModuleGenerator jacksonModuleGenerator; private boolean supportedJdk = true; @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); - this.logger = new ProcessingLogger(processingEnv); - logger.debug("Starting BuilderProcessor..."); - // Read global configuration from compiler arguments CompilerArgumentsReader reader = new CompilerArgumentsReader(processingEnv); - BuilderConfiguration globalConfig = reader.readBuilderConfiguration(logger); + this.logger = + reader.readBooleanValue(CompilerArgumentsEnum.VERBOSE) + ? ProcessingLogger.verbose(processingEnv) + : new ProcessingLogger(processingEnv); + logger.debug("Starting BuilderProcessor..."); + + BuilderConfiguration globalConfig = + BuilderConfigurationReader.readBuilderConfiguration(reader, logger); logger.debug("Loaded global configuration from compiler arguments: %s", globalConfig); SimpleBuildersSpiIntegration.initCompilation(processingEnv.getElementUtils()); this.context = new ProcessingContext(logger, globalConfig, processingEnv); - this.codeGenerator = new RoasterCodeGenerator(context, processingEnv); + this.codeGenerator = + new RoasterCodeGenerator( + new GenerationEnvironment( + processingEnv, + logger, + context.getPerformanceTracker(), + context::reportBasedOnStrictMode), + context::createSourceFormatter); this.jacksonModuleGenerator = new JacksonModuleGenerator(processingEnv, logger, globalConfig); // Initialize GeneratorRegistry once during processor initialization diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangAnalyser.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangAnalyser.java index 95a584b7..32bc04fb 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangAnalyser.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangAnalyser.java @@ -49,7 +49,6 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Strings; -import org.javahelpers.simple.builders.core.annotations.SimpleBuilderConstructor; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.processing.ProcessingContext; @@ -847,24 +846,28 @@ public static Optional findSetterForField( /** * Determines which constructor to use for builder initialization. Prioritizes constructors - * annotated with {@link SimpleBuilderConstructor}. If none is annotated, selects the constructor + * annotated with the given constructor annotation. If none is annotated, selects the constructor * with the highest number of parameters. Returns empty if no constructor has parameters (i.e., * only default constructor or none found). * * @param annotatedType the type element to search for constructors + * @param constructorAnnotation the marker annotation selecting a constructor explicitly; may be + * {@code null} to skip the annotation check * @param context the processing context providing access to elements and types utilities * @return Optional containing the selected constructor, or empty if none suitable */ public static Optional findConstructorForBuilder( - TypeElement annotatedType, ProcessingContext context) { + TypeElement annotatedType, + Class constructorAnnotation, + ProcessingContext context) { List ctors = ElementFilter.constructorsIn(context.getAllMembers(annotatedType)).stream() .filter(context::isMemberAccessibleFromBuilderPackage) .toList(); - // First, check if any constructor is annotated with @SimpleBuilderConstructor + // First, check if any constructor is annotated with the constructor annotation for (ExecutableElement ctor : ctors) { - if (ctor.getAnnotation(SimpleBuilderConstructor.class) != null) { + if (constructorAnnotation != null && ctor.getAnnotation(constructorAnnotation) != null) { return Optional.of(ctor); } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangMapper.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangMapper.java index cd15fa12..5bd56824 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangMapper.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/JavaLangMapper.java @@ -74,13 +74,14 @@ public static TypeName map2TypeName(Class clazz) { * Maps the declared type parameters of the given type element into GenericParameterDto list. * * @param type the type element whose type parameters to map + * @param options the mapper options * @param context the processing context * @return list of GenericParameterDto representing the type parameters */ public static List map2GenericParameterDtos( - TypeElement type, ProcessingContext context) { + TypeElement type, MapperOptions options, ProcessingContext context) { return type.getTypeParameters().stream() - .map(tp -> map2GenericParameterDto(tp, context)) + .map(tp -> map2GenericParameterDto(tp, options, context)) .toList(); } @@ -88,16 +89,17 @@ public static List map2GenericParameterDtos( * Maps a single {@code TypeParameterElement} to {@code GenericParameterDto}. * * @param tp the type parameter element to map + * @param options the mapper options * @param context the processing context * @return a GenericParameterDto representing the type parameter */ public static GenericParameterDto map2GenericParameterDto( - TypeParameterElement tp, ProcessingContext context) { + TypeParameterElement tp, MapperOptions options, ProcessingContext context) { GenericParameterDto g = new GenericParameterDto(); g.setName(tp.getSimpleName().toString()); tp.getBounds().stream() .filter(b -> !"java.lang.Object".equals(b.toString())) - .map(b -> extractType(b, context)) + .map(b -> extractType(b, options, context)) .forEach(g::addUpperBound); return g; } @@ -107,22 +109,23 @@ public static GenericParameterDto map2GenericParameterDto( * org.javahelpers.simple.builders.processor.dtos.MethodParameterDto}. * * @param param Parameter variable to be mapped + * @param options the mapper options * @param context processing context * @return MethodParameterDto holding the information of the param. */ public static MethodParameterDto map2MethodParameter( - VariableElement param, ProcessingContext context) { + VariableElement param, MapperOptions options, ProcessingContext context) { MethodParameterDto result = new MethodParameterDto(); result.setParameterName(param.getSimpleName().toString()); TypeMirror typeMirror = param.asType(); - TypeName typeName = extractType(typeMirror, context); + TypeName typeName = extractType(typeMirror, options, context); if (typeName == null) { return null; } // Set builder and constructor information on the TypeName (only if not already set) if (!typeName.getBuilderType().isPresent()) { - setBuilderAndConstructorInfo(typeName, param, context); + setBuilderAndConstructorInfo(typeName, param, options, context); } result.setParameterTypeName(typeName); @@ -151,20 +154,22 @@ public static TypeName mapToTypeName(TypeElement typeElement, ProcessingContext * Maps a {@code TypeElement} to a simple-builder {@code TypeName}. * * @param typeElement the TypeElement to map + * @param options the mapper options * @param context the processing context * @return TypeName holding the information of the type element, or null if mapping fails */ - public static TypeName map2TypeName(TypeElement typeElement, ProcessingContext context) { + public static TypeName map2TypeName( + TypeElement typeElement, MapperOptions options, ProcessingContext context) { if (typeElement == null) { return null; } TypeMirror typeMirror = typeElement.asType(); - TypeName typeName = extractType(typeMirror, context); + TypeName typeName = extractType(typeMirror, options, context); // Set builder and constructor information on the TypeName (only if not already set) if (typeName != null && !typeName.getBuilderType().isPresent()) { - setBuilderAndConstructorInfo(typeName, typeElement, context); + setBuilderAndConstructorInfo(typeName, typeElement, options, context); } return typeName; @@ -175,13 +180,17 @@ public static TypeName map2TypeName(TypeElement typeElement, ProcessingContext c * * @param typeName the TypeName to enhance with builder/constructor info * @param typeElement the TypeElement representing the type + * @param options the mapper options * @param context the processing context */ private static void setBuilderAndConstructorInfo( - TypeName typeName, TypeElement typeElement, ProcessingContext context) { - setBuilderTypeIfScopeMatches(typeName, typeElement, context); + TypeName typeName, + TypeElement typeElement, + MapperOptions options, + ProcessingContext context) { + setBuilderTypeIfScopeMatches(typeName, typeElement, options); setEmptyConstructorInfoIfAvailable(typeName, typeElement, context); - setElementBuilderTypeIfScopeMatches(typeName, context); + setElementBuilderTypeIfScopeMatches(typeName, options, context); } /** @@ -189,14 +198,11 @@ private static void setBuilderAndConstructorInfo( * * @param typeName the TypeName to enhance * @param typeElement the type element to check - * @param context the processing context + * @param options the mapper options */ private static void setBuilderTypeIfScopeMatches( - TypeName typeName, TypeElement typeElement, ProcessingContext context) { - context - .getBuilderScopeResolver() - .resolveUsableBuilderType(typeElement) - .ifPresent(typeName::setResolvedBuilder); + TypeName typeName, TypeElement typeElement, MapperOptions options) { + options.resolveGeneratedType(typeElement).ifPresent(typeName::setResolvedBuilder); } /** @@ -220,10 +226,11 @@ private static void setEmptyConstructorInfoIfAvailable( * configured builder scope. * * @param typeName the TypeName to enhance + * @param options the mapper options * @param context the processing context */ private static void setElementBuilderTypeIfScopeMatches( - TypeName typeName, ProcessingContext context) { + TypeName typeName, MapperOptions options, ProcessingContext context) { // Only process generic types if (!(typeName instanceof TypeNameGeneric genericType)) { return; @@ -243,10 +250,9 @@ private static void setElementBuilderTypeIfScopeMatches( return; } - // Resolve usable element builder type through the scope resolver - context - .getBuilderScopeResolver() - .resolveUsableBuilderType(elementTypeElement) + // Resolve usable element builder type through the generated type resolver + options + .resolveGeneratedType(elementTypeElement) .ifPresent(genericType::setElementResolvedBuilder); } @@ -263,18 +269,6 @@ private static TypeElement retrieveTypeElementIfExists( return element instanceof TypeElement typeElement ? typeElement : null; } - /** - * Creates a TypeName for the builder of a given TypeElement using the configured builder suffix. - * - * @param typeElement the type element to create builder name for - * @param context the processing context - * @return the TypeName for the builder - */ - public static TypeName createBuilderTypeName(TypeElement typeElement, ProcessingContext context) { - return createBuilderTypeName( - typeElement, context, context.getConfiguration().getBuilderSuffix()); - } - /** * Creates a TypeName for the builder of a given TypeElement using an explicit suffix. * @@ -295,13 +289,14 @@ public static TypeName createBuilderTypeName( * * @param typeName the TypeName to enhance with builder/constructor info * @param param the VariableElement representing the parameter + * @param options the mapper options * @param context the processing context */ private static void setBuilderAndConstructorInfo( - TypeName typeName, VariableElement param, ProcessingContext context) { + TypeName typeName, VariableElement param, MapperOptions options, ProcessingContext context) { Element element = context.asElement(param.asType()); if (element instanceof TypeElement typeElement) { - setBuilderAndConstructorInfo(typeName, typeElement, context); + setBuilderAndConstructorInfo(typeName, typeElement, options, context); } } @@ -310,10 +305,10 @@ private static void setBuilderAndConstructorInfo( * #extractType(TypeMirror, ProcessingContext)}. */ private static List extractTypeForList( - List typeMirrors, ProcessingContext context) { + List typeMirrors, MapperOptions options, ProcessingContext context) { List result = new ArrayList<>(typeMirrors.size()); for (TypeMirror tm : typeMirrors) { - result.add(extractType(tm, context)); + result.add(extractType(tm, options, context)); } return result; } @@ -329,23 +324,28 @@ private static List extractTypeForList( * @param rawType the base TypeName (preserves concrete class information) * @param argTypes the generic type arguments from the class declaration (may not match interface) * @param typeMirror the TypeMirror to check for interface implementation + * @param options the mapper options * @param context the processing context * @return a specialized TypeName (TypeNameList, TypeNameSet, TypeNameMap) if applicable, or a * TypeNameGeneric/rawType otherwise */ private static TypeName wrapInCollectionTypeIfApplicable( - TypeName rawType, List argTypes, TypeMirror typeMirror, ProcessingContext context) { - TypeName listWrapper = tryWrapAsList(rawType, argTypes, typeMirror, context); + TypeName rawType, + List argTypes, + TypeMirror typeMirror, + MapperOptions options, + ProcessingContext context) { + TypeName listWrapper = tryWrapAsList(rawType, argTypes, typeMirror, options, context); if (listWrapper != null) { return listWrapper; } - TypeName setWrapper = tryWrapAsSet(rawType, argTypes, typeMirror, context); + TypeName setWrapper = tryWrapAsSet(rawType, argTypes, typeMirror, options, context); if (setWrapper != null) { return setWrapper; } - TypeName mapWrapper = tryWrapAsMap(rawType, argTypes, typeMirror, context); + TypeName mapWrapper = tryWrapAsMap(rawType, argTypes, typeMirror, options, context); if (mapWrapper != null) { return mapWrapper; } @@ -361,7 +361,11 @@ private static TypeName wrapInCollectionTypeIfApplicable( * @return TypeNameList if applicable, null otherwise */ private static TypeName tryWrapAsList( - TypeName rawType, List argTypes, TypeMirror typeMirror, ProcessingContext context) { + TypeName rawType, + List argTypes, + TypeMirror typeMirror, + MapperOptions options, + ProcessingContext context) { TypeElement listElement = context.getTypeElement("java.util.List"); if (listElement == null) { return null; @@ -378,7 +382,7 @@ private static TypeName tryWrapAsList( } List interfaceTypeArgs = - extractInterfaceTypeArguments(typeMirror, listElement, context); + extractInterfaceTypeArguments(typeMirror, listElement, options, context); TypeName elementType = interfaceTypeArgs.isEmpty() ? null : interfaceTypeArgs.get(0); return new TypeNameList(rawType, argTypes, elementType); } @@ -390,7 +394,11 @@ private static TypeName tryWrapAsList( * @return TypeNameSet if applicable, null otherwise */ private static TypeName tryWrapAsSet( - TypeName rawType, List argTypes, TypeMirror typeMirror, ProcessingContext context) { + TypeName rawType, + List argTypes, + TypeMirror typeMirror, + MapperOptions options, + ProcessingContext context) { TypeElement setElement = context.getTypeElement("java.util.Set"); if (setElement == null) { return null; @@ -407,7 +415,7 @@ private static TypeName tryWrapAsSet( } List interfaceTypeArgs = - extractInterfaceTypeArguments(typeMirror, setElement, context); + extractInterfaceTypeArguments(typeMirror, setElement, options, context); TypeName elementType = interfaceTypeArgs.isEmpty() ? null : interfaceTypeArgs.get(0); return new TypeNameSet(rawType, argTypes, elementType); } @@ -419,7 +427,11 @@ private static TypeName tryWrapAsSet( * @return TypeNameMap if applicable, null otherwise */ private static TypeName tryWrapAsMap( - TypeName rawType, List argTypes, TypeMirror typeMirror, ProcessingContext context) { + TypeName rawType, + List argTypes, + TypeMirror typeMirror, + MapperOptions options, + ProcessingContext context) { TypeElement mapElement = context.getTypeElement("java.util.Map"); if (mapElement == null) { return null; @@ -440,7 +452,7 @@ private static TypeName tryWrapAsMap( } List interfaceTypeArgs = - extractInterfaceTypeArguments(typeMirror, mapElement, context); + extractInterfaceTypeArguments(typeMirror, mapElement, options, context); TypeName keyType = interfaceTypeArgs.isEmpty() ? null : interfaceTypeArgs.get(0); TypeName valueType = interfaceTypeArgs.size() < 2 ? null : interfaceTypeArgs.get(1); return new TypeNameMap(rawType, argTypes, keyType, valueType); @@ -520,18 +532,22 @@ private static boolean hasConstructorWithParameterOfType( * * @param typeMirror the type to examine * @param targetInterface the interface element (e.g., List, Set, Map) + * @param options the mapper options * @param context the processing context * @return list of type arguments used by the interface, or empty list if raw type */ public static List extractInterfaceTypeArguments( - TypeMirror typeMirror, TypeElement targetInterface, ProcessingContext context) { + TypeMirror typeMirror, + TypeElement targetInterface, + MapperOptions options, + ProcessingContext context) { // Walk the supertype hierarchy to find the specific instantiation of the target interface TypeMirror found = findSupertype(typeMirror, targetInterface, context); if (found instanceof DeclaredType declaredType) { List typeArgs = declaredType.getTypeArguments(); if (!typeArgs.isEmpty()) { - return extractTypeForList(new ArrayList<>(typeArgs), context); + return extractTypeForList(new ArrayList<>(typeArgs), options, context); } } @@ -568,7 +584,8 @@ private static TypeMirror findSupertype( return null; } - private static TypeName extractType(TypeMirror typeOfParameter, ProcessingContext context) { + private static TypeName extractType( + TypeMirror typeOfParameter, MapperOptions options, ProcessingContext context) { TypeName typeName = typeOfParameter.accept( new SimpleTypeVisitor14() { @@ -606,23 +623,23 @@ public TypeName visitDeclared(DeclaredType t, Void p) { : null; if (t.getTypeArguments().isEmpty() && !(enclosing instanceof TypeNameGeneric)) { return wrapInCollectionTypeIfApplicable( - rawType, List.of(), typeOfParameter, context); + rawType, List.of(), typeOfParameter, options, context); } List typesExtracted = new ArrayList<>(t.getTypeArguments()); if (typesExtracted.isEmpty()) { return wrapInCollectionTypeIfApplicable( - rawType, List.of(), typeOfParameter, context); + rawType, List.of(), typeOfParameter, options, context); } else { - List argTypes = extractTypeForList(typesExtracted, context); + List argTypes = extractTypeForList(typesExtracted, options, context); return wrapInCollectionTypeIfApplicable( - rawType, argTypes, typeOfParameter, context); + rawType, argTypes, typeOfParameter, options, context); } } @Override public TypeNameArray visitArray(ArrayType t, Void p) { - return new TypeNameArray(extractType(t.getComponentType(), context)); + return new TypeNameArray(extractType(t.getComponentType(), options, context)); } @Override @@ -638,7 +655,7 @@ protected TypeName defaultAction(TypeMirror e, Void p) { }, null); - if (typeName != null && context.getConfiguration().shouldCopyTypeAnnotations()) { + if (typeName != null && options.copyTypeAnnotations()) { List annotations = FieldAnnotationExtractor.extractAnnotations(typeOfParameter, context); annotations.forEach(typeName::addAnnotation); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/MapperOptions.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/MapperOptions.java new file mode 100644 index 00000000..8a28374f --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/analysis/MapperOptions.java @@ -0,0 +1,58 @@ +/* + * MIT License + * + * Copyright (c) 2025-2026 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.processor.analysis; + +import java.util.Optional; +import java.util.function.Function; +import javax.lang.model.element.TypeElement; +import org.javahelpers.simple.builders.processor.model.type.ResolvedBuilder; + +/** + * Options controlling how {@link JavaLangMapper} maps elements to {@code TypeName} instances. + * + * @param copyTypeAnnotations whether annotations declared on a type usage are copied onto the + * mapped {@code TypeName} + * @param generatedTypeResolver resolves a source type element to the generated type usable for it + * (e.g. its builder), or empty when none applies; may be {@code null} to disable resolution + */ +public record MapperOptions( + boolean copyTypeAnnotations, + Function> generatedTypeResolver) { + + /** Default options: no annotation copying, no generated-type resolution. */ + public static final MapperOptions DEFAULT = new MapperOptions(false, null); + + /** + * Resolves the generated type usable for the given type element. + * + * @param typeElement the type element to resolve + * @return the usable generated type, or empty if resolution is disabled or no match exists + */ + public Optional resolveGeneratedType(TypeElement typeElement) { + return generatedTypeResolver != null + ? generatedTypeResolver.apply(typeElement) + : Optional.empty(); + } +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/ClassCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/ClassCodeGenerator.java new file mode 100644 index 00000000..2641ce9b --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/ClassCodeGenerator.java @@ -0,0 +1,47 @@ +/* + * MIT License + * + * Copyright (c) 2025-2026 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.processor.classgen; + +import org.javahelpers.simple.builders.processor.exceptions.BuilderException; +import org.javahelpers.simple.builders.processor.model.core.GenerationTargetClassDto; + +/** + * SPI for class code generators: turns a {@link GenerationTargetClassDto} into source code and + * writes it through the processing environment's filer. + * + *

Implementations are backend-specific (e.g. Roaster, JavaPoet) and are instantiated with a + * {@link GenerationEnvironment} providing the processing environment, logger, and performance + * tracker. + */ +public interface ClassCodeGenerator { + + /** + * Generates a class from the given class definition and writes the source file. + * + * @param classDef DTO containing all information to create the class + * @throws BuilderException if there is an error in source code generation + */ + void generateClass(GenerationTargetClassDto classDef) throws BuilderException; +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/GenerationEnvironment.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/GenerationEnvironment.java new file mode 100644 index 00000000..d8753a45 --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/GenerationEnvironment.java @@ -0,0 +1,97 @@ +/* + * MIT License + * + * Copyright (c) 2025-2026 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.processor.classgen; + +import javax.annotation.processing.ProcessingEnvironment; +import org.javahelpers.simple.builders.processor.processing.logging.PerformanceTracker; +import org.javahelpers.simple.builders.processor.processing.logging.ProcessingLogger; + +/** + * Environment handed to a {@link ClassCodeGenerator}: the annotation processing environment, the + * logger, the performance tracker and the reporter for generation problems. + */ +public class GenerationEnvironment { + + private final ProcessingEnvironment processingEnvironment; + private final ProcessingLogger logger; + private final PerformanceTracker performanceTracker; + private final GenerationIssueReporter issueReporter; + + /** + * Creates a new generation environment. + * + * @param processingEnvironment the annotation processing environment + * @param logger the logger for debug output during code generation + * @param performanceTracker the tracker for sub-phase timing + * @param issueReporter the reporter for problems found during generation; the reporter decides + * the severity + */ + public GenerationEnvironment( + ProcessingEnvironment processingEnvironment, + ProcessingLogger logger, + PerformanceTracker performanceTracker, + GenerationIssueReporter issueReporter) { + this.processingEnvironment = processingEnvironment; + this.logger = logger; + this.performanceTracker = performanceTracker; + this.issueReporter = issueReporter; + } + + /** + * Returns the annotation processing environment. + * + * @return the processing environment + */ + public ProcessingEnvironment getProcessingEnvironment() { + return processingEnvironment; + } + + /** + * Returns the logger for debug output during code generation. + * + * @return the logger + */ + public ProcessingLogger getLogger() { + return logger; + } + + /** + * Returns the performance tracker for sub-phase timing. + * + * @return the tracker + */ + public PerformanceTracker getPerformanceTracker() { + return performanceTracker; + } + + /** + * Returns the reporter for problems found during generation. + * + * @return the issue reporter + */ + public GenerationIssueReporter getIssueReporter() { + return issueReporter; + } +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/GenerationIssueReporter.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/GenerationIssueReporter.java new file mode 100644 index 00000000..ea2f947a --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/GenerationIssueReporter.java @@ -0,0 +1,42 @@ +/* + * MIT License + * + * Copyright (c) 2025-2026 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.processor.classgen; + +/** + * Reports a problem found during code generation. The receiving side decides the severity — e.g. an + * unexpected generated-code inconsistency may be a warning in normal operation and an error in a + * caller-defined strict mode. + */ +@FunctionalInterface +public interface GenerationIssueReporter { + + /** + * Reports a problem found during code generation. + * + * @param format the message format string + * @param args arguments referenced by the format specifiers in the format string + */ + void report(String format, Object... args); +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/GenerationPhases.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/GenerationPhases.java new file mode 100644 index 00000000..0049f8f6 --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/GenerationPhases.java @@ -0,0 +1,52 @@ +/* + * MIT License + * + * Copyright (c) 2025-2026 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.processor.classgen; + +/** + * Phase names a {@link ClassCodeGenerator} may report through {@code PerformanceTracker.endPhase} + * while turning a class definition into a source file. + */ +public final class GenerationPhases { + + // Code Generation children + public static final String PHASE_SOURCE_CONSTRUCTION = "Source Construction"; + public static final String PHASE_FILE_WRITING = "File Writing"; + + // Source Construction children + public static final String PHASE_ELEMENT_BUILDING = "Element Building"; + public static final String PHASE_STRING_GENERATION = "String Generation"; + public static final String PHASE_FORMATTING = "Formatting"; + + // Element Building children + public static final String PHASE_CLASS_CREATION = "Class Creation"; + public static final String PHASE_CLASS_METADATA = "Class Metadata"; + public static final String PHASE_FIELDS = "Fields"; + public static final String PHASE_CONSTRUCTORS = "Constructors"; + public static final String PHASE_METHODS = "Methods"; + public static final String PHASE_NESTED_TYPES = "Nested Types"; + public static final String PHASE_CLASS_ANNOTATIONS = "Class Annotations"; + + private GenerationPhases() {} +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java index 35a552cd..d1e61e03 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java @@ -24,9 +24,9 @@ package org.javahelpers.simple.builders.processor.classgen.roaster; +import static org.javahelpers.simple.builders.processor.classgen.GenerationPhases.*; import static org.javahelpers.simple.builders.processor.classgen.roaster.RoasterMapper.mapType; import static org.javahelpers.simple.builders.processor.classgen.roaster.RoasterMapper.resolveCodeTemplate; -import static org.javahelpers.simple.builders.processor.processing.logging.PerformanceTracker.*; import java.io.IOException; import java.io.Writer; @@ -46,6 +46,9 @@ import org.apache.commons.lang3.StringUtils; import org.javahelpers.simple.builders.core.enums.AccessModifier; import org.javahelpers.simple.builders.core.enums.FormattingMode; +import org.javahelpers.simple.builders.processor.classgen.ClassCodeGenerator; +import org.javahelpers.simple.builders.processor.classgen.GenerationEnvironment; +import org.javahelpers.simple.builders.processor.classgen.GenerationIssueReporter; import org.javahelpers.simple.builders.processor.exceptions.BuilderException; import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto; import org.javahelpers.simple.builders.processor.model.annotation.InterfaceName; @@ -62,7 +65,8 @@ import org.javahelpers.simple.builders.processor.model.type.NestedTypeDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.model.type.TypeNameArray; -import org.javahelpers.simple.builders.processor.processing.ProcessingContext; +import org.javahelpers.simple.builders.processor.processing.logging.PerformanceTracker; +import org.javahelpers.simple.builders.processor.processing.logging.ProcessingLogger; import org.javahelpers.simple.builders.processor.util.ImportCollector; import org.jboss.forge.roaster.Roaster; import org.jboss.forge.roaster.model.source.AnnotationSource; @@ -75,27 +79,47 @@ import org.jboss.forge.roaster.model.source.TypeVariableSource; /** Roaster-based code generator for builder source files. */ -public class RoasterCodeGenerator { - /** Processing context providing logger, performance tracking, type lookup and profile. */ - private final ProcessingContext context; - - /** Processing environment for creating generated source files via its filer. */ +public class RoasterCodeGenerator implements ClassCodeGenerator { + /** Processing environment for accessing filer and element utilities. */ private final ProcessingEnvironment processingEnv; + /** Logger for debug output during code generation. */ + private final ProcessingLogger logger; + + /** Performance tracker for sub-phase timing (Source Construction, File Writing). */ + private final PerformanceTracker performanceTracker; + + /** Reporter for problems found during generation; the reporter decides the severity. */ + private final GenerationIssueReporter issueReporter; + + /** Factory for creating a source formatter per formatting mode. */ + private final RoasterSourceFormatterFactory formatterFactory; + /** Cached formatters per formatting mode (at most 3 instances, created lazily). */ private final EnumMap formatterCache = new EnumMap<>(FormattingMode.class); /** - * Creates a code generator using the given processing context and environment. + * Creates a code generator using the given generation environment and formatter factory. * - * @param context processing context providing logger, performance tracking, type lookup and - * formatter profile - * @param processingEnv processing environment providing the filer for generated source files + * @param environment the generation environment providing processing environment, logger, + * performance tracker and issue reporter + * @param formatterFactory factory creating a {@link RoasterSourceFormatter} per formatting mode, + * applying any configured formatter profile */ - public RoasterCodeGenerator(ProcessingContext context, ProcessingEnvironment processingEnv) { - this.context = Objects.requireNonNull(context, "context must not be null"); - this.processingEnv = Objects.requireNonNull(processingEnv, "processingEnv must not be null"); + public RoasterCodeGenerator( + GenerationEnvironment environment, RoasterSourceFormatterFactory formatterFactory) { + this.processingEnv = + Objects.requireNonNull( + environment.getProcessingEnvironment(), "processingEnv must not be null"); + this.logger = Objects.requireNonNull(environment.getLogger(), "logger must not be null"); + this.performanceTracker = + Objects.requireNonNull( + environment.getPerformanceTracker(), "performanceTracker must not be null"); + this.issueReporter = + Objects.requireNonNull(environment.getIssueReporter(), "issueReporter must not be null"); + this.formatterFactory = + Objects.requireNonNull(formatterFactory, "formatterFactory must not be null"); } /** @@ -108,7 +132,7 @@ public RoasterCodeGenerator(ProcessingContext context, ProcessingEnvironment pro * @return a cached or new formatter instance */ private RoasterSourceFormatter getFormatter(FormattingMode mode) { - return formatterCache.computeIfAbsent(mode, context::createSourceFormatter); + return formatterCache.computeIfAbsent(mode, formatterFactory::create); } /** @@ -117,42 +141,43 @@ private RoasterSourceFormatter getFormatter(FormattingMode mode) { * @param classDef DTO containing all information to create the class * @throws BuilderException if there is an error in source code generation */ + @Override public void generateClass(GenerationTargetClassDto classDef) throws BuilderException { - context.debugStartOperation( + logger.debugStartOperation( "Code generation for class: %s", classDef.getTypeName().getClassName()); String sourceCode; try { - context.startPerformancePhase(); + performanceTracker.startPhase(); JavaClassSource source = buildClassSource(classDef); - context.startPerformancePhase(); + performanceTracker.startPhase(); String unformatted = source.toUnformattedString(); - context.endPerformancePhase(PHASE_STRING_GENERATION); - context.startPerformancePhase(); + performanceTracker.endPhase(PHASE_STRING_GENERATION); + performanceTracker.startPhase(); sourceCode = formatSource(unformatted, classDef.getFormattingMode()); // Roaster renders some java.lang annotations (e.g. @SuppressWarnings, @Deprecated with // members) with their FQN (@java.lang.SuppressWarnings) even though java.lang types don't // need qualification. Fix this by replacing @java.lang.Xxx with @Xxx for known annotations. sourceCode = sourceCode.replace("@java.lang.SuppressWarnings", "@SuppressWarnings"); sourceCode = sourceCode.replace("@java.lang.Deprecated", "@Deprecated"); - context.endPerformancePhase(PHASE_FORMATTING); - context.endPerformancePhase(PHASE_SOURCE_CONSTRUCTION); + performanceTracker.endPhase(PHASE_FORMATTING); + performanceTracker.endPhase(PHASE_SOURCE_CONSTRUCTION); } catch (RuntimeException ex) { // Rendering failures (e.g. RoasterMapperException) are RuntimeExceptions. Convert them into // a BuilderException so callers can isolate the failure to this single class and keep // generating the remaining builders instead of aborting the whole processing round. throw new BuilderException(null, ex); } - context.startPerformancePhase(); + performanceTracker.startPhase(); writeClassToFile(sourceCode, classDef); - context.endPerformancePhase(PHASE_FILE_WRITING); + performanceTracker.endPhase(PHASE_FILE_WRITING); - context.debugEndOperation( + logger.debugEndOperation( "Successfully generated class: %s", classDef.getTypeName().getClassName()); } private JavaClassSource buildClassSource(GenerationTargetClassDto classDef) { - context.startPerformancePhase(); + performanceTracker.startPhase(); JavaClassSource source = createJavaClassSource(classDef); addClassMetadata(source, classDef); appendFields(source, classDef); @@ -160,24 +185,24 @@ private JavaClassSource buildClassSource(GenerationTargetClassDto classDef) { appendMethods(source, classDef); appendNestedTypes(source, classDef); applyClassAnnotations(source, classDef); - context.endPerformancePhase(PHASE_ELEMENT_BUILDING); + performanceTracker.endPhase(PHASE_ELEMENT_BUILDING); return source; } private void applyClassAnnotations(JavaClassSource source, GenerationTargetClassDto classDef) { - context.startPerformancePhase(); + performanceTracker.startPhase(); if (CollectionUtils.isNotEmpty(classDef.getClassAnnotations())) { // Adding class annotations applyAnnotations(source, classDef.getClassAnnotations()); - context.debug("Class-level annotations added"); + logger.debug("Class-level annotations added"); } - context.endPerformancePhase(PHASE_CLASS_ANNOTATIONS); + performanceTracker.endPhase(PHASE_CLASS_ANNOTATIONS); } private JavaClassSource createJavaClassSource(GenerationTargetClassDto classDef) { - context.startPerformancePhase(); + performanceTracker.startPhase(); if (CollectionUtils.isNotEmpty(classDef.getGenerics())) { - context.debug("Class has %d generic type parameter(s)", classDef.getGenerics().size()); + logger.debug("Class has %d generic type parameter(s)", classDef.getGenerics().size()); } JavaClassSource source = Roaster.create(JavaClassSource.class); @@ -200,13 +225,13 @@ private JavaClassSource createJavaClassSource(GenerationTargetClassDto classDef) } } - context.debug("JavaClassSource created"); - context.endPerformancePhase(PHASE_CLASS_CREATION); + logger.debug("JavaClassSource created"); + performanceTracker.endPhase(PHASE_CLASS_CREATION); return source; } private void addClassMetadata(JavaClassSource source, GenerationTargetClassDto classDef) { - context.startPerformancePhase(); + performanceTracker.startPhase(); applyJavadoc(source, classDef.getClassJavadoc()); applyVisibility(source, classDef.getClassAccessModifier()); applySuperType(source, classDef.getSuperType()); @@ -216,20 +241,20 @@ private void addClassMetadata(JavaClassSource source, GenerationTargetClassDto c source.addInterface(RoasterMapper.mapInterfaceToTypeName(interfaceName)); } - context.debug("Class metadata added"); - context.endPerformancePhase(PHASE_CLASS_METADATA); + logger.debug("Class metadata added"); + performanceTracker.endPhase(PHASE_CLASS_METADATA); } private void appendFields(JavaClassSource source, GenerationTargetClassDto classDef) { - context.startPerformancePhase(); - context.debugStartOperation("Generating %d fields", classDef.getClassFields().size()); + performanceTracker.startPhase(); + logger.debugStartOperation("Generating %d fields", classDef.getClassFields().size()); for (ClassFieldDto fieldDto : classDef.getClassFields()) { appendField(source, fieldDto); } - context.debugEndOperation("Fields added: %d fields", source.getFields().size()); - context.endPerformancePhase(PHASE_FIELDS); + logger.debugEndOperation("Fields added: %d fields", source.getFields().size()); + performanceTracker.endPhase(PHASE_FIELDS); } private void appendField(JavaClassSource source, ClassFieldDto fieldDto) { @@ -242,15 +267,15 @@ private void appendField(JavaClassSource source, ClassFieldDto fieldDto) { } private void appendConstructors(JavaClassSource source, GenerationTargetClassDto classDef) { - context.startPerformancePhase(); - context.debugStartOperation("Generating %d constructors", classDef.getConstructors().size()); + performanceTracker.startPhase(); + logger.debugStartOperation("Generating %d constructors", classDef.getConstructors().size()); for (ConstructorDto constructor : classDef.getConstructors()) { appendConstructor(source, constructor); } - context.debugEndOperation("Constructors added: %d", classDef.getConstructors().size()); - context.endPerformancePhase(PHASE_CONSTRUCTORS); + logger.debugEndOperation("Constructors added: %d", classDef.getConstructors().size()); + performanceTracker.endPhase(PHASE_CONSTRUCTORS); } private void appendConstructor(JavaClassSource source, ConstructorDto constructor) { @@ -266,19 +291,19 @@ private void appendConstructor(JavaClassSource source, ConstructorDto constructo } private void appendMethods(JavaClassSource source, GenerationTargetClassDto classDef) { - context.startPerformancePhase(); - context.debugStartOperation("Generating %d method candidates", classDef.getMethods().size()); + performanceTracker.startPhase(); + logger.debugStartOperation("Generating %d method candidates", classDef.getMethods().size()); // Resolve method conflicts by signature and priority List resolvedMethods = resolveMethodConflicts(classDef.getMethods()); - context.debug("Resolved to %d methods after conflict resolution", resolvedMethods.size()); + logger.debug("Resolved to %d methods after conflict resolution", resolvedMethods.size()); for (MethodDto methodDto : resolvedMethods) { appendMethod(source, methodDto, false, false); } - context.debugEndOperation("Methods added: %d", resolvedMethods.size()); - context.endPerformancePhase(PHASE_METHODS); + logger.debugEndOperation("Methods added: %d", resolvedMethods.size()); + performanceTracker.endPhase(PHASE_METHODS); } /** @@ -301,7 +326,7 @@ private List resolveMethodConflicts(List methods) { if (seenSignatures.add(method.getSignatureKey())) { deduplicated.add(method); } else { - context.reportBasedOnStrictMode( + issueReporter.report( "Unexpected duplicate method signature: '%s' — keeping first occurrence (safety net)", method.getSignatureKey()); } @@ -320,16 +345,16 @@ private void appendMethod( } private void appendNestedTypes(JavaClassSource source, GenerationTargetClassDto classDef) { - context.startPerformancePhase(); + performanceTracker.startPhase(); if (CollectionUtils.isNotEmpty(classDef.getNestedTypes())) { - context.debugStartOperation("Generating %d nested type(s)", classDef.getNestedTypes().size()); + logger.debugStartOperation("Generating %d nested type(s)", classDef.getNestedTypes().size()); for (NestedTypeDto nestedType : classDef.getNestedTypes()) { appendNestedType(source, nestedType); - context.debug("Generated nested type: %s", nestedType.getTypeName()); + logger.debug("Generated nested type: %s", nestedType.getTypeName()); } - context.debugEndOperation("Nested types added"); + logger.debugEndOperation("Nested types added"); } - context.endPerformancePhase(PHASE_NESTED_TYPES); + performanceTracker.endPhase(PHASE_NESTED_TYPES); } private void appendNestedType(JavaClassSource source, NestedTypeDto nestedType) { @@ -562,7 +587,7 @@ private String formatSource(String rawSource, FormattingMode mode) { private void writeClassToFile(String sourceCode, GenerationTargetClassDto classDef) throws BuilderException { - context.debug( + logger.debug( "Writing class to file: %s.%s", classDef.getTypeName().getPackageName(), classDef.getTypeName().getClassName()); @@ -606,10 +631,10 @@ private void writeClassToFile(String sourceCode, GenerationTargetClassDto classD */ private boolean builderClassAlreadyExists(String qualifiedName) { try { - TypeElement existingType = context.getTypeElement(qualifiedName); + TypeElement existingType = processingEnv.getElementUtils().getTypeElement(qualifiedName); return existingType != null; } catch (Exception e) { - context.debug( + logger.debug( "Error checking if builder class '%s' already exists: %s", qualifiedName, StringUtils.isNotBlank(e.getMessage()) ? e.getMessage() : "No message"); return false; diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterSourceFormatterFactory.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterSourceFormatterFactory.java new file mode 100644 index 00000000..96cdbf01 --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterSourceFormatterFactory.java @@ -0,0 +1,40 @@ +/* + * MIT License + * + * Copyright (c) 2025-2026 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.javahelpers.simple.builders.processor.classgen.roaster; + +import org.javahelpers.simple.builders.core.enums.FormattingMode; + +/** Factory for creating a {@link RoasterSourceFormatter} per {@link FormattingMode}. */ +@FunctionalInterface +public interface RoasterSourceFormatterFactory { + + /** + * Creates a source formatter for the given formatting mode, applying any configured formatter + * profile. + * + * @param mode the formatting mode + * @return a new formatter instance + */ + RoasterSourceFormatter create(FormattingMode mode); +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderConfigurationReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderConfigurationReader.java index 01bcd15c..e4789086 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderConfigurationReader.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderConfigurationReader.java @@ -30,6 +30,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.stream.Stream; import javax.lang.model.SourceVersion; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.AnnotationValue; @@ -101,6 +102,57 @@ public BuilderConfiguration getGlobalConfiguration() { return globalConfiguration; } + /** + * Reads a complete BuilderConfiguration from compiler arguments. + * + *

This method reads all configuration options from compiler arguments like: + * + *

    + *
  • {@code -Dsimplebuilder.generateFieldSupplier=true} (JVM system property, highest + * precedence) + *
  • {@code -Asimplebuilder.generateFieldSupplier=true} (compiler argument) + *
  • {@code -AgenerateFieldSupplier=true} (bare option name, backward compatibility) + *
  • {@code -Asimplebuilder.builderAccess=public} + *
  • etc. + *
+ * + *

Options set via {@code @SimpleBuilder.Options} on the annotated type are not handled here; + * they are read by {@link BuilderConfigurationReader} and merged on top of this global + * configuration. + * + *

All values default to UNSET or DEFAULT if not specified in compiler arguments. + * + *

Adding a new option: every {@link CompilerArgumentsEnum} constant is applied; those + * without a builder applier are no-ops in {@link CompilerArgumentsEnum#apply}, so wiring a new + * option means declaring the applier on the enum constant once — no change is needed here. The + * remaining checklist when adding a new option: + * + *

    + *
  1. {@code CompilerArgumentsEnum} — add the enum constant with its applier (or pass {@code + * annotationOnly = true} if the option must not be set globally). + *
  2. {@code BuilderConfiguration} — add the field, builder method, merge logic, and a typed + * accessor (e.g. {@code formattingModeEnum}) if enum conversion is needed. Set the default + * in {@code BuilderConfiguration.DEFAULT}. + *
  3. {@code ProcessingContext} — should NOT need a dedicated field or getter. The resolved + * per-target config ({@code context.getConfiguration()}) and global config ({@code + * context.getConfigurationReader().getGlobalConfiguration()}) carry all option values. + * Special-casing outside {@code BuilderConfiguration} breaks the merge chain and bypasses + * annotation overrides. + *
+ * + * @param reader the compiler arguments reader to read option values through + * @param logger the logger for debug output + * @return a BuilderConfiguration with values read from compiler arguments + */ + public static BuilderConfiguration readBuilderConfiguration( + CompilerArgumentsReader reader, ProcessingLogger logger) { + BuilderConfiguration.Builder builder = BuilderConfiguration.builder(); + Stream.of(CompilerArgumentsEnum.values()) + .filter(option -> !option.isAnnotationOnly()) + .forEach(option -> option.apply(builder, reader.readValue(option), logger)); + return builder.build(); + } + /** * Resolves the complete builder configuration for an element by chaining all configuration * sources in priority order. diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java index 3a8ce4a1..c5763a01 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/BuilderDefinitionCreator.java @@ -46,11 +46,13 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Strings; import org.javahelpers.simple.builders.core.annotations.IgnoreInBuilder; +import org.javahelpers.simple.builders.core.annotations.SimpleBuilderConstructor; import org.javahelpers.simple.builders.core.enums.AccessModifier; import org.javahelpers.simple.builders.core.util.TrackedValue; import org.javahelpers.simple.builders.processor.analysis.FieldAnnotationExtractor; import org.javahelpers.simple.builders.processor.analysis.JavaLangAnalyser; import org.javahelpers.simple.builders.processor.analysis.JavaLangMapper; +import org.javahelpers.simple.builders.processor.analysis.MapperOptions; import org.javahelpers.simple.builders.processor.exceptions.BuilderException; import org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil; import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto; @@ -79,6 +81,18 @@ private BuilderDefinitionCreator() { // Private constructor to prevent instantiation } + /** + * Creates the {@link MapperOptions} used by {@link JavaLangMapper} for this builder run. + * + * @param context the processing context + * @return mapper options derived from the current configuration and scope resolver + */ + private static MapperOptions mapperOptions(ProcessingContext context) { + return new MapperOptions( + context.getConfiguration().shouldCopyTypeAnnotations(), + context.getBuilderScopeResolver()::resolveUsableBuilderType); + } + /** * Extracts a BuilderDefinition from the annotated element. * @@ -469,7 +483,8 @@ private static BuilderDefinitionDto initializeBuilderDefinition( "Builder will be generated as: %s.%s", builderPackageName, simpleClassName + builderSuffix); // Extract generics from the annotated type via mapper (stream-based) - JavaLangMapper.map2GenericParameterDtos(annotatedType, context).forEach(result::addGeneric); + JavaLangMapper.map2GenericParameterDtos(annotatedType, mapperOptions(context), context) + .forEach(result::addGeneric); return result; } @@ -489,7 +504,8 @@ private static List extractConstructorFields( ProcessingContext context, Map fieldNameRegistry) { List constructorFields = new LinkedList<>(); - Optional constructorOpt = findConstructorForBuilder(annotatedType, context); + Optional constructorOpt = + findConstructorForBuilder(annotatedType, SimpleBuilderConstructor.class, context); if (constructorOpt.isPresent()) { ExecutableElement ctor = constructorOpt.get(); context.debugStartOperation( @@ -658,7 +674,7 @@ private static Optional createFieldFromSetter( VariableElement fieldParameter = parameters.get(0); TypeElement dtoTypeElement = (TypeElement) mth.getEnclosingElement(); - TypeName dtoType = JavaLangMapper.map2TypeName(dtoTypeElement, context); + TypeName dtoType = JavaLangMapper.map2TypeName(dtoTypeElement, mapperOptions(context), context); // Extract only the @param Javadoc for the single setter parameter (if present) String fullJavaDoc = context.getDocComment(mth); @@ -712,7 +728,7 @@ private static Optional createFieldFromConstructor( JavaLangAnalyser.extractParamJavaDoc(context.getDocComment(annotatedType), param); // Convert TypeElement to TypeName once - TypeName dtoType = JavaLangMapper.map2TypeName(annotatedType, context); + TypeName dtoType = JavaLangMapper.map2TypeName(annotatedType, mapperOptions(context), context); // Check for field name conflicts and rename if necessary String finalFieldName = resolveFieldNameConflict(fieldName, param, fieldNameRegistry, context); @@ -753,7 +769,7 @@ private static String resolveFieldNameConflict( } // Conflict detected: rename the new field by appending the simple type name - MethodParameterDto paramDto = map2MethodParameter(param, context); + MethodParameterDto paramDto = map2MethodParameter(param, mapperOptions(context), context); if (paramDto == null) { // If we can't determine the type, just return the original name return fieldName; @@ -805,7 +821,7 @@ private static Optional createFieldDto( TypeName dtoType, TypeName builderType, ProcessingContext context) { - MethodParameterDto paramDto = map2MethodParameter(param, context); + MethodParameterDto paramDto = map2MethodParameter(param, mapperOptions(context), context); if (paramDto == null || dtoType == null) { return Optional.empty(); } @@ -1181,7 +1197,8 @@ private static void applyDeprecationSuppressions( */ private static boolean isConstructorDeprecated( TypeElement annotatedType, ProcessingContext context) { - return JavaLangAnalyser.findConstructorForBuilder(annotatedType, context) + return JavaLangAnalyser.findConstructorForBuilder( + annotatedType, SimpleBuilderConstructor.class, context) .filter(ctor -> ctor.getAnnotation(Deprecated.class) != null) .isPresent(); } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/CompilerArgumentsEnum.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/CompilerArgumentsEnum.java index 0374aa4f..66fef459 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/CompilerArgumentsEnum.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/CompilerArgumentsEnum.java @@ -44,7 +44,7 @@ * *

Each enum constant provides the option name and the full compiler argument. */ -public enum CompilerArgumentsEnum { +public enum CompilerArgumentsEnum implements CompilerOption { // === Field Setter Generation === /** Option for field supplier generation. */ GENERATE_FIELD_SUPPLIER("generateFieldSupplier", optionState(Builder::generateSupplier)), diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/CompilerArgumentsReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/CompilerArgumentsReader.java index 5f4ee76d..f2d0dede 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/CompilerArgumentsReader.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/CompilerArgumentsReader.java @@ -25,18 +25,14 @@ package org.javahelpers.simple.builders.processor.processing; import java.util.Map; -import java.util.stream.Stream; import javax.annotation.processing.ProcessingEnvironment; import org.apache.commons.lang3.Strings; -import org.javahelpers.simple.builders.processor.model.core.BuilderConfiguration; -import org.javahelpers.simple.builders.processor.processing.logging.ProcessingLogger; /** * Utility class for reading compiler arguments from the annotation processing environment. * - *

This class provides a centralized way to read compiler arguments using {@link - * CompilerArgumentsEnum} values, ensuring consistent handling of option names and values across the - * processor. + *

This class provides a centralized way to read compiler arguments using {@link CompilerOption} + * values, ensuring consistent handling of option names and values across the processor. */ public class CompilerArgumentsReader { private final ProcessingEnvironment processingEnv; @@ -59,10 +55,10 @@ public CompilerArgumentsReader(ProcessingEnvironment processingEnv) { * property is available when the build tool runs javac in-process and is not available with * {@code true}. * - * @param argument the compiler argument enum to read + * @param argument the compiler option to read * @return the value of the compiler argument, or null if not set */ - public String readValue(CompilerArgumentsEnum argument) { + public String readValue(CompilerOption argument) { return readValue(argument, processingEnv.getOptions()); } @@ -70,11 +66,11 @@ public String readValue(CompilerArgumentsEnum argument) { * Reads the value of a compiler argument from an explicit options map — the same lookup SPI * adapters can use on the options map their host framework hands them. * - * @param argument the compiler argument enum to read + * @param argument the compiler option to read * @param options the compiler options map to search * @return the value of the compiler argument, or null if not set */ - public static String readValue(CompilerArgumentsEnum argument, Map options) { + public static String readValue(CompilerOption argument, Map options) { // Try the -D JVM system property first (e.g., -Dsimplebuilder.verbose) String value = System.getProperty(argument.getCompilerArgument()); @@ -96,10 +92,10 @@ public static String readValue(CompilerArgumentsEnum argument, MapReturns true if the value equals "true" (case-insensitive), false otherwise. * - * @param argument the compiler argument enum to read + * @param argument the compiler option to read * @return true if the value is "true" (case-insensitive), false otherwise */ - public boolean readBooleanValue(CompilerArgumentsEnum argument) { + public boolean readBooleanValue(CompilerOption argument) { return readBooleanValue(argument, processingEnv.getOptions(), false); } @@ -107,14 +103,14 @@ public boolean readBooleanValue(CompilerArgumentsEnum argument) { * Reads the value of a compiler argument as a boolean with an explicit default — the same * resolution SPI adapters can use on the options map their host framework hands them. * - * @param argument the compiler argument enum to read + * @param argument the compiler option to read * @param options the compiler options map to search * @param defaultValue the result for an unset or unrecognizable value * @return "true"/"enabled" resolves to true, "false"/"disabled" to false, anything else to {@code * defaultValue} */ public static boolean readBooleanValue( - CompilerArgumentsEnum argument, Map options, boolean defaultValue) { + CompilerOption argument, Map options, boolean defaultValue) { String value = readValue(argument, options); if (value == null || value.isEmpty()) { return defaultValue; @@ -127,52 +123,4 @@ public static boolean readBooleanValue( } return defaultValue; } - - /** - * Reads a complete BuilderConfiguration from compiler arguments. - * - *

This method reads all configuration options from compiler arguments like: - * - *

    - *
  • {@code -Dsimplebuilder.generateFieldSupplier=true} (JVM system property, highest - * precedence) - *
  • {@code -Asimplebuilder.generateFieldSupplier=true} (compiler argument) - *
  • {@code -AgenerateFieldSupplier=true} (bare option name, backward compatibility) - *
  • {@code -Asimplebuilder.builderAccess=public} - *
  • etc. - *
- * - *

Options set via {@code @SimpleBuilder.Options} on the annotated type are not handled here; - * they are read by {@link BuilderConfigurationReader} and merged on top of this global - * configuration. - * - *

All values default to UNSET or DEFAULT if not specified in compiler arguments. - * - *

Adding a new option: every {@link CompilerArgumentsEnum} constant is applied; those - * without a builder applier are no-ops in {@link CompilerArgumentsEnum#apply}, so wiring a new - * option means declaring the applier on the enum constant once — no change is needed here or in - * {@code BuilderConfigurationReader}. The remaining checklist when adding a new option: - * - *

    - *
  1. {@code CompilerArgumentsEnum} — add the enum constant with its applier (or pass {@code - * annotationOnly = true} if the option must not be set globally). - *
  2. {@code BuilderConfiguration} — add the field, builder method, merge logic, and a typed - * accessor (e.g. {@code formattingModeEnum}) if enum conversion is needed. Set the default - * in {@code BuilderConfiguration.DEFAULT}. - *
  3. {@code ProcessingContext} — should NOT need a dedicated field or getter. The resolved - * per-target config ({@code context.getConfiguration()}) and global config ({@code - * context.getConfigurationReader().getGlobalConfiguration()}) carry all option values. - * Special-casing outside {@code BuilderConfiguration} breaks the merge chain and bypasses - * annotation overrides. - *
- * - * @return a BuilderConfiguration with values read from compiler arguments - */ - public BuilderConfiguration readBuilderConfiguration(ProcessingLogger logger) { - BuilderConfiguration.Builder builder = BuilderConfiguration.builder(); - Stream.of(CompilerArgumentsEnum.values()) - .filter(option -> !option.isAnnotationOnly()) - .forEach(option -> option.apply(builder, readValue(option), logger)); - return builder.build(); - } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/CompilerOption.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/CompilerOption.java new file mode 100644 index 00000000..dbc54d53 --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/CompilerOption.java @@ -0,0 +1,49 @@ +/* + * MIT License + * + * Copyright (c) 2025-2026 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.processor.processing; + +/** + * A compiler option that can be read from the annotation processing environment. + * + *

An option has a bare option name (e.g. {@code "verbose"}) and a prefixed compiler argument + * name (e.g. {@code "simplebuilder.verbose"}) which is looked up as {@code -A} compiler argument + * and as {@code -D} JVM system property. + */ +public interface CompilerOption { + + /** + * Returns the bare option name (e.g. {@code "verbose"}). + * + * @return the option name without prefix + */ + String getOptionName(); + + /** + * Returns the prefixed compiler argument name (e.g. {@code "simplebuilder.verbose"}). + * + * @return the compiler argument name + */ + String getCompilerArgument(); +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingContext.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingContext.java index 1243f36e..910ad7bc 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingContext.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingContext.java @@ -94,7 +94,8 @@ public ProcessingContext( StringUtils.trimToNull(argReader.readValue(CompilerArgumentsEnum.FORMATTER_PROFILE)); this.performanceTracker = perfTrackingEnabled - ? new ActivePerformanceTracker(perfOutputFile) + ? new ActivePerformanceTracker( + perfOutputFile, ProcessingPhases.TOP_LEVEL_PHASES, ProcessingPhases.PHASE_CHILDREN) : new NoOpPerformanceTracker(); this.builderScopeResolver = new BuilderScopeResolver(this); // GeneratorRegistry will be lazily initialized on first access diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingPhases.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingPhases.java new file mode 100644 index 00000000..6ad3a54c --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/ProcessingPhases.java @@ -0,0 +1,79 @@ +/* + * MIT License + * + * Copyright (c) 2025-2026 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.processor.processing; + +import static org.javahelpers.simple.builders.processor.classgen.GenerationPhases.*; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Phase names of the builder processing pipeline, reported through {@code + * PerformanceTracker.endPhase}, plus the phase hierarchy an {@code ActivePerformanceTracker} needs + * to render its report. + */ +public final class ProcessingPhases { + + // Top-level phases + public static final String PHASE_ELEMENT_COLLECTION = "Element Collection"; + public static final String PHASE_CONFIGURATION_RESOLUTION = "Configuration Resolution"; + public static final String PHASE_BUILDER_DEFINITION_EXTRACTION = "Builder Definition Extraction"; + public static final String PHASE_DTO_MAPPING = "DTO Mapping"; + public static final String PHASE_CODE_GENERATION = "Code Generation"; + + /** Top-level phases of the pipeline, in report display order. */ + public static final List TOP_LEVEL_PHASES = + List.of( + PHASE_ELEMENT_COLLECTION, + PHASE_CONFIGURATION_RESOLUTION, + PHASE_BUILDER_DEFINITION_EXTRACTION, + PHASE_DTO_MAPPING, + PHASE_CODE_GENERATION); + + /** Phase hierarchy for report display: parent phase to ordered child phases. */ + public static final Map> PHASE_CHILDREN = phaseChildren(); + + private static Map> phaseChildren() { + Map> children = new LinkedHashMap<>(); + children.put(PHASE_CODE_GENERATION, List.of(PHASE_SOURCE_CONSTRUCTION, PHASE_FILE_WRITING)); + children.put( + PHASE_SOURCE_CONSTRUCTION, + List.of(PHASE_ELEMENT_BUILDING, PHASE_STRING_GENERATION, PHASE_FORMATTING)); + children.put( + PHASE_ELEMENT_BUILDING, + List.of( + PHASE_CLASS_CREATION, + PHASE_CLASS_METADATA, + PHASE_FIELDS, + PHASE_CONSTRUCTORS, + PHASE_METHODS, + PHASE_NESTED_TYPES, + PHASE_CLASS_ANNOTATIONS)); + return children; + } + + private ProcessingPhases() {} +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/ActivePerformanceTracker.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/ActivePerformanceTracker.java index be243479..90b50430 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/ActivePerformanceTracker.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/ActivePerformanceTracker.java @@ -48,9 +48,9 @@ * start} pushes a timestamp onto the stack, and the matching {@code end} pops it and accumulates * the elapsed time. This allows nested calls without passing identifiers. * - *

The tracker maintains per-phase totals (with a hardcoded hierarchy defined by {@link - * #PHASE_CHILDREN} for report display), per-generator and per-enhancer totals with call counts, and - * per-class metrics including field and collection counts. + *

The tracker maintains per-phase totals (rendered using the phase hierarchy handed in at + * construction), per-generator and per-enhancer totals with call counts, and per-class metrics + * including field and collection counts. * *

When an output file path is provided, {@link #generateReport(ProcessingLogger)} writes a * structured JSON report with hierarchical phase breakdown, class metrics, and generator/enhancer @@ -64,36 +64,13 @@ */ public final class ActivePerformanceTracker implements PerformanceTracker { - /** Hardcoded phase hierarchy for report display. Order defines display order. */ - private static final List TOP_LEVEL_PHASES = - List.of( - PHASE_ELEMENT_COLLECTION, - PHASE_CONFIGURATION_RESOLUTION, - PHASE_BUILDER_DEFINITION_EXTRACTION, - PHASE_DTO_MAPPING, - PHASE_CODE_GENERATION); - private static final String JSON_KEY_ELAPSED_NANOS = "elapsedNanos"; - private static final Map> PHASE_CHILDREN = new LinkedHashMap<>(); - - static { - PHASE_CHILDREN.put( - PHASE_CODE_GENERATION, List.of(PHASE_SOURCE_CONSTRUCTION, PHASE_FILE_WRITING)); - PHASE_CHILDREN.put( - PHASE_SOURCE_CONSTRUCTION, - List.of(PHASE_ELEMENT_BUILDING, PHASE_STRING_GENERATION, PHASE_FORMATTING)); - PHASE_CHILDREN.put( - PHASE_ELEMENT_BUILDING, - List.of( - PHASE_CLASS_CREATION, - PHASE_CLASS_METADATA, - PHASE_FIELDS, - PHASE_CONSTRUCTORS, - PHASE_METHODS, - PHASE_NESTED_TYPES, - PHASE_CLASS_ANNOTATIONS)); - } + /** Top-level phases in report display order. */ + private final List topLevelPhases; + + /** Phase hierarchy for report display: parent phase to ordered child phases. */ + private final Map> phaseChildren; private final Map phaseTimes = new ConcurrentHashMap<>(); private final Map generatorTimes = new ConcurrentHashMap<>(); @@ -116,13 +93,19 @@ public final class ActivePerformanceTracker implements PerformanceTracker { private final ThreadLocal classStartTime = new ThreadLocal<>(); /** - * Creates a new ActivePerformanceTracker and records the overall start time. + * Creates a new ActivePerformanceTracker with a phase hierarchy for report display and records + * the overall start time. * * @param outputFilePath optional path for JSON report output; null or empty disables file output + * @param topLevelPhases top-level phases in display order + * @param phaseChildren parent phase to ordered child phases */ - public ActivePerformanceTracker(String outputFilePath) { + public ActivePerformanceTracker( + String outputFilePath, List topLevelPhases, Map> phaseChildren) { this.totalStartTime = System.nanoTime(); this.outputFilePath = outputFilePath; + this.topLevelPhases = topLevelPhases; + this.phaseChildren = phaseChildren; } @Override @@ -209,11 +192,10 @@ public void generateReport(ProcessingLogger logger) { logger.info(String.format(Locale.US, "Total processing time: %.1fs", totalSeconds)); logger.info(""); - // Phase breakdown (hierarchical, using hardcoded hierarchy) + // Phase breakdown (hierarchical, using configured hierarchy) logger.info("Phase breakdown:"); - for (int i = 0; i < TOP_LEVEL_PHASES.size(); i++) { - reportPhase( - logger, TOP_LEVEL_PHASES.get(i), totalSeconds, "", i == TOP_LEVEL_PHASES.size() - 1); + for (int i = 0; i < topLevelPhases.size(); i++) { + reportPhase(logger, topLevelPhases.get(i), totalSeconds, "", i == topLevelPhases.size() - 1); } logger.info(""); @@ -356,7 +338,7 @@ private void reportPhase( logger.info( String.format( Locale.US, "%s%s%s: %.1fs (%.1f%%)", prefix, connector, phase, seconds, percentage)); - List children = PHASE_CHILDREN.get(phase); + List children = phaseChildren.get(phase); if (children != null) { String childPrefix = prefix + (isLast ? " " : "│ "); for (int i = 0; i < children.size(); i++) { @@ -388,7 +370,7 @@ private void writeJsonReport(long totalNanos) throws IOException { // Phase breakdown (hierarchical) Map phaseBreakdown = new LinkedHashMap<>(); - for (String phase : TOP_LEVEL_PHASES) { + for (String phase : topLevelPhases) { phaseBreakdown.put(phase, buildPhaseJson(phase, totalNanos)); } root.put("phaseBreakdown", phaseBreakdown); @@ -435,7 +417,7 @@ private Map buildPhaseJson(String phase, long parentNanos) { phaseMap.put(JSON_KEY_ELAPSED_NANOS, nanos); phaseMap.put("elapsedSeconds", seconds); phaseMap.put("percentage", percentage); - List children = PHASE_CHILDREN.get(phase); + List children = phaseChildren.get(phase); if (children != null && !children.isEmpty()) { Map childrenMap = new LinkedHashMap<>(); for (String child : children) { diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/PerformanceTracker.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/PerformanceTracker.java index bb787dab..dc6c1e00 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/PerformanceTracker.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/PerformanceTracker.java @@ -40,31 +40,6 @@ */ public interface PerformanceTracker { - // Top-level phases - String PHASE_ELEMENT_COLLECTION = "Element Collection"; - String PHASE_CONFIGURATION_RESOLUTION = "Configuration Resolution"; - String PHASE_BUILDER_DEFINITION_EXTRACTION = "Builder Definition Extraction"; - String PHASE_DTO_MAPPING = "DTO Mapping"; - String PHASE_CODE_GENERATION = "Code Generation"; - - // Code Generation children - String PHASE_SOURCE_CONSTRUCTION = "Source Construction"; - String PHASE_FILE_WRITING = "File Writing"; - - // Source Construction children - String PHASE_ELEMENT_BUILDING = "Element Building"; - String PHASE_STRING_GENERATION = "String Generation"; - String PHASE_FORMATTING = "Formatting"; - - // Element Building children - String PHASE_CLASS_CREATION = "Class Creation"; - String PHASE_CLASS_METADATA = "Class Metadata"; - String PHASE_FIELDS = "Fields"; - String PHASE_CONSTRUCTORS = "Constructors"; - String PHASE_METHODS = "Methods"; - String PHASE_NESTED_TYPES = "Nested Types"; - String PHASE_CLASS_ANNOTATIONS = "Class Annotations"; - /** * Starts tracking a processing phase. * diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/ProcessingLogger.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/ProcessingLogger.java index c67fc9cb..67239f34 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/ProcessingLogger.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/processing/logging/ProcessingLogger.java @@ -28,8 +28,6 @@ import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.Element; import javax.tools.Diagnostic; -import org.javahelpers.simple.builders.processor.processing.CompilerArgumentsEnum; -import org.javahelpers.simple.builders.processor.processing.CompilerArgumentsReader; /** * Logger for all messages during annotation processing. Providing util-functions for posting @@ -46,20 +44,41 @@ public class ProcessingLogger { /** Thread-local indentation level for hierarchical logging. */ private static final ThreadLocal indentationLevel = ThreadLocal.withInitial(() -> 0); + /** Verbosity level of a {@link ProcessingLogger}. */ + private enum Verbosity { + /** Errors, warnings and info messages only. */ + NORMAL, + /** Additionally reports debug messages. */ + VERBOSE + } + /** * 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 or -Asimplebuilder.verbose=true + * used to report errors, warnings, and other notices during annotation processing. To enable + * debug messages use {@link #verbose(ProcessingEnvironment)} instead. * - * @param processingEnv the processing environment providing messager and options + * @param processingEnv the processing environment providing the messager */ public ProcessingLogger(ProcessingEnvironment processingEnv) { + this(processingEnv, Verbosity.NORMAL); + } + + /** + * Constructs a new ProcessingLogger that additionally reports debug messages. + * + * @param processingEnv the processing environment providing the messager + * @return a debug-enabled processing logger + */ + public static ProcessingLogger verbose(ProcessingEnvironment processingEnv) { + return new ProcessingLogger(processingEnv, Verbosity.VERBOSE); + } + + private ProcessingLogger(ProcessingEnvironment processingEnv, Verbosity verbosity) { // 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); + this.debugEnabled = verbosity == Verbosity.VERBOSE; } /** Resets ThreadLocal state to ensure clean state between test runs. */ diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/CompilerArgumentsReaderTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/CompilerArgumentsReaderTest.java index 76e1247c..f2a67d52 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/CompilerArgumentsReaderTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/CompilerArgumentsReaderTest.java @@ -33,6 +33,7 @@ import org.javahelpers.simple.builders.core.enums.AccessModifier; import org.javahelpers.simple.builders.core.enums.OptionState; import org.javahelpers.simple.builders.processor.model.core.BuilderConfiguration; +import org.javahelpers.simple.builders.processor.processing.BuilderConfigurationReader; import org.javahelpers.simple.builders.processor.processing.CompilerArgumentsEnum; import org.javahelpers.simple.builders.processor.processing.CompilerArgumentsReader; import org.javahelpers.simple.builders.processor.testing.CapturingProcessingLogger; @@ -257,7 +258,8 @@ void readBuilderConfiguration_NoArguments_ReturnsDefaults() { CompilerArgumentsReader reader = new CompilerArgumentsReader(env); BuilderConfiguration config = - reader.readBuilderConfiguration(CapturingProcessingLogger.create().logger()); + BuilderConfigurationReader.readBuilderConfiguration( + reader, CapturingProcessingLogger.create().logger()); assertNotNull(config, "Configuration should not be null"); assertEquals(OptionState.UNSET, config.generateFieldSupplier()); @@ -298,7 +300,8 @@ void readBuilderConfiguration_AllOptionsSet_ReadsCorrectly() { CompilerArgumentsReader reader = new CompilerArgumentsReader(env); BuilderConfiguration config = - reader.readBuilderConfiguration(CapturingProcessingLogger.create().logger()); + BuilderConfigurationReader.readBuilderConfiguration( + reader, CapturingProcessingLogger.create().logger()); assertEquals(OptionState.ENABLED, config.generateFieldSupplier()); assertEquals(OptionState.DISABLED, config.generateFieldConsumer()); @@ -327,7 +330,8 @@ void readBuilderConfiguration_MixedValidInvalid_HandlesGracefully() { CompilerArgumentsReader reader = new CompilerArgumentsReader(env); BuilderConfiguration config = - reader.readBuilderConfiguration(CapturingProcessingLogger.create().logger()); + BuilderConfigurationReader.readBuilderConfiguration( + reader, CapturingProcessingLogger.create().logger()); assertEquals( OptionState.UNSET, config.generateFieldSupplier(), "Invalid option should be UNSET"); @@ -352,7 +356,8 @@ void readBuilderConfiguration_EmptyStringValues_HandlesGracefully() { CompilerArgumentsReader reader = new CompilerArgumentsReader(env); BuilderConfiguration config = - reader.readBuilderConfiguration(CapturingProcessingLogger.create().logger()); + BuilderConfigurationReader.readBuilderConfiguration( + reader, CapturingProcessingLogger.create().logger()); assertEquals(OptionState.UNSET, config.generateFieldSupplier(), "Empty should be UNSET"); assertEquals(AccessModifier.DEFAULT, config.getBuilderAccess(), "Empty should be DEFAULT"); diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/RoasterCodeGeneratorResilienceTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/RoasterCodeGeneratorResilienceTest.java index 4c221819..a4baa51d 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/RoasterCodeGeneratorResilienceTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/RoasterCodeGeneratorResilienceTest.java @@ -30,6 +30,7 @@ import javax.annotation.processing.ProcessingEnvironment; import org.javahelpers.simple.builders.core.enums.AccessModifier; import org.javahelpers.simple.builders.core.enums.FormattingMode; +import org.javahelpers.simple.builders.processor.classgen.GenerationEnvironment; import org.javahelpers.simple.builders.processor.classgen.roaster.RoasterCodeGenerator; import org.javahelpers.simple.builders.processor.classgen.roaster.exceptions.RoasterMapperException; import org.javahelpers.simple.builders.processor.exceptions.BuilderException; @@ -82,7 +83,11 @@ void shouldWrapRenderingRuntimeExceptionInBuilderException() { ProcessingEnvironment env = ProcessingEnvironmentStub.createEmpty(); ProcessingLogger logger = new ProcessingLogger(env); ProcessingContext context = new ProcessingContext(logger, BuilderConfiguration.DEFAULT, env); - RoasterCodeGenerator generator = new RoasterCodeGenerator(context, env); + RoasterCodeGenerator generator = + new RoasterCodeGenerator( + new GenerationEnvironment( + env, logger, context.getPerformanceTracker(), context::reportBasedOnStrictMode), + context::createSourceFormatter); BuilderException thrown = assertThrows(BuilderException.class, () -> generator.generateClass(classDef)); @@ -109,7 +114,14 @@ void shouldKeepFirstMethodAndWarnOnDuplicateSignature() { ProcessingEnvironment env = ProcessingEnvironmentStub.createEmpty(); ProcessingContext context = new ProcessingContext(capturing.logger(), BuilderConfiguration.DEFAULT, env); - RoasterCodeGenerator generator = new RoasterCodeGenerator(context, env); + RoasterCodeGenerator generator = + new RoasterCodeGenerator( + new GenerationEnvironment( + env, + capturing.logger(), + context.getPerformanceTracker(), + context::reportBasedOnStrictMode), + context::createSourceFormatter); // generation itself succeeds; only the file-write step fails on the stub's null filer assertThrows(NullPointerException.class, () -> generator.generateClass(classDef)); diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/processing/logging/ActivePerformanceTrackerTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/processing/logging/ActivePerformanceTrackerTest.java index a8b07629..a01949dc 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/processing/logging/ActivePerformanceTrackerTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/processing/logging/ActivePerformanceTrackerTest.java @@ -31,6 +31,8 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import org.javahelpers.simple.builders.processor.classgen.GenerationPhases; +import org.javahelpers.simple.builders.processor.processing.ProcessingPhases; import org.javahelpers.simple.builders.processor.testing.CapturingProcessingLogger; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -47,10 +49,12 @@ class ActivePerformanceTrackerTest { /** Helper to create a tracker, track some data, and generate report with JSON output. */ private JsonNode generateReportAndParseJson(String outputFile) throws IOException { - ActivePerformanceTracker tracker = new ActivePerformanceTracker(outputFile); + ActivePerformanceTracker tracker = + new ActivePerformanceTracker( + outputFile, ProcessingPhases.TOP_LEVEL_PHASES, ProcessingPhases.PHASE_CHILDREN); tracker.startClass("TestClassA"); tracker.startPhase(); - tracker.endPhase(PerformanceTracker.PHASE_BUILDER_DEFINITION_EXTRACTION); + tracker.endPhase(ProcessingPhases.PHASE_BUILDER_DEFINITION_EXTRACTION); tracker.startGenerator(); tracker.endGenerator("FieldSupplierGenerator"); tracker.startEnhancer(); @@ -59,7 +63,7 @@ private JsonNode generateReportAndParseJson(String outputFile) throws IOExceptio tracker.startClass("TestClassB"); tracker.startPhase(); - tracker.endPhase(PerformanceTracker.PHASE_DTO_MAPPING); + tracker.endPhase(ProcessingPhases.PHASE_DTO_MAPPING); tracker.endClass(3, 1); CapturingProcessingLogger capturing = CapturingProcessingLogger.create(); @@ -72,7 +76,9 @@ private JsonNode generateReportAndParseJson(String outputFile) throws IOExceptio @Test void generateReport_withNoData_logsBasicReport() { - ActivePerformanceTracker tracker = new ActivePerformanceTracker(null); + ActivePerformanceTracker tracker = + new ActivePerformanceTracker( + null, ProcessingPhases.TOP_LEVEL_PHASES, ProcessingPhases.PHASE_CHILDREN); CapturingProcessingLogger capturing = CapturingProcessingLogger.create(); ProcessingLogger logger = capturing.logger(); @@ -85,7 +91,9 @@ void generateReport_withNoData_logsBasicReport() { @Test void generateReport_withClassData_logsClassCount() { - ActivePerformanceTracker tracker = new ActivePerformanceTracker(null); + ActivePerformanceTracker tracker = + new ActivePerformanceTracker( + null, ProcessingPhases.TOP_LEVEL_PHASES, ProcessingPhases.PHASE_CHILDREN); tracker.startClass("MyClass"); tracker.endClass(4, 1); @@ -100,7 +108,9 @@ void generateReport_withClassData_logsClassCount() { @Test void generateReport_withGeneratorData_logsGeneratorStats() { - ActivePerformanceTracker tracker = new ActivePerformanceTracker(null); + ActivePerformanceTracker tracker = + new ActivePerformanceTracker( + null, ProcessingPhases.TOP_LEVEL_PHASES, ProcessingPhases.PHASE_CHILDREN); tracker.startClass("MyClass"); tracker.startGenerator(); tracker.endGenerator("MyGenerator"); @@ -116,7 +126,9 @@ void generateReport_withGeneratorData_logsGeneratorStats() { @Test void generateReport_withEnhancerData_logsEnhancerStats() { - ActivePerformanceTracker tracker = new ActivePerformanceTracker(null); + ActivePerformanceTracker tracker = + new ActivePerformanceTracker( + null, ProcessingPhases.TOP_LEVEL_PHASES, ProcessingPhases.PHASE_CHILDREN); tracker.startClass("MyClass"); tracker.startEnhancer(); tracker.endEnhancer("MyEnhancer"); @@ -132,11 +144,13 @@ void generateReport_withEnhancerData_logsEnhancerStats() { @Test void generateReport_withPhaseData_logsPhaseBreakdown() { - ActivePerformanceTracker tracker = new ActivePerformanceTracker(null); + ActivePerformanceTracker tracker = + new ActivePerformanceTracker( + null, ProcessingPhases.TOP_LEVEL_PHASES, ProcessingPhases.PHASE_CHILDREN); tracker.startPhase(); - tracker.endPhase(PerformanceTracker.PHASE_CONFIGURATION_RESOLUTION); + tracker.endPhase(ProcessingPhases.PHASE_CONFIGURATION_RESOLUTION); tracker.startPhase(); - tracker.endPhase(PerformanceTracker.PHASE_CODE_GENERATION); + tracker.endPhase(ProcessingPhases.PHASE_CODE_GENERATION); CapturingProcessingLogger capturing = CapturingProcessingLogger.create(); ProcessingLogger logger = capturing.logger(); @@ -145,17 +159,19 @@ void generateReport_withPhaseData_logsPhaseBreakdown() { assertTrue(capturing.messages().stream().anyMatch(m -> m.contains("Phase breakdown"))); assertTrue( capturing.messages().stream() - .anyMatch(m -> m.contains(PerformanceTracker.PHASE_CONFIGURATION_RESOLUTION))); + .anyMatch(m -> m.contains(ProcessingPhases.PHASE_CONFIGURATION_RESOLUTION))); assertTrue( capturing.messages().stream() - .anyMatch(m -> m.contains(PerformanceTracker.PHASE_CODE_GENERATION))); + .anyMatch(m -> m.contains(ProcessingPhases.PHASE_CODE_GENERATION))); } @Test void generateReport_withElementCollectionPhase_logsPhaseInReport() { - ActivePerformanceTracker tracker = new ActivePerformanceTracker(null); + ActivePerformanceTracker tracker = + new ActivePerformanceTracker( + null, ProcessingPhases.TOP_LEVEL_PHASES, ProcessingPhases.PHASE_CHILDREN); tracker.startPhase(); - tracker.endPhase(PerformanceTracker.PHASE_ELEMENT_COLLECTION); + tracker.endPhase(ProcessingPhases.PHASE_ELEMENT_COLLECTION); CapturingProcessingLogger capturing = CapturingProcessingLogger.create(); ProcessingLogger logger = capturing.logger(); @@ -163,16 +179,20 @@ void generateReport_withElementCollectionPhase_logsPhaseInReport() { assertTrue( capturing.messages().stream() - .anyMatch(m -> m.contains(PerformanceTracker.PHASE_ELEMENT_COLLECTION)), - "Text report should contain " + PerformanceTracker.PHASE_ELEMENT_COLLECTION); + .anyMatch(m -> m.contains(ProcessingPhases.PHASE_ELEMENT_COLLECTION)), + "Text report should contain " + ProcessingPhases.PHASE_ELEMENT_COLLECTION); } @Test void jsonReport_phaseBreakdown_containsElementCollectionPhase() throws IOException { Path jsonFile = tempDir.resolve("report-element-collection.json"); - ActivePerformanceTracker tracker = new ActivePerformanceTracker(jsonFile.toString()); + ActivePerformanceTracker tracker = + new ActivePerformanceTracker( + jsonFile.toString(), + ProcessingPhases.TOP_LEVEL_PHASES, + ProcessingPhases.PHASE_CHILDREN); tracker.startPhase(); - tracker.endPhase(PerformanceTracker.PHASE_ELEMENT_COLLECTION); + tracker.endPhase(ProcessingPhases.PHASE_ELEMENT_COLLECTION); CapturingProcessingLogger capturing = CapturingProcessingLogger.create(); ProcessingLogger logger = capturing.logger(); @@ -181,9 +201,9 @@ void jsonReport_phaseBreakdown_containsElementCollectionPhase() throws IOExcepti JsonNode root = new ObjectMapper().readTree(Files.readString(jsonFile)); JsonNode phases = root.get("phaseBreakdown"); assertTrue( - phases.has(PerformanceTracker.PHASE_ELEMENT_COLLECTION), - "JSON phaseBreakdown should contain " + PerformanceTracker.PHASE_ELEMENT_COLLECTION); - JsonNode elementCollection = phases.get(PerformanceTracker.PHASE_ELEMENT_COLLECTION); + phases.has(ProcessingPhases.PHASE_ELEMENT_COLLECTION), + "JSON phaseBreakdown should contain " + ProcessingPhases.PHASE_ELEMENT_COLLECTION); + JsonNode elementCollection = phases.get(ProcessingPhases.PHASE_ELEMENT_COLLECTION); assertTrue(elementCollection.has("elapsedNanos")); assertTrue(elementCollection.has("elapsedSeconds")); assertTrue(elementCollection.has("percentage")); @@ -191,7 +211,9 @@ void jsonReport_phaseBreakdown_containsElementCollectionPhase() throws IOExcepti @Test void generateReport_withNullOutputFile_doesNotWriteFile() { - ActivePerformanceTracker tracker = new ActivePerformanceTracker(null); + ActivePerformanceTracker tracker = + new ActivePerformanceTracker( + null, ProcessingPhases.TOP_LEVEL_PHASES, ProcessingPhases.PHASE_CHILDREN); tracker.startClass("MyClass"); tracker.endClass(1, 0); @@ -273,12 +295,12 @@ void jsonReport_phaseBreakdown_containsPhaseHierarchy() throws IOException { JsonNode root = generateReportAndParseJson(jsonFile.toString()); JsonNode phases = root.get("phaseBreakdown"); - assertTrue(phases.has(PerformanceTracker.PHASE_CONFIGURATION_RESOLUTION)); - assertTrue(phases.has(PerformanceTracker.PHASE_BUILDER_DEFINITION_EXTRACTION)); - assertTrue(phases.has(PerformanceTracker.PHASE_DTO_MAPPING)); - assertTrue(phases.has(PerformanceTracker.PHASE_CODE_GENERATION)); + assertTrue(phases.has(ProcessingPhases.PHASE_CONFIGURATION_RESOLUTION)); + assertTrue(phases.has(ProcessingPhases.PHASE_BUILDER_DEFINITION_EXTRACTION)); + assertTrue(phases.has(ProcessingPhases.PHASE_DTO_MAPPING)); + assertTrue(phases.has(ProcessingPhases.PHASE_CODE_GENERATION)); - JsonNode codeGen = phases.get(PerformanceTracker.PHASE_CODE_GENERATION); + JsonNode codeGen = phases.get(ProcessingPhases.PHASE_CODE_GENERATION); assertTrue(codeGen.has("elapsedNanos")); assertTrue(codeGen.has("elapsedSeconds")); assertTrue(codeGen.has("percentage")); @@ -289,17 +311,21 @@ void jsonReport_phaseBreakdown_codeGenerationHasChildren() throws IOException { Path jsonFile = tempDir.resolve("report.json"); JsonNode root = generateReportAndParseJson(jsonFile.toString()); - JsonNode codeGen = root.get("phaseBreakdown").get(PerformanceTracker.PHASE_CODE_GENERATION); + JsonNode codeGen = root.get("phaseBreakdown").get(ProcessingPhases.PHASE_CODE_GENERATION); assertTrue(codeGen.has("children")); JsonNode children = codeGen.get("children"); - assertTrue(children.has(PerformanceTracker.PHASE_SOURCE_CONSTRUCTION)); - assertTrue(children.has(PerformanceTracker.PHASE_FILE_WRITING)); + assertTrue(children.has(GenerationPhases.PHASE_SOURCE_CONSTRUCTION)); + assertTrue(children.has(GenerationPhases.PHASE_FILE_WRITING)); } @Test void jsonReport_withNoData_hasEmptyArrays() throws IOException { Path jsonFile = tempDir.resolve("report-empty.json"); - ActivePerformanceTracker tracker = new ActivePerformanceTracker(jsonFile.toString()); + ActivePerformanceTracker tracker = + new ActivePerformanceTracker( + jsonFile.toString(), + ProcessingPhases.TOP_LEVEL_PHASES, + ProcessingPhases.PHASE_CHILDREN); CapturingProcessingLogger capturing = CapturingProcessingLogger.create(); ProcessingLogger logger = capturing.logger(); @@ -326,8 +352,10 @@ void jsonReport_createsParentDirectoriesIfMissing() throws IOException { @Test void endMethodsWithoutStart_doesNothing() { - ActivePerformanceTracker tracker = new ActivePerformanceTracker(null); - tracker.endPhase(PerformanceTracker.PHASE_CONFIGURATION_RESOLUTION); + ActivePerformanceTracker tracker = + new ActivePerformanceTracker( + null, ProcessingPhases.TOP_LEVEL_PHASES, ProcessingPhases.PHASE_CHILDREN); + tracker.endPhase(ProcessingPhases.PHASE_CONFIGURATION_RESOLUTION); tracker.endGenerator("NonexistentGenerator"); tracker.endEnhancer("NonexistentEnhancer"); tracker.endClass(5, 2); @@ -346,7 +374,11 @@ void endMethodsWithoutStart_doesNothing() { @Test void multipleGenerators_accumulateTimeAndCalls() throws IOException { Path jsonFile = tempDir.resolve("report-multi-gen.json"); - ActivePerformanceTracker tracker = new ActivePerformanceTracker(jsonFile.toString()); + ActivePerformanceTracker tracker = + new ActivePerformanceTracker( + jsonFile.toString(), + ProcessingPhases.TOP_LEVEL_PHASES, + ProcessingPhases.PHASE_CHILDREN); tracker.startClass("MyClass"); tracker.startGenerator(); tracker.endGenerator("GenA"); @@ -371,7 +403,11 @@ void multipleGenerators_accumulateTimeAndCalls() throws IOException { @Test void jsonReport_escapesSpecialCharactersInNames() throws IOException { Path jsonFile = tempDir.resolve("report-escaping.json"); - ActivePerformanceTracker tracker = new ActivePerformanceTracker(jsonFile.toString()); + ActivePerformanceTracker tracker = + new ActivePerformanceTracker( + jsonFile.toString(), + ProcessingPhases.TOP_LEVEL_PHASES, + ProcessingPhases.PHASE_CHILDREN); tracker.startClass("MyClass"); tracker.startGenerator(); tracker.endGenerator("Gen\"\\\n\t\r\b\fA\u0001"); @@ -392,11 +428,15 @@ void jsonReport_escapesSpecialCharactersInNames() throws IOException { @Test void multiplePhases_accumulateTime() throws IOException { Path jsonFile = tempDir.resolve("report-multi-phase.json"); - ActivePerformanceTracker tracker = new ActivePerformanceTracker(jsonFile.toString()); + ActivePerformanceTracker tracker = + new ActivePerformanceTracker( + jsonFile.toString(), + ProcessingPhases.TOP_LEVEL_PHASES, + ProcessingPhases.PHASE_CHILDREN); tracker.startPhase(); - tracker.endPhase(PerformanceTracker.PHASE_CONFIGURATION_RESOLUTION); + tracker.endPhase(ProcessingPhases.PHASE_CONFIGURATION_RESOLUTION); tracker.startPhase(); - tracker.endPhase(PerformanceTracker.PHASE_CONFIGURATION_RESOLUTION); + tracker.endPhase(ProcessingPhases.PHASE_CONFIGURATION_RESOLUTION); CapturingProcessingLogger capturing = CapturingProcessingLogger.create(); ProcessingLogger logger = capturing.logger(); @@ -404,14 +444,17 @@ void multiplePhases_accumulateTime() throws IOException { JsonNode root = new ObjectMapper().readTree(Files.readString(jsonFile)); JsonNode phase = - root.get("phaseBreakdown").get(PerformanceTracker.PHASE_CONFIGURATION_RESOLUTION); + root.get("phaseBreakdown").get(ProcessingPhases.PHASE_CONFIGURATION_RESOLUTION); assertTrue(phase.get("elapsedNanos").asLong() > 0); } @Test void generateReport_logsWarningOnInvalidPath() { ActivePerformanceTracker tracker = - new ActivePerformanceTracker("/nonexistent\0invalid/path.json"); + new ActivePerformanceTracker( + "/nonexistent\0invalid/path.json", + ProcessingPhases.TOP_LEVEL_PHASES, + ProcessingPhases.PHASE_CHILDREN); tracker.startClass("MyClass"); tracker.endClass(1, 0); @@ -446,7 +489,11 @@ void noOpPerformanceTracker_allMethodsAreNoOps() { @Test void jsonReport_generatorAndEnhancerStats_sortedByElapsedDesc() throws IOException { Path jsonFile = tempDir.resolve("report-sorted.json"); - ActivePerformanceTracker tracker = new ActivePerformanceTracker(jsonFile.toString()); + ActivePerformanceTracker tracker = + new ActivePerformanceTracker( + jsonFile.toString(), + ProcessingPhases.TOP_LEVEL_PHASES, + ProcessingPhases.PHASE_CHILDREN); tracker.startClass("MyClass"); tracker.startGenerator(); tracker.endGenerator("SlowGen"); diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/testing/CapturingProcessingLogger.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/testing/CapturingProcessingLogger.java index 3ee63ad0..41301532 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/testing/CapturingProcessingLogger.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/testing/CapturingProcessingLogger.java @@ -81,16 +81,14 @@ public static CapturingProcessingLogger create() { } /** - * Creates a new capturing logger with debug logging enabled via {@code - * simplebuilder.verbose=true}. + * Creates a new capturing logger with debug logging enabled. * * @return a new debug-enabled capturing logger instance */ public static CapturingProcessingLogger createDebugEnabled() { List messages = new ArrayList<>(); ProcessingLogger logger = - new ProcessingLogger( - createCapturingEnvironment(messages, Map.of("simplebuilder.verbose", "true"))); + ProcessingLogger.verbose(createCapturingEnvironment(messages, Collections.emptyMap())); return new CapturingProcessingLogger(messages, logger); }