Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -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.PipelinePhases.PHASE_BUILDER_DEFINITION_EXTRACTION;
import static org.javahelpers.simple.builders.processor.processing.PipelinePhases.PHASE_CODE_GENERATION;
import static org.javahelpers.simple.builders.processor.processing.PipelinePhases.PHASE_CONFIGURATION_RESOLUTION;
import static org.javahelpers.simple.builders.processor.processing.PipelinePhases.PHASE_DTO_MAPPING;
import static org.javahelpers.simple.builders.processor.processing.PipelinePhases.PHASE_ELEMENT_COLLECTION;

import com.google.auto.service.AutoService;
import java.util.ArrayList;
Expand Down Expand Up @@ -63,6 +63,9 @@
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.CodeGenerationException;
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;
Expand All @@ -79,6 +82,7 @@
import org.javahelpers.simple.builders.processor.processing.CompilerArgumentsReader;
import org.javahelpers.simple.builders.processor.processing.ProcessingContext;
import org.javahelpers.simple.builders.processor.processing.ProcessingTarget;
import org.javahelpers.simple.builders.processor.processing.SourceFormatterResolver;
import org.javahelpers.simple.builders.processor.processing.logging.PerformanceTracker;
import org.javahelpers.simple.builders.processor.processing.logging.ProcessingLogger;

Expand All @@ -95,25 +99,37 @@ public class BuilderProcessor extends AbstractProcessor {

private ProcessingContext context;
private ProcessingLogger logger;
private RoasterCodeGenerator codeGenerator;
private ClassCodeGenerator codeGenerator;
private SourceFormatterResolver formatterResolver;
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.createVerbose(processingEnv)
: ProcessingLogger.create(processingEnv);
logger.debug("Starting BuilderProcessor...");

BuilderConfiguration globalConfig =
BuilderConfigurationReader.readCompilerConfiguration(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));
this.formatterResolver = new SourceFormatterResolver(processingEnv, logger);
this.jacksonModuleGenerator = new JacksonModuleGenerator(processingEnv, logger, globalConfig);

// Initialize GeneratorRegistry once during processor initialization
Expand Down Expand Up @@ -222,8 +238,10 @@ private void generateJacksonModules(PerformanceTracker tracker) {
String packageName = moduleClassDef.getTypeName().getPackageName();
context.info("Generating Jackson Module in package '%s'", packageName);
try {
codeGenerator.generateClass(moduleClassDef);
} catch (BuilderException e) {
assertTargetClassDoesNotExist(moduleClassDef);
codeGenerator.generateClass(
moduleClassDef, formatterResolver.resolve(moduleClassDef.getFormattingMode()));
} catch (CodeGenerationException e) {
// By default Jackson module generation failures are warnings. In opt-in strict mode
// they are promoted to errors that fail the build.
context.reportBasedOnStrictMode(
Expand Down Expand Up @@ -592,6 +610,38 @@ public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}

/**
* Fails the build with a descriptive error when the class to be generated already exists (a
* manually written class or a leftover from a previous compilation).
*
* @param classDef the class definition to check
* @throws CodeGenerationException if a class with the target qualified name already exists
*/
private void assertTargetClassDoesNotExist(GenerationTargetClassDto classDef)
throws CodeGenerationException {
String qualifiedName = classDef.getTypeName().getFullQualifiedName();
boolean exists;
try {
exists = context.getTypeElement(qualifiedName) != null;
} catch (Exception e) {
context.debug(
"Error checking if class '%s' already exists: %s",
qualifiedName, e.getMessage() != null ? e.getMessage() : "No message");
exists = false;
}
if (exists) {
throw new CodeGenerationException(
"""
Builder class '%s' already exists. This may be a manually written builder or a previously generated builder.
To resolve this:
1. If you have a manual builder, consider renaming it or removing @SimpleBuilder from the DTO
2. If this is from a previous compilation, clean and rebuild the project
3. Check that you're not trying to generate multiple builders for the same DTO
"""
.formatted(qualifiedName));
}
}

private void process(Element annotatedElement, BuilderConfiguration config, String builderPackage)
throws BuilderException {
context.initProcessingTarget(new ProcessingTarget(config, builderPackage));
Expand Down Expand Up @@ -622,7 +672,13 @@ private void process(Element annotatedElement, BuilderConfiguration config, Stri
// Track Code Generation (parent phase; sub-phases tracked inside RoasterCodeGenerator)
tracker.startPhase();

codeGenerator.generateClass(renderingDto);
try {
assertTargetClassDoesNotExist(renderingDto);
codeGenerator.generateClass(
renderingDto, formatterResolver.resolve(renderingDto.getFormattingMode()));
} catch (CodeGenerationException e) {
throw new BuilderException(annotatedElement, e);
}
tracker.endPhase(PHASE_CODE_GENERATION);

// Collect info for Jackson Module if enabled
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@
import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto;
import org.javahelpers.simple.builders.processor.model.type.TypeName;
import org.javahelpers.simple.builders.processor.model.type.TypeNamePrimitive;
import org.javahelpers.simple.builders.processor.processing.ProcessingContext;

/** Extractor for field annotations, converting them from Java model elements to DTOs. */
public final class FieldAnnotationExtractor {
Expand Down Expand Up @@ -82,7 +81,7 @@ private FieldAnnotationExtractor() {
* Extracts the {@code @Deprecated} annotation from the given element, preserving its members
* (e.g. {@code since} and {@code forRemoval}).
*
* <p>Unlike {@link #extractAnnotations(VariableElement, ProcessingContext)}, this method targets
* <p>Unlike {@link #extractAnnotations(VariableElement, TypeMappingContext)}, this method targets
* a single annotation and works on any {@link Element} (method, field, parameter, record
* component, type), not just {@link VariableElement}s.
*
Expand All @@ -92,7 +91,7 @@ private FieldAnnotationExtractor() {
* element is not deprecated
*/
public static Optional<AnnotationDto> extractDeprecatedAnnotation(
Element element, ProcessingContext context) {
Element element, TypeMappingContext context) {
if (element == null) {
return Optional.empty();
}
Expand Down Expand Up @@ -120,7 +119,7 @@ public static Optional<AnnotationDto> extractDeprecatedAnnotation(
* @return list of annotations to be copied to the builder field
*/
public static List<AnnotationDto> extractAnnotations(
VariableElement param, ProcessingContext context) {
VariableElement param, TypeMappingContext context) {
List<AnnotationDto> annotations = new ArrayList<>();
List<? extends AnnotationMirror> annotationMirrors = param.getAnnotationMirrors();

Expand All @@ -140,7 +139,7 @@ public static List<AnnotationDto> extractAnnotations(
* @return list of annotations to be copied to the builder field
*/
public static List<AnnotationDto> extractAnnotations(
javax.lang.model.type.TypeMirror typeMirror, ProcessingContext context) {
javax.lang.model.type.TypeMirror typeMirror, TypeMappingContext context) {
List<AnnotationDto> annotations = new ArrayList<>();
List<? extends AnnotationMirror> annotationMirrors = typeMirror.getAnnotationMirrors();

Expand All @@ -160,7 +159,7 @@ public static List<AnnotationDto> extractAnnotations(
* @return Optional containing the extracted annotation, or empty if it should be skipped
*/
private static Optional<AnnotationDto> extractAnnotation(
AnnotationMirror mirror, ProcessingContext context) {
AnnotationMirror mirror, TypeMappingContext context) {
// Get the annotation type element
Element annotationElement = mirror.getAnnotationType().asElement();
if (!(annotationElement instanceof TypeElement annotationType)) {
Expand Down Expand Up @@ -188,7 +187,7 @@ private static Optional<AnnotationDto> extractAnnotation(
* @return Optional containing the extracted annotation
*/
private static Optional<AnnotationDto> extractAnnotationWithoutFiltering(
AnnotationMirror mirror, ProcessingContext context) {
AnnotationMirror mirror, TypeMappingContext context) {
Element annotationElement = mirror.getAnnotationType().asElement();
if (!(annotationElement instanceof TypeElement annotationType)) {
return Optional.empty();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -204,9 +203,9 @@ public static boolean isSetterForField(ExecutableElement mth) {
* @param context processing context
* @return {@code true}, if the element has an accessible empty constructor
*/
public static boolean hasEmptyConstructor(TypeElement typeElement, ProcessingContext context) {
public static boolean hasEmptyConstructor(TypeElement typeElement, TypeMappingContext context) {
return ElementFilter.constructorsIn(context.getAllMembers(typeElement)).stream()
.filter(context::isMemberAccessibleFromBuilderPackage)
.filter(context::isAccessibleFromGeneratedPackage)
.anyMatch(JavaLangAnalyser::hasNoParameters);
}

Expand Down Expand Up @@ -389,7 +388,7 @@ private static List<ExecutableElement> findMethods(
return List.of();
}
return ElementFilter.methodsIn(context.getAllMembers(type)).stream()
.filter(context::isMemberAccessibleFromBuilderPackage)
.filter(context::isAccessibleFromGeneratedPackage)
.filter(method -> returnTypeMatcher.test(method.getReturnType()))
.filter(method -> hasParameters(method, expectedParameterTypes))
.toList();
Expand Down Expand Up @@ -510,7 +509,7 @@ public static List<ExecutableElement> findFieldFunctions(
return ElementFilter.methodsIn(context.getAllMembers(type)).stream()
.filter(JavaLangAnalyser::isNotStatic)
.filter(method -> isNoMethodOfObjectClass(method, context))
.filter(context::isMemberAccessibleFromBuilderPackage)
.filter(context::isAccessibleFromGeneratedPackage)
.filter(method -> method.getParameters().size() == 1)
.toList();
}
Expand Down Expand Up @@ -548,7 +547,7 @@ public static boolean hasConstructorAccepting(
return false;
}
return ElementFilter.constructorsIn(context.getAllMembers(builderType)).stream()
.filter(context::isMemberAccessibleFromBuilderPackage)
.filter(context::isAccessibleFromGeneratedPackage)
.anyMatch(constructor -> hasParameters(constructor, List.of(expectedType)));
}

Expand Down Expand Up @@ -847,24 +846,28 @@ public static Optional<ExecutableElement> 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<ExecutableElement> findConstructorForBuilder(
TypeElement annotatedType, ProcessingContext context) {
TypeElement annotatedType,
Class<? extends Annotation> constructorAnnotation,
ProcessingContext context) {
List<ExecutableElement> ctors =
ElementFilter.constructorsIn(context.getAllMembers(annotatedType)).stream()
.filter(context::isMemberAccessibleFromBuilderPackage)
.filter(context::isAccessibleFromGeneratedPackage)
.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);
}
}
Expand Down Expand Up @@ -894,6 +897,6 @@ public static Optional<ExecutableElement> findConstructorForBuilder(
public static boolean hasAccessibleConstructor(
TypeElement typeElement, ProcessingContext context) {
return ElementFilter.constructorsIn(context.getAllMembers(typeElement)).stream()
.anyMatch(context::isMemberAccessibleFromBuilderPackage);
.anyMatch(context::isAccessibleFromGeneratedPackage);
}
}
Loading
Loading