diff --git a/core/src/main/java/org/javahelpers/simple/builders/core/enums/AccessModifier.java b/core/src/main/java/org/javahelpers/simple/builders/core/enums/AccessModifier.java index 38a481c2..d2b4aa19 100644 --- a/core/src/main/java/org/javahelpers/simple/builders/core/enums/AccessModifier.java +++ b/core/src/main/java/org/javahelpers/simple/builders/core/enums/AccessModifier.java @@ -41,6 +41,9 @@ public enum AccessModifier { /** Public access - accessible from anywhere */ PUBLIC("public"), + /** Protected access - accessible from the same package and subclasses */ + PROTECTED("protected"), + /** Package-private access (default) - accessible only within the same package */ PACKAGE_PRIVATE(""), diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java index 94e880ca..0461b5df 100644 --- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java @@ -10,8 +10,6 @@ import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.function.Consumer; -import java.util.function.Supplier; import org.apache.commons.lang3.builder.ToStringBuilder; import org.javahelpers.simple.builders.core.util.BuilderToStringStyle; import org.javahelpers.simple.builders.core.util.TrackedValue; @@ -116,52 +114,20 @@ public BookDtoBuilder() { public BookDtoBuilder(BookDto instance) { this.author = initialValue(instance.getAuthor()); this.available = initialValue(instance.isAvailable()); - if (this.available.value() == null) { - throw new IllegalArgumentException( - "Cannot initialize builder from instance: field 'available' is marked as non-null but source object has null value"); - } this.category = initialValue(instance.getCategory()); - if (this.category.value() == null) { - throw new IllegalArgumentException( - "Cannot initialize builder from instance: field 'category' is marked as non-null but source object has null value"); - } this.discount = initialValue(instance.getDiscount()); - if (this.discount.value() == null) { - throw new IllegalArgumentException( - "Cannot initialize builder from instance: field 'discount' is marked as non-null but source object has null value"); - } this.edition = initialValue(instance.getEdition()); - if (this.edition.value() == null) { - throw new IllegalArgumentException( - "Cannot initialize builder from instance: field 'edition' is marked as non-null but source object has null value"); - } this.exactPrice = initialValue(instance.getExactPrice()); this.genres = initialValue(instance.getGenres()); this.isbn = initialValue(instance.getIsbn()); this.lastUpdated = initialValue(instance.getLastUpdated()); this.metadata = initialValue(instance.getMetadata()); this.pages = initialValue(instance.getPages()); - if (this.pages.value() == null) { - throw new IllegalArgumentException( - "Cannot initialize builder from instance: field 'pages' is marked as non-null but source object has null value"); - } this.price = initialValue(instance.getPrice()); - if (this.price.value() == null) { - throw new IllegalArgumentException( - "Cannot initialize builder from instance: field 'price' is marked as non-null but source object has null value"); - } this.publishDate = initialValue(instance.getPublishDate()); this.publisher = initialValue(instance.getPublisher()); this.rating = initialValue(instance.getRating()); - if (this.rating.value() == null) { - throw new IllegalArgumentException( - "Cannot initialize builder from instance: field 'rating' is marked as non-null but source object has null value"); - } this.salesCount = initialValue(instance.getSalesCount()); - if (this.salesCount.value() == null) { - throw new IllegalArgumentException( - "Cannot initialize builder from instance: field 'salesCount' is marked as non-null but source object has null value"); - } this.subtitle = initialValue(instance.getSubtitle()); this.tags = initialValue(instance.getTags()); this.title = initialValue(instance.getTitle()); diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/JacksonIntegrationDtoBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/JacksonIntegrationDtoBuilder.java index f430b042..b9aba88d 100644 --- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/JacksonIntegrationDtoBuilder.java +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/JacksonIntegrationDtoBuilder.java @@ -50,10 +50,6 @@ public JacksonIntegrationDtoBuilder() { public JacksonIntegrationDtoBuilder(JacksonIntegrationDto instance) { this.name = initialValue(instance.name()); this.age = initialValue(instance.age()); - if (this.age.value() == null) { - throw new IllegalArgumentException( - "Cannot initialize builder from instance: field 'age' is marked as non-null but source object has null value"); - } } /** diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/ProductRecordBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/ProductRecordBuilder.java index fc7d430a..adc17e48 100644 --- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/ProductRecordBuilder.java +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/ProductRecordBuilder.java @@ -51,10 +51,6 @@ public ProductRecordBuilder() { public ProductRecordBuilder(ProductRecord instance) { this.name = initialValue(instance.name()); this.price = initialValue(instance.price()); - if (this.price.value() == null) { - throw new IllegalArgumentException( - "Cannot initialize builder from instance: field 'price' is marked as non-null but source object has null value"); - } this.category = initialValue(instance.category()); } diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SimpleBuildersJacksonModule.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SimpleBuildersJacksonModule.java index 6e406c1a..f1ecab70 100644 --- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SimpleBuildersJacksonModule.java +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SimpleBuildersJacksonModule.java @@ -5,11 +5,11 @@ public class SimpleBuildersJacksonModule extends SimpleModule { - @JsonDeserialize(builder = JacksonIntegrationDtoBuilder.class) - private interface JacksonIntegrationDtoMixin { - } - public SimpleBuildersJacksonModule() { setMixInAnnotation(JacksonIntegrationDto.class, JacksonIntegrationDtoMixin.class); } + + @JsonDeserialize(builder = JacksonIntegrationDtoBuilder.class) + private interface JacksonIntegrationDtoMixin { + } } \ No newline at end of file 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 5ab749ff..07530341 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 @@ -47,6 +47,7 @@ import org.javahelpers.simple.builders.processor.generators.integration.JacksonModuleGenerator; import org.javahelpers.simple.builders.processor.model.core.BuilderConfiguration; import org.javahelpers.simple.builders.processor.model.core.BuilderDefinitionDto; +import org.javahelpers.simple.builders.processor.model.core.GenerationTargetClassDto; import org.javahelpers.simple.builders.processor.processing.BuilderConfigurationReader; import org.javahelpers.simple.builders.processor.processing.CompilerArgumentsEnum; import org.javahelpers.simple.builders.processor.processing.CompilerArgumentsReader; @@ -107,9 +108,18 @@ public boolean process(Set annotations, RoundEnvironment // Generate Jackson Module if processing is over and feature is enabled if (roundEnv.processingOver()) { - var modules = jacksonModuleGenerator.getModuleDefinitions(); - for (var module : modules) { - codeGenerator.generateJacksonModule(module); + List moduleClassDefs = + jacksonModuleGenerator.getModuleDefinitions(); + for (GenerationTargetClassDto moduleClassDef : moduleClassDefs) { + String packageName = moduleClassDef.getTypeName().getPackageName(); + context.info("Generating Jackson Module in package '%s'", packageName); + try { + codeGenerator.generateClass(moduleClassDef); + } catch (BuilderException e) { + context.warning( + "simple-builders: Error generating Jackson module for package %s: %s", + packageName, e.getMessage()); + } } // Reset indentation after Jackson module generation as well context.resetIndentation(); @@ -199,7 +209,7 @@ private void process(Element annotatedElement, BuilderConfiguration config) throws BuilderException { context.initConfigurationForProcessingTarget(config); BuilderDefinitionDto builderDef = extractFromElement(annotatedElement, context); - codeGenerator.generateBuilder(builderDef); + codeGenerator.generateClass(builderDef); // Collect info for Jackson Module if enabled jacksonModuleGenerator.addEntry(builderDef, annotatedElement); @@ -209,7 +219,7 @@ private void process(Element annotatedElement, BuilderConfiguration config) context.debugEndOperation( "Generated builder with %d fields and %d methods for %s", builderDef.getAllFieldsForBuilder().size(), - builderDef.getCoreMethods().size(), + builderDef.getMethods().size(), builderDef.getBuilderTypeName().getClassName()); } 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 4b42d3ba..3c5b66a6 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 @@ -93,6 +93,7 @@ public static Modifier mapAccessModifier(AccessModifier accessModifier) { } return switch (accessModifier) { case PUBLIC, DEFAULT -> Modifier.PUBLIC; + case PROTECTED -> Modifier.PROTECTED; case PRIVATE -> Modifier.PRIVATE; case PACKAGE_PRIVATE -> null; // Package-private has no explicit modifier }; 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 4360b33e..a718849a 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,55 +24,43 @@ package org.javahelpers.simple.builders.processor.classgen.roaster; -import static org.javahelpers.simple.builders.processor.classgen.roaster.RoasterMapper.mapBoxedType; import static org.javahelpers.simple.builders.processor.classgen.roaster.RoasterMapper.mapType; -import static org.javahelpers.simple.builders.processor.classgen.roaster.RoasterMapper.packageNameOf; import static org.javahelpers.simple.builders.processor.classgen.roaster.RoasterMapper.resolveCodeTemplate; import java.io.IOException; import java.io.InputStream; import java.io.Writer; -import java.util.HashMap; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; -import java.util.function.Consumer; -import java.util.function.Supplier; import javax.annotation.processing.ProcessingEnvironment; -import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; import javax.tools.JavaFileObject; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; -import org.javahelpers.simple.builders.core.util.TrackedValue; -import org.javahelpers.simple.builders.processor.analysis.JavaLangMapper; +import org.javahelpers.simple.builders.core.enums.AccessModifier; import org.javahelpers.simple.builders.processor.exceptions.BuilderException; import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto; import org.javahelpers.simple.builders.processor.model.annotation.InterfaceName; -import org.javahelpers.simple.builders.processor.model.core.BuilderDefinitionDto; -import org.javahelpers.simple.builders.processor.model.core.FieldDto; -import org.javahelpers.simple.builders.processor.model.integration.JacksonModuleDefinitionDto; -import org.javahelpers.simple.builders.processor.model.integration.JacksonModuleEntryDto; +import org.javahelpers.simple.builders.processor.model.core.ClassFieldDto; +import org.javahelpers.simple.builders.processor.model.core.GenerationTargetClassDto; +import org.javahelpers.simple.builders.processor.model.imports.ImportStatement; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocTagDto; -import org.javahelpers.simple.builders.processor.model.method.MethodCodePlaceholder; -import org.javahelpers.simple.builders.processor.model.method.MethodCodeTypePlaceholder; +import org.javahelpers.simple.builders.processor.model.method.ConstructorDto; +import org.javahelpers.simple.builders.processor.model.method.MethodCodeDto; import org.javahelpers.simple.builders.processor.model.method.MethodDto; import org.javahelpers.simple.builders.processor.model.method.MethodParameterDto; import org.javahelpers.simple.builders.processor.model.type.NestedTypeDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.model.type.TypeNameArray; -import org.javahelpers.simple.builders.processor.model.type.TypeNameGeneric; -import org.javahelpers.simple.builders.processor.model.type.TypeNamePrimitive; -import org.javahelpers.simple.builders.processor.model.type.TypeNameVariable; import org.javahelpers.simple.builders.processor.processing.ProcessingLogger; +import org.javahelpers.simple.builders.processor.util.ImportCollector; import org.jboss.forge.roaster.Roaster; import org.jboss.forge.roaster.model.source.AnnotationSource; import org.jboss.forge.roaster.model.source.FieldSource; import org.jboss.forge.roaster.model.source.JavaClassSource; -import org.jboss.forge.roaster.model.source.JavaDocSource; import org.jboss.forge.roaster.model.source.JavaInterfaceSource; import org.jboss.forge.roaster.model.source.JavaSource; import org.jboss.forge.roaster.model.source.MethodSource; @@ -105,72 +93,78 @@ public RoasterCodeGenerator(ProcessingEnvironment processingEnv, ProcessingLogge } /** - * Generates a builder class from the given builder definition. + * Generates a class from the given class definition. * - * @param builderDef dto of all information to create the builder + * @param classDef DTO containing all information to create the class * @throws BuilderException if there is an error in source code generation */ - public void generateBuilder(BuilderDefinitionDto builderDef) throws BuilderException { + public void generateClass(GenerationTargetClassDto classDef) throws BuilderException { logger.debugStartOperation( - "Code generation for builder: %s", builderDef.getBuilderTypeName().getClassName()); + "Code generation for class: %s", classDef.getTypeName().getClassName()); - String sourceCode = createBuilderSource(builderDef); - writeBuilderClassToFile(sourceCode, builderDef); + String sourceCode = createClassSource(classDef); + writeClassToFile(sourceCode, classDef); logger.debugEndOperation( - "Successfully generated builder: %s", builderDef.getBuilderTypeName().getClassName()); + "Successfully generated class: %s", classDef.getTypeName().getClassName()); } - private String createBuilderSource(BuilderDefinitionDto builderDef) { - JavaClassSource source = createClassSource(builderDef); - addClassMetadata(source, builderDef); - appendFields(source, builderDef); - appendConstructors(source, builderDef); - appendMethods(source, builderDef); - appendNestedTypes(source, builderDef); - applyClassAnnotations(source, builderDef); + private String createClassSource(GenerationTargetClassDto classDef) { + JavaClassSource source = createJavaClassSource(classDef); + addClassMetadata(source, classDef); + appendFields(source, classDef); + appendConstructors(source, classDef); + appendMethods(source, classDef); + appendNestedTypes(source, classDef); + applyClassAnnotations(source, classDef); return renderClassSource(source); } - private void applyClassAnnotations(JavaClassSource source, BuilderDefinitionDto builderDef) { - if (CollectionUtils.isEmpty(builderDef.getClassAnnotations())) { + private void applyClassAnnotations(JavaClassSource source, GenerationTargetClassDto classDef) { + if (CollectionUtils.isEmpty(classDef.getClassAnnotations())) { return; } - // Adding annotations from enhancers - applyAnnotations(source, builderDef.getClassAnnotations()); + // Adding class annotations + applyAnnotations(source, classDef.getClassAnnotations()); logger.debug("Class-level annotations added"); } - private JavaClassSource createClassSource(BuilderDefinitionDto builderDef) { - if (CollectionUtils.isNotEmpty(builderDef.getGenerics())) { - logger.debug("Builder has %d generic type parameter(s)", builderDef.getGenerics().size()); + private JavaClassSource createJavaClassSource(GenerationTargetClassDto classDef) { + if (CollectionUtils.isNotEmpty(classDef.getGenerics())) { + logger.debug("Class has %d generic type parameter(s)", classDef.getGenerics().size()); } JavaClassSource source = Roaster.create(JavaClassSource.class); - String packageName = builderDef.getBuilderTypeName().getPackageName(); + String packageName = classDef.getTypeName().getPackageName(); if (StringUtils.isNotBlank(packageName)) { source.setPackage(packageName); } else { source.setDefaultPackage(); } - source.setName(builderDef.getBuilderTypeName().getClassName()); - addGenericDeclarations(source, builderDef.getGenerics()); - addTrackedValueStaticImports(source); - collectImports(builderDef).stream().sorted().forEach(source::addImport); - logger.debug("Class builder created"); + source.setName(classDef.getTypeName().getClassName()); + addGenericDeclarations(source, classDef.getGenerics()); + + // Collect and add imports early (before elements are added) + Set importStmts = ImportCollector.collectAndSortImports(classDef); + for (ImportStatement importStmt : importStmts) { + if (importStmt.isStatic()) { + source.addImport(importStmt.getFullyQualifiedName()).setStatic(true); + } else { + source.addImport(importStmt.getFullyQualifiedName()); + } + } + + logger.debug("JavaClassSource created"); return source; } - private void addClassMetadata(JavaClassSource source, BuilderDefinitionDto builderDef) { - // Add class JavaDoc if provided by enhancer - applyJavadoc(source, builderDef.getClassJavadoc()); - - // Set builder class access level - applyVisibility( - source, JavaLangMapper.mapAccessModifier(builderDef.getConfiguration().getBuilderAccess())); + private void addClassMetadata(JavaClassSource source, GenerationTargetClassDto classDef) { + applyJavadoc(source, classDef.getClassJavadoc()); + applyVisibility(source, classDef.getClassAccessModifier()); + applySuperType(source, classDef.getSuperType()); - // Adding interfaces from enhancers - for (InterfaceName interfaceName : builderDef.getInterfaces()) { + // Add interfaces + for (InterfaceName interfaceName : classDef.getInterfaces()) { source.addInterface(RoasterMapper.mapInterfaceToTypeName(interfaceName)); } @@ -182,206 +176,94 @@ private String renderClassSource(JavaClassSource source) { return formatSource(rendered); } - private void appendFields(JavaClassSource source, BuilderDefinitionDto builderDef) { - logger.debugStartOperation( - "Generating %d constructor fields and %d setter fields", - builderDef.getConstructorFieldsForBuilder().size(), - builderDef.getSetterFieldsForBuilder().size()); + private void appendFields(JavaClassSource source, GenerationTargetClassDto classDef) { + logger.debugStartOperation("Generating %d fields", classDef.getClassFields().size()); - for (FieldDto fieldDto : builderDef.getConstructorFieldsForBuilder()) { - appendField(source, fieldDto); - } - for (FieldDto fieldDto : builderDef.getSetterFieldsForBuilder()) { + for (ClassFieldDto fieldDto : classDef.getClassFields()) { appendField(source, fieldDto); } - logger.debugEndOperation("Fields added: %d fields", builderDef.getAllFieldsForBuilder().size()); + logger.debugEndOperation("Fields added: %d fields", source.getFields().size()); } - private void appendField(JavaClassSource source, FieldDto fieldDto) { - String boxedFieldType = mapBoxedType(fieldDto.getFieldType()); + private void appendField(JavaClassSource source, ClassFieldDto fieldDto) { FieldSource field = source.addField(); - field.setName(fieldDto.getFieldNameInBuilder()); - field.setType(TrackedValue.class.getSimpleName() + "<" + boxedFieldType + ">"); - field.setPrivate(); - field.setLiteralInitializer("unsetValue()"); - applyJavaDocToField(field.getJavaDoc(), fieldDto); - } - - private void applyJavaDocToField(JavaDocSource javaDoc, FieldDto fieldDto) { - JavadocDto trackedValueJavadoc = fieldDto.getJavaDoc(); - if (trackedValueJavadoc != null) { - javaDoc.setText(trackedValueJavadoc.getDescription()); - } - } - - private void appendConstructors(JavaClassSource source, BuilderDefinitionDto builderDef) { - Modifier constructorAccessModifier = - JavaLangMapper.mapAccessModifier( - builderDef.getConfiguration().getBuilderConstructorAccess()); - TypeName dtoBaseClass = builderDef.getBuildingTargetTypeName(); - - appendEmptyConstructor(source, dtoBaseClass, constructorAccessModifier); - appendConstructorWithInstance( - source, dtoBaseClass, builderDef.getAllFieldsForBuilder(), constructorAccessModifier); - logger.debug("Constructors added"); - } - - private void appendEmptyConstructor( - JavaClassSource source, TypeName dtoClass, Modifier accessModifier) { - MethodSource constructor = source.addMethod(); - constructor.setConstructor(true); - applyVisibility(constructor, accessModifier); - constructor.setBody(""); - applyJavadoc( - constructor, - new JavadocDto( - "Empty constructor of builder for {@code %s}.", dtoClass.getFullQualifiedName())); + field.setName(fieldDto.getFieldName()); + field.setType(mapType(fieldDto.getFieldType())); + applyVisibility(field, fieldDto.getVisibility()); + applyLiteralInitializer(field, fieldDto.getLiteralInitializer()); + applyJavadoc(field, fieldDto.getJavadoc()); } - private void appendConstructorWithInstance( - JavaClassSource source, - TypeName dtoBaseClass, - List fields, - Modifier accessModifier) { - MethodSource constructor = source.addMethod(); - constructor.setConstructor(true); - applyVisibility(constructor, accessModifier); - constructor.addParameter(mapType(dtoBaseClass), "instance"); - constructor.setBody(buildConstructorBody(fields)); - applyJavadoc( - constructor, - new JavadocDto( - "Initialisation of builder for {@code %s} by a instance.", - dtoBaseClass.getFullQualifiedName()) - .addParam("instance", "object instance for initialisiation")); - } + private void appendConstructors(JavaClassSource source, GenerationTargetClassDto classDef) { + logger.debugStartOperation("Generating %d constructors", classDef.getConstructors().size()); - private String buildConstructorBody(List fields) { - StringBuilder body = new StringBuilder(); - for (FieldDto field : fields) { - field - .getGetterName() - .ifPresent(getter -> addFieldInitializationWithValidation(body, field, getter)); + for (ConstructorDto constructor : classDef.getConstructors()) { + appendConstructor(source, constructor); } - return body.toString(); - } - private void addFieldInitializationWithValidation( - StringBuilder body, FieldDto field, String getter) { - String fieldInBuilder = field.getFieldNameInBuilder(); - - String initialisationCode = - """ - this.%s = initialValue(instance.%s()); - """ - .formatted(fieldInBuilder, getter); - - String validationCode = ""; - if (field.isNonNullable()) { - validationCode = - """ + logger.debugEndOperation("Constructors added: %d", classDef.getConstructors().size()); + } - if (this.%s.value() == null) { - throw new IllegalArgumentException("Cannot initialize builder from instance: field '%s' is marked as non-null but source object has null value"); - } - """ - .formatted(fieldInBuilder, fieldInBuilder); + private void appendConstructor(JavaClassSource source, ConstructorDto constructor) { + MethodSource method = source.addMethod(); + method.setConstructor(true); + applyVisibility(method, constructor.getVisibility()); + for (MethodParameterDto param : constructor.getParameters()) { + method.addParameter(mapType(param.getParameterType()), param.getParameterName()); } - - body.append(initialisationCode).append(validationCode); + applyJavadoc(method, constructor.getJavadoc()); + applyCodeBlock(method, constructor.getMethodCodeDto()); } - private void appendMethods(JavaClassSource source, BuilderDefinitionDto builderDef) { - Map allMethods = collectAllMethods(builderDef); - logger.debugStartOperation("Adding Methods for %d candidates", allMethods.size()); + private void appendMethods(JavaClassSource source, GenerationTargetClassDto classDef) { + logger.debugStartOperation("Generating %d method candidates", classDef.getMethods().size()); - List resolvedMethods = resolveMethodConflicts(allMethods); - logger.debug("Resolved %d methods after conflict resolution", resolvedMethods.size()); + // Resolve method conflicts by signature and priority + List resolvedMethods = resolveMethodConflicts(classDef.getMethods()); + logger.debug("Resolved to %d methods after conflict resolution", resolvedMethods.size()); - int generatedCnt = 0; for (MethodDto methodDto : resolvedMethods) { appendMethod(source, methodDto, false, false); - generatedCnt++; - } - logger.debugEndOperation("%d Methods added", generatedCnt); - } - - private Map collectAllMethods(BuilderDefinitionDto builderDef) { - Map allMethods = new HashMap<>(); - - for (FieldDto fieldDto : builderDef.getConstructorFieldsForBuilder()) { - for (MethodDto method : fieldDto.getMethods()) { - allMethods.put(method, fieldDto); - } - } - for (FieldDto fieldDto : builderDef.getSetterFieldsForBuilder()) { - for (MethodDto method : fieldDto.getMethods()) { - allMethods.put(method, fieldDto); - } - } - for (MethodDto coreMethod : builderDef.getCoreMethods()) { - allMethods.put(coreMethod, null); } - return allMethods; + logger.debugEndOperation("Methods added: %d", resolvedMethods.size()); } - private List resolveMethodConflicts(Map methodToField) { + private List resolveMethodConflicts(List methods) { MethodDto.MethodComparator comparator = new MethodDto.MethodComparator(); - List> sortedEntries = - methodToField.entrySet().stream() - .sorted((e1, e2) -> comparator.compare(e1.getKey(), e2.getKey())) - .toList(); + // Sort methods by comparator for consistent ordering + List sortedMethods = methods.stream().sorted(comparator).toList(); Map signatureToMethod = new java.util.LinkedHashMap<>(); - for (Map.Entry entry : sortedEntries) { - MethodDto method = entry.getKey(); - FieldDto field = entry.getValue(); + for (MethodDto method : sortedMethods) { String signature = method.getSignatureKey(); MethodDto existing = signatureToMethod.get(signature); if (existing == null) { signatureToMethod.put(signature, method); } else { - String existingSource = - createSourceDescriptionForLogging(existing, methodToField.get(existing)); - String newSource = createSourceDescriptionForLogging(method, field); - if (method.getPriority() > existing.getPriority()) { signatureToMethod.put(signature, method); logger.warning( - " Method conflict: '%s' from %s (priority %d) dropped in favor of %s (priority %d)", - signature, existingSource, existing.getPriority(), newSource, method.getPriority()); + " Method conflict: '%s' (priority %d) dropped in favor of priority %d", + signature, existing.getPriority(), method.getPriority()); } else if (method.getPriority() < existing.getPriority()) { logger.warning( - " Method conflict: '%s' from %s (priority %d) dropped in favor of %s (priority %d)", - signature, newSource, method.getPriority(), existingSource, existing.getPriority()); + " Method conflict: '%s' (priority %d) dropped in favor of priority %d", + signature, method.getPriority(), existing.getPriority()); } else { logger.warning( - " Method conflict: '%s' from %s (priority %d) dropped in favor of %s (priority %d) - equal priority, keeping first", - signature, newSource, method.getPriority(), existingSource, existing.getPriority()); + " Method conflict: '%s' (priority %d) - equal priority, keeping first", + signature, method.getPriority()); } } } - return new java.util.ArrayList<>(signatureToMethod.values()); - } - - /** - * Creates a description string for method source identification. - * - * @param method method to describe - * @param field associated field (may be null) - * @return description string for logging/debugging - */ - public static String createSourceDescriptionForLogging(MethodDto method, FieldDto field) { - if (field == null) { - return "core method '" + method.getMethodName() + "'"; - } - return "field '" + field.getFieldNameInBuilder() + "'"; + // Sort the final result for reproducible output + return signatureToMethod.values().stream().sorted(comparator).toList(); } private void appendMethod( @@ -391,20 +273,15 @@ private void appendMethod( boolean interfaceMethod) { MethodSource method = source.addMethod(); configureMethod(method, methodDto, nestedTypeMethod, interfaceMethod); - String body = - methodDto.getMethodCodeDto() != null - && StringUtils.isNotBlank(methodDto.getMethodCodeDto().getCodeFormat()) - ? resolveCodeTemplate(methodDto.getMethodCodeDto()) - : ""; - method.setBody(body); + applyCodeBlock(method, methodDto.getMethodCodeDto()); } - private void appendNestedTypes(JavaClassSource source, BuilderDefinitionDto builderDef) { - if (CollectionUtils.isEmpty(builderDef.getNestedTypes())) { + private void appendNestedTypes(JavaClassSource source, GenerationTargetClassDto classDef) { + if (CollectionUtils.isEmpty(classDef.getNestedTypes())) { return; } - logger.debugStartOperation("Generating %d nested type(s)", builderDef.getNestedTypes().size()); - for (NestedTypeDto nestedType : builderDef.getNestedTypes()) { + logger.debugStartOperation("Generating %d nested type(s)", classDef.getNestedTypes().size()); + for (NestedTypeDto nestedType : classDef.getNestedTypes()) { appendNestedType(source, nestedType); logger.debug("Generated nested type: %s", nestedType.getTypeName()); } @@ -417,11 +294,8 @@ private void appendNestedType(JavaClassSource source, NestedTypeDto nestedType) ? source.addNestedType(JavaInterfaceSource.class) : source.addNestedType(JavaClassSource.class); nestedSource.setName(nestedType.getTypeName()); - if (nestedType.isPublic()) { - nestedSource.setPublic(); - } else { - nestedSource.setPackagePrivate(); - } + applyVisibility(nestedSource, nestedType.getVisibility()); + applyAnnotations(nestedSource, nestedType.getAnnotations()); applyJavadoc(nestedSource, nestedType.getJavadoc()); boolean isInterface = nestedType.getKind() == NestedTypeDto.NestedTypeKind.INTERFACE; for (MethodDto methodDto : nestedType.getMethods()) { @@ -444,7 +318,8 @@ private void appendNestedMethod(JavaSource source, MethodDto methodDto, boole } private void applyVisibility( - org.jboss.forge.roaster.model.source.VisibilityScopedSource source, Modifier modifier) { + org.jboss.forge.roaster.model.source.VisibilityScopedSource source, + AccessModifier modifier) { if (modifier == null) { source.setPackagePrivate(); return; @@ -453,7 +328,21 @@ private void applyVisibility( case PUBLIC -> source.setPublic(); case PROTECTED -> source.setProtected(); case PRIVATE -> source.setPrivate(); - default -> source.setPackagePrivate(); + case PACKAGE_PRIVATE -> source.setPackagePrivate(); + case DEFAULT -> source.setPackagePrivate(); // DEFAULT maps to PACKAGE_PRIVATE + } + } + + private void applySuperType(JavaClassSource source, TypeName superType) { + if (superType != null) { + source.setSuperType(mapType(superType)); + } + } + + private void applyLiteralInitializer( + FieldSource field, String literalInitializer) { + if (literalInitializer != null) { + field.setLiteralInitializer(literalInitializer); } } @@ -489,6 +378,15 @@ private void addGenericDeclarations( } } + private void applyCodeBlock(MethodSource method, MethodCodeDto codeDto) { + if (!codeDto.hasCode()) { + // If implementation is missing, an empty body needs to be set + method.setBody(""); + return; + } + method.setBody(resolveCodeTemplate(codeDto)); + } + private void applyJavadoc( org.jboss.forge.roaster.model.source.JavaDocCapableSource source, JavadocDto javadoc) { if (javadoc == null || !javadoc.hasContent()) { @@ -585,168 +483,6 @@ private void addParameter( applyAnnotations(parameter, paramDto.getAnnotations()); } - private void addTrackedValueStaticImports(JavaClassSource source) { - source.addImport(TrackedValue.class.getName() + ".changedValue").setStatic(true); - source.addImport(TrackedValue.class.getName() + ".initialValue").setStatic(true); - source.addImport(TrackedValue.class.getName() + ".unsetValue").setStatic(true); - } - - private Set collectImports(BuilderDefinitionDto builderDef) { - Set imports = new LinkedHashSet<>(); - String currentPackage = builderDef.getBuilderTypeName().getPackageName(); - - addImportIfNeeded(imports, currentPackage, TrackedValue.class.getName()); - addImportIfNeeded(imports, currentPackage, Consumer.class.getName()); - addImportIfNeeded(imports, currentPackage, Supplier.class.getName()); - addTypeImports(imports, currentPackage, builderDef.getBuildingTargetTypeName()); - addTypeImports(imports, currentPackage, builderDef.getBuilderTypeName()); - builderDef - .getGenerics() - .forEach( - generic -> - generic - .getUpperBounds() - .forEach(type -> addTypeImports(imports, currentPackage, type))); - builderDef - .getInterfaces() - .forEach(interfaceName -> addInterfaceImports(imports, currentPackage, interfaceName)); - builderDef - .getClassAnnotations() - .forEach(annotation -> addAnnotationImports(imports, currentPackage, annotation)); - builderDef - .getAllFieldsForBuilder() - .forEach(field -> addFieldImports(imports, currentPackage, field)); - builderDef - .getCoreMethods() - .forEach(method -> addMethodImports(imports, currentPackage, method)); - builderDef - .getNestedTypes() - .forEach( - nestedType -> - nestedType - .getMethods() - .forEach(method -> addMethodImports(imports, currentPackage, method))); - - return imports; - } - - private void addFieldImports(Set imports, String currentPackage, FieldDto field) { - addTypeImports(imports, currentPackage, field.getFieldType()); - field - .getParameterAnnotations() - .forEach(annotation -> addAnnotationImports(imports, currentPackage, annotation)); - field.getMethods().forEach(method -> addMethodImports(imports, currentPackage, method)); - } - - private void addMethodImports(Set imports, String currentPackage, MethodDto method) { - if (method.getReturnType() != null) { - addTypeImports(imports, currentPackage, method.getReturnType()); - } - method - .getGenericParameters() - .forEach( - generic -> - generic - .getUpperBounds() - .forEach(type -> addTypeImports(imports, currentPackage, type))); - method - .getAnnotations() - .forEach(annotation -> addAnnotationImports(imports, currentPackage, annotation)); - method - .getParameters() - .forEach(parameter -> addParameterImports(imports, currentPackage, parameter)); - addBodyImports(imports, currentPackage, method); - } - - private void addParameterImports( - Set imports, String currentPackage, MethodParameterDto parameter) { - addTypeImports(imports, currentPackage, parameter.getParameterType()); - parameter - .getAnnotations() - .forEach(annotation -> addAnnotationImports(imports, currentPackage, annotation)); - } - - private void addInterfaceImports( - Set imports, - String currentPackage, - org.javahelpers.simple.builders.processor.model.annotation.InterfaceName interfaceName) { - if (StringUtils.isNotBlank(interfaceName.getPackageName())) { - addImportIfNeeded( - imports, - currentPackage, - interfaceName.getPackageName() + "." + interfaceName.getSimpleName()); - } - interfaceName - .getAnnotations() - .forEach(annotation -> addAnnotationImports(imports, currentPackage, annotation)); - interfaceName - .getTypeParameters() - .forEach(type -> addTypeImports(imports, currentPackage, type)); - } - - private void addAnnotationImports( - Set imports, String currentPackage, AnnotationDto annotation) { - if (annotation.getAnnotationType() != null) { - addTypeImports(imports, currentPackage, annotation.getAnnotationType()); - } - } - - private void addTypeImports(Set imports, String currentPackage, TypeName type) { - if (type == null || type instanceof TypeNamePrimitive || type instanceof TypeNameVariable) { - return; - } - - type.getAnnotations() - .forEach(annotation -> addAnnotationImports(imports, currentPackage, annotation)); - - if (type instanceof TypeNameArray arrayType) { - addTypeImports(imports, currentPackage, arrayType.getTypeOfArray()); - return; - } - - if (type instanceof TypeNameGeneric genericType) { - addImportIfNeeded( - imports, currentPackage, genericType.getFullQualifiedName().replaceAll("<.*$", "")); - genericType - .getInnerTypeArguments() - .forEach(inner -> addTypeImports(imports, currentPackage, inner)); - return; - } - - addImportIfNeeded(imports, currentPackage, type.getFullQualifiedName()); - } - - private void addImportIfNeeded(Set imports, String currentPackage, String fqn) { - if (StringUtils.isBlank(fqn) - || !fqn.contains(".") - || fqn.startsWith("java.lang.") - || java.util.Objects.equals(packageNameOf(fqn), currentPackage)) { - return; - } - imports.add(fqn); - } - - private void addBodyImports(Set imports, String currentPackage, MethodDto method) { - if (method.getMethodCodeDto() == null - || StringUtils.isBlank(method.getMethodCodeDto().getCodeFormat())) { - return; - } - for (MethodCodePlaceholder argument : method.getMethodCodeDto().getCodeArguments()) { - if (argument instanceof MethodCodeTypePlaceholder typePlaceholder) { - addTypeImports(imports, currentPackage, typePlaceholder.getValue()); - } - } - String code = method.getMethodCodeDto().getCodeFormat(); - if (code.contains("List.of(")) { - imports.add(List.class.getName()); - } - if (code.contains("Optional.of(") - || code.contains("Optional.empty(") - || code.contains("Optional.ofNullable(")) { - imports.add(java.util.Optional.class.getName()); - } - } - private String formatSource(String rawSource) { if (formatterProperties.isEmpty()) { return rawSource; @@ -783,14 +519,13 @@ private Properties loadFormatterProperties() { } } - private void writeBuilderClassToFile(String sourceCode, BuilderDefinitionDto builderDef) + private void writeClassToFile(String sourceCode, GenerationTargetClassDto classDef) throws BuilderException { logger.debug( - "Writing builder class to file: %s.%s", - builderDef.getBuilderTypeName().getPackageName(), - builderDef.getBuilderTypeName().getClassName()); + "Writing class to file: %s.%s", + classDef.getTypeName().getPackageName(), classDef.getTypeName().getClassName()); - String qualifiedName = builderDef.getBuilderTypeName().getFullQualifiedName(); + String qualifiedName = classDef.getTypeName().getFullQualifiedName(); if (builderClassAlreadyExists(qualifiedName)) { throw new BuilderException( null, @@ -839,112 +574,4 @@ private boolean builderClassAlreadyExists(String qualifiedName) { return false; } } - - /** - * Generates a Jackson SimpleModule based on the provided definition. - * - * @param moduleDef the definition of the Jackson module to generate - */ - public void generateJacksonModule(JacksonModuleDefinitionDto moduleDef) { - String packageName = moduleDef.getTargetPackage(); - String moduleClassName = "SimpleBuildersJacksonModule"; - - logger.info("Generating Jackson Module '%s' in package '%s'", moduleClassName, packageName); - - try { - // Create the class using Roaster - JavaClassSource moduleClass = Roaster.create(JavaClassSource.class); - moduleClass.setPackage(packageName); - moduleClass.setName(moduleClassName); - moduleClass.setSuperType("SimpleModule"); - - // Collect imports in a list, sort them, then add to Roaster - Set imports = new LinkedHashSet<>(); - imports.add("com.fasterxml.jackson.databind.annotation.JsonDeserialize"); - imports.add("com.fasterxml.jackson.databind.module.SimpleModule"); - - // Adding imports for DTO types - moduleDef.getEntries().stream() - .filter(e -> shouldAddImport(packageName, e.dtoType().getFullQualifiedName())) - .forEach(e -> imports.add(e.dtoType().getFullQualifiedName())); - - // Adding imports for builder types - moduleDef.getEntries().stream() - .filter(e -> shouldAddImport(packageName, e.builderType().getFullQualifiedName())) - .forEach(e -> imports.add(e.builderType().getFullQualifiedName())); - - // Sort imports and add to Roaster - imports.stream().sorted().forEach(moduleClass::addImport); - - // Add mixin interfaces as nested interfaces - for (JacksonModuleEntryDto entry : moduleDef.getEntries()) { - String mixinName = entry.dtoType().getClassName() + "Mixin"; - - // Create the nested interface - JavaInterfaceSource mixinInterface = Roaster.create(JavaInterfaceSource.class); - mixinInterface.setName(mixinName); - mixinInterface.setPrivate(); - - // Add the JsonDeserialize annotation - AnnotationSource annotation = - mixinInterface.addAnnotation("JsonDeserialize"); - annotation.setLiteralValue("builder", entry.builderType().getClassName() + ".class"); - - // Add as nested type to the module class - moduleClass.addNestedType(mixinInterface); - } - - // Add constructor - MethodSource constructor = moduleClass.addMethod(); - constructor.setConstructor(true); - constructor.setPublic(); - - // Build constructor body - StringBuilder constructorBody = new StringBuilder(); - for (JacksonModuleEntryDto entry : moduleDef.getEntries()) { - String mixinName = entry.dtoType().getClassName() + "Mixin"; - constructorBody.append( - "setMixInAnnotation(%s.class, %s.class);%n" - .formatted(entry.dtoType().getClassName(), mixinName)); - } - constructor.setBody(constructorBody.toString()); - - // Write the generated class - String source = formatSource(moduleClass.toString()); - writeSimpleClassToFile(packageName, moduleClassName, source); - - } catch (BuilderException e) { - logger.warning( - "simple-builders: Error generating Jackson module for package %s: %s\n%s", - packageName, e.getMessage(), java.util.Arrays.toString(e.getStackTrace())); - } - } - - private boolean shouldAddImport(String currentPackage, String fqn) { - return StringUtils.isNotBlank(fqn) - && fqn.contains(".") - && !fqn.startsWith("java.lang.") - && !currentPackage.equals(packageNameOf(fqn)); - } - - private void writeSimpleClassToFile(String packageName, String className, String sourceCode) - throws BuilderException { - try { - String qualifiedName = - StringUtils.isBlank(packageName) ? className : packageName + "." + className; - JavaFileObject file = processingEnv.getFiler().createSourceFile(qualifiedName); - try (Writer writer = file.openWriter()) { - writer.write(sourceCode); - } - } catch (IOException ex) { - String message = ex.getMessage(); - String errorMessage = - """ - Unable to create class: %s. - Check the build environment and ensure all necessary directories are accessible. - """ - .formatted(StringUtils.isNotBlank(message) ? message : "Unknown error"); - throw new BuilderException(null, errorMessage); - } - } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ConditionalEnhancer.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ConditionalEnhancer.java index 59267d1f..94a38881 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ConditionalEnhancer.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ConditionalEnhancer.java @@ -24,7 +24,7 @@ package org.javahelpers.simple.builders.processor.generators.builder; import java.util.function.BooleanSupplier; -import javax.lang.model.element.Modifier; +import org.javahelpers.simple.builders.core.enums.AccessModifier; import org.javahelpers.simple.builders.processor.analysis.JavaLangMapper; import org.javahelpers.simple.builders.processor.generators.BuilderEnhancer; import org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil; @@ -104,11 +104,11 @@ public boolean appliesTo( public void enhanceBuilder(BuilderDefinitionDto builderDto, ProcessingContext context) { // Add conditional(BooleanSupplier, Consumer, Consumer) method MethodDto conditionalMethod = createConditionalMethod(builderDto); - builderDto.addCoreMethod(conditionalMethod); + builderDto.addMethod(conditionalMethod); // Add conditional(BooleanSupplier, Consumer) method MethodDto conditionalPositiveMethod = createConditionalPositiveOnlyMethod(builderDto); - builderDto.addCoreMethod(conditionalPositiveMethod); + builderDto.addMethod(conditionalPositiveMethod); context.debug( "Added conditional methods to builder %s", builderDto.getBuilderTypeName().getClassName()); @@ -119,7 +119,7 @@ private MethodDto createConditionalMethod(BuilderDefinitionDto builderDto) { MethodDto method = new MethodDto("conditional", builderDto.getBuilderTypeName()); method.setOrdering(ORDERING_CONDITIONAL); method.setPriority(MethodDto.PRIORITY_HIGHEST); - method.setModifier(Modifier.PUBLIC); + method.setModifier(AccessModifier.PUBLIC); // Add parameters addConditionalPositiveNegativeParameters(method, builderDto.getBuilderTypeName()); @@ -151,7 +151,7 @@ private MethodDto createConditionalPositiveOnlyMethod(BuilderDefinitionDto build MethodDto method = new MethodDto("conditional", builderDto.getBuilderTypeName()); method.setOrdering(ORDERING_CONDITIONAL_POSITIVE_ONLY); method.setPriority(MethodDto.PRIORITY_HIGHEST); - method.setModifier(Modifier.PUBLIC); + method.setModifier(AccessModifier.PUBLIC); // Add parameters addConditionalPositiveOnlyParameters(method, builderDto.getBuilderTypeName()); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ConstructorEnhancer.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ConstructorEnhancer.java new file mode 100644 index 00000000..89f46bd5 --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/ConstructorEnhancer.java @@ -0,0 +1,138 @@ +/* + * MIT License + * + * Copyright (c) 2026 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.processor.generators.builder; + +import org.javahelpers.simple.builders.processor.generators.BuilderEnhancer; +import org.javahelpers.simple.builders.processor.model.core.BuilderDefinitionDto; +import org.javahelpers.simple.builders.processor.model.core.FieldDto; +import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; +import org.javahelpers.simple.builders.processor.model.method.ConstructorDto; +import org.javahelpers.simple.builders.processor.model.method.MethodCodeDto; +import org.javahelpers.simple.builders.processor.model.method.MethodParameterDto; +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; + +/** + * Enhancer that adds builder constructors. + * + *

This enhancer generates the two builder constructors: + * + *

    + *
  • Empty constructor - for creating a new builder from scratch + *
  • From-instance constructor - for creating a builder initialized from an existing DTO + * instance + *
+ * + *

Priority: 95 (runs after CoreMethodsEnhancer at 100) + */ +public class ConstructorEnhancer implements BuilderEnhancer { + + private static final int PRIORITY = 95; + + @Override + public int getPriority() { + return PRIORITY; + } + + @Override + public boolean appliesTo( + BuilderDefinitionDto builderDto, TypeName dtoType, ProcessingContext context) { + return true; // Constructors are always needed + } + + @Override + public void enhanceBuilder(BuilderDefinitionDto builderDto, ProcessingContext context) { + builderDto.addConstructor(createEmptyConstructor(builderDto)); + builderDto.addConstructor(createFromInstanceConstructor(builderDto)); + } + + /** Creates the empty constructor. */ + protected ConstructorDto createEmptyConstructor(BuilderDefinitionDto builderDto) { + ConstructorDto constructor = new ConstructorDto(); + constructor.setVisibility(builderDto.getConfiguration().getBuilderConstructorAccess()); + + String targetFullName = builderDto.getBuildingTargetTypeName().getFullQualifiedName(); + constructor.setJavadoc( + new JavadocDto("Empty constructor of builder for {@code %s}.", targetFullName)); + + return constructor; + } + + /** Creates the from-instance constructor. */ + protected ConstructorDto createFromInstanceConstructor(BuilderDefinitionDto builderDto) { + ConstructorDto constructor = new ConstructorDto(); + constructor.setVisibility(builderDto.getConfiguration().getBuilderConstructorAccess()); + + // Add parameter for the DTO instance + MethodParameterDto instanceParam = new MethodParameterDto(); + instanceParam.setParameterName("instance"); + instanceParam.setParameterTypeName(builderDto.getBuildingTargetTypeName()); + constructor.addParameter(instanceParam); + + // Build constructor code body + MethodCodeDto codeDto = new MethodCodeDto(); + constructor.setMethodCodeDto(codeDto); + + // Initialize fields from instance + for (FieldDto field : builderDto.getAllFieldsForBuilder()) { + String fieldName = field.getFieldNameInBuilder(); + + // getGetterName() returns Optional + field + .getGetterName() + .ifPresent( + getterName -> { + // Field has a getter - use it + if (field.isNonNullable() && !(field.getFieldType() instanceof TypeNamePrimitive)) { + // Non-nullable non-primitive field - validate not null + // Skip primitives as they can't be compared to null (compilation error) + codeDto.append( + """ + if (instance.%s() == null) { + throw new IllegalArgumentException("Field '%s' is non-null but instance.%s() returned null"); + } + """, + getterName, fieldName, getterName); + codeDto.addCodeBlockImport(IllegalArgumentException.class); + } + codeDto.append( + """ + this.%s = TrackedValue.initialValue(instance.%s()); + """ + .formatted(fieldName, getterName)); + }); + // If no getter available - cannot initialize this field + // Leave it unset (will use unsetValue() from field initializer) + } + + String targetFullName = builderDto.getBuildingTargetTypeName().getFullQualifiedName(); + constructor.setJavadoc( + new JavadocDto("Initialisation of builder for {@code %s} by a instance.", targetFullName) + .addParam("instance", "object instance for initialisiation")); + + return constructor; + } +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java index c7aeef7b..44ed603f 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/CoreMethodsEnhancer.java @@ -25,7 +25,7 @@ import java.util.ArrayList; import java.util.List; -import javax.lang.model.element.Modifier; +import org.javahelpers.simple.builders.core.enums.AccessModifier; import org.javahelpers.simple.builders.processor.analysis.JavaLangMapper; import org.javahelpers.simple.builders.processor.generators.BuilderEnhancer; import org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil; @@ -101,15 +101,15 @@ public boolean appliesTo( public void enhanceBuilder(BuilderDefinitionDto builderDto, ProcessingContext context) { // Add build() method MethodDto buildMethod = createBuildMethod(builderDto); - builderDto.addCoreMethod(buildMethod); + builderDto.addMethod(buildMethod); // Add static create() method MethodDto createMethod = createStaticCreateMethod(builderDto); - builderDto.addCoreMethod(createMethod); + builderDto.addMethod(createMethod); // Add toString() method MethodDto toStringMethod = createToStringMethod(builderDto); - builderDto.addCoreMethod(toStringMethod); + builderDto.addMethod(toStringMethod); } /** Creates the build() method. */ @@ -120,7 +120,7 @@ protected MethodDto createBuildMethod(BuilderDefinitionDto builderDto) { MethodDto method = new MethodDto("build", returnType); method.setOrdering(ORDERING_BUILD); method.setPriority(MethodDto.PRIORITY_HIGHEST); - method.setModifier(Modifier.PUBLIC); + method.setModifier(AccessModifier.PUBLIC); // Add @Override annotation only if implementing IBuilderBase interface if (builderDto.getConfiguration().shouldImplementBuilderBase()) { @@ -153,6 +153,9 @@ protected MethodDto createBuildMethod(BuilderDefinitionDto builderDto) { } // Add validation for non-nullable setter fields + // Note: Primitives are stored as boxed types in TrackedValue, etc. + // They can be null via Supplier methods: builder.pages(() -> null) + // So we need null checks for ALL non-nullable fields, including primitives for (var field : builderDto.getSetterFieldsForBuilder()) { if (field.isNonNullable()) { code.append("if (this.") @@ -193,7 +196,7 @@ protected MethodDto createBuildMethod(BuilderDefinitionDto builderDto) { method.setCode(code.toString()); method.addArgument("dtoBaseType", builderDto.getBuildingTargetTypeName()); method.addArgument("buildResultType", returnType); - + method.getMethodCodeDto().addCodeBlockImport(IllegalStateException.class); method.setJavadoc(new JavadocDto("Builds the configured DTO instance.")); return method; @@ -207,7 +210,7 @@ protected MethodDto createStaticCreateMethod(BuilderDefinitionDto builderDto) { MethodDto method = new MethodDto("create", returnType); method.setOrdering(ORDERING_CREATE); method.setPriority(MethodDto.PRIORITY_HIGHEST); - method.setModifier(Modifier.PUBLIC); + method.setModifier(AccessModifier.PUBLIC); method.setStatic(true); // Use appropriate code template based on whether we have generics @@ -235,10 +238,10 @@ protected MethodDto createStaticCreateMethod(BuilderDefinitionDto builderDto) { /** Creates the toString() method. */ protected MethodDto createToStringMethod(BuilderDefinitionDto builderDto) { - MethodDto method = new MethodDto("toString", new TypeName("java.lang", "String")); + MethodDto method = new MethodDto("toString", TypeName.of(String.class)); method.setOrdering(ORDERING_TO_STRING); method.setPriority(MethodDto.PRIORITY_HIGHEST); - method.setModifier(Modifier.PUBLIC); + method.setModifier(AccessModifier.PUBLIC); AnnotationDto overrideAnnotation = new AnnotationDto(); overrideAnnotation.setAnnotationType(JavaLangMapper.map2TypeName(Override.class)); method.addAnnotation(overrideAnnotation); @@ -250,11 +253,15 @@ protected MethodDto createToStringMethod(BuilderDefinitionDto builderDto) { + "\n .toString();"); // Add template arguments for code generation - method.addArgument( - "toStringBuilder", new TypeName("org.apache.commons.lang3.builder", "ToStringBuilder")); - method.addArgument( - "toStringStyle", - new TypeName("org.javahelpers.simple.builders.core.util", "BuilderToStringStyle")); + TypeName toStringBuilderType = + new TypeName("org.apache.commons.lang3.builder", "ToStringBuilder"); + TypeName toStringStyleType = + new TypeName("org.javahelpers.simple.builders.core.util", "BuilderToStringStyle"); + + method.addArgument("toStringBuilder", toStringBuilderType); + method.addArgument("toStringStyle", toStringStyleType); + method.getMethodCodeDto().addCodeBlockImport(toStringBuilderType); + method.getMethodCodeDto().addCodeBlockImport(toStringStyleType); method.setJavadoc( new JavadocDto( diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/WithInterfaceEnhancer.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/WithInterfaceEnhancer.java index 545472fd..5344d746 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/WithInterfaceEnhancer.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/builder/WithInterfaceEnhancer.java @@ -23,6 +23,7 @@ */ package org.javahelpers.simple.builders.processor.generators.builder; +import org.javahelpers.simple.builders.core.enums.AccessModifier; import org.javahelpers.simple.builders.processor.analysis.JavaLangMapper; import org.javahelpers.simple.builders.processor.generators.BuilderEnhancer; import org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil; @@ -113,7 +114,7 @@ private NestedTypeDto createWithInterface(BuilderDefinitionDto builderDto) { NestedTypeDto withInterface = new NestedTypeDto(); withInterface.setTypeName("With"); withInterface.setKind(NestedTypeDto.NestedTypeKind.INTERFACE); - withInterface.setPublic(true); + withInterface.setVisibility(AccessModifier.PUBLIC); withInterface.setJavadoc( new JavadocDto( "Interface that can be implemented by the DTO to provide fluent modification methods.")); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/AddToCollectionGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/AddToCollectionGenerator.java index 3d91e0d9..e835219b 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/AddToCollectionGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/AddToCollectionGenerator.java @@ -146,7 +146,7 @@ private MethodDto createAddToCollectionMethod( parameter.setParameterTypeName(elementType); methodDto.addParameter(parameter); - setMethodAccessModifier(methodDto, getMethodAccessModifier(context)); + methodDto.setModifier(getMethodAccessModifier(context)); String collectionImpl; TypeName collectionVarType; @@ -172,11 +172,14 @@ private MethodDto createAddToCollectionMethod( this.$fieldName:N = $builderFieldWrapper:T.changedValue(newCollection); return this; """); + TypeName collectionImplType = new TypeName("java.util", collectionImpl); + methodDto.addArgument("collectionVarType", collectionVarType); - methodDto.addArgument("collectionImpl", new TypeName("java.util", collectionImpl)); + methodDto.addArgument("collectionImpl", collectionImplType); methodDto.addArgument("fieldName", fieldNameInBuilder); methodDto.addArgument("elementType", elementType); methodDto.addArgument("builderFieldWrapper", TRACKED_VALUE_TYPE); + methodDto.getMethodCodeDto().addCodeBlockImport(collectionImplType); methodDto.setPriority(MethodDto.PRIORITY_MEDIUM); methodDto.setJavadoc( diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ArrayBuilderConsumerGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ArrayBuilderConsumerGenerator.java index 02f59310..d6d02486 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ArrayBuilderConsumerGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ArrayBuilderConsumerGenerator.java @@ -132,14 +132,20 @@ private MethodDto createFieldConsumerWithArrayBuilder( MethodDto methodDto = new MethodDto(generateBuilderMethodName(fieldName, context), returnBuilderType); methodDto.addParameter(parameter); - setMethodAccessModifier(methodDto, getMethodAccessModifier(context)); + methodDto.setModifier(getMethodAccessModifier(context)); methodDto.setCode( """ - $helperType:T builder = this.$fieldName:N.isSet() ? new $helperType:T(java.util.List.of(this.$fieldName:N.value())) : new $helperType:T(); + $helperType:T builder = this.$fieldName:N.isSet() + ? new $helperType:T(java.util.List.of(this.$fieldName:N.value())) + : new $helperType:T(); $dtoMethodParam:N.accept(builder); this.$fieldName:N = $builderFieldWrapper:T.changedValue(builder.build().toArray(new $elementType:T[0])); return this; """); + + // Add code block import for java.util.List.of + methodDto.getMethodCodeDto().addCodeBlockImport(List.class); + methodDto.addArgument("fieldName", fieldNameInBuilder); methodDto.addArgument("dtoMethodParam", parameter.getParameterName()); methodDto.addArgument("helperType", builderTypeGeneric); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ArrayConversionGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ArrayConversionGenerator.java index 927f06c7..379172ad 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ArrayConversionGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/ArrayConversionGenerator.java @@ -119,7 +119,7 @@ private MethodDto createFieldSetterForArrayFromList( MethodDto methodDto = new MethodDto(generateBuilderMethodName(fieldName, context), builderType); methodDto.addParameter(parameter); - setMethodAccessModifier(methodDto, getMethodAccessModifier(context)); + methodDto.setModifier(getMethodAccessModifier(context)); methodDto.setCode( """ this.$fieldName:N = $builderFieldWrapper:T.changedValue($dtoMethodParams:N.toArray(new $elementType:T[0])); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/OptionalHelperGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/OptionalHelperGenerator.java index 499cd4df..d60bb31e 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/OptionalHelperGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/OptionalHelperGenerator.java @@ -28,6 +28,7 @@ import java.util.Collections; import java.util.List; +import java.util.Optional; import org.apache.commons.collections4.CollectionUtils; import org.javahelpers.simple.builders.processor.generators.MethodGenerator; import org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil; @@ -105,6 +106,9 @@ public List generateMethods( MethodGeneratorUtil.createBuilderMethodForFieldWithTransform( field, "Optional.ofNullable(%s)", innerType, builderType, context); + // Add code block import for Optional.ofNullable + method.getMethodCodeDto().addCodeBlockImport(Optional.class); + return Collections.singletonList(method); } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringBuilderConsumerGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringBuilderConsumerGenerator.java index 7735031f..49eea86f 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringBuilderConsumerGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringBuilderConsumerGenerator.java @@ -29,6 +29,7 @@ import static org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil.*; import java.util.List; +import java.util.Optional; import org.javahelpers.simple.builders.processor.generators.MethodGenerator; import org.javahelpers.simple.builders.processor.generators.util.JavadocConstants; import org.javahelpers.simple.builders.processor.model.core.FieldDto; @@ -118,6 +119,12 @@ public List generateMethods( transform, builderType, context); + + // Add code block import for Optional.of when used + if (isOptionalString(field.getFieldType())) { + method.getMethodCodeDto().addCodeBlockImport(Optional.class); + } + return List.of(method); } @@ -135,7 +142,7 @@ private MethodDto createStringBuilderConsumer( parameter.setParameterTypeName(consumerType); MethodDto methodDto = new MethodDto(generateBuilderMethodName(fieldName, context), builderType); methodDto.addParameter(parameter); - setMethodAccessModifier(methodDto, getMethodAccessModifier(context)); + methodDto.setModifier(getMethodAccessModifier(context)); methodDto.setCode( """ StringBuilder builder = new StringBuilder(); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringFormatHelperGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringFormatHelperGenerator.java index b6182c0b..5ee2fd9f 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringFormatHelperGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/StringFormatHelperGenerator.java @@ -31,6 +31,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Optional; import org.apache.commons.collections4.CollectionUtils; import org.javahelpers.simple.builders.processor.generators.MethodGenerator; import org.javahelpers.simple.builders.processor.generators.util.JavadocConstants; @@ -139,6 +140,10 @@ public List generateMethods( field.getParameterAnnotations(), builderType, context); + + // Add code block imports for Optional.of and String.format + method.getMethodCodeDto().addCodeBlockImport(Optional.class); + methods.add(method); } } @@ -180,7 +185,7 @@ private MethodDto createStringFormatMethodWithTransform( MethodDto methodDto = new MethodDto(generateBuilderMethodName(fieldName, context), builderType); methodDto.addParameter(formatParam); methodDto.addParameter(argsParam); - setMethodAccessModifier(methodDto, getMethodAccessModifier(context)); + methodDto.setModifier(getMethodAccessModifier(context)); methodDto.setCode( """ this.$fieldName:N = $builderFieldWrapper:T.changedValue($transform:N); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/SupplierMethodGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/SupplierMethodGenerator.java index 8e8ef06d..101468dd 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/SupplierMethodGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/SupplierMethodGenerator.java @@ -133,7 +133,7 @@ private MethodDto createFieldSupplier( MethodDto methodDto = new MethodDto(generateBuilderMethodName(fieldName, context), builderType); methodDto.addParameter(parameter); - setMethodAccessModifier(methodDto, getMethodAccessModifier(context)); + methodDto.setModifier(getMethodAccessModifier(context)); methodDto.setCode( """ diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/VarArgsHelperGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/VarArgsHelperGenerator.java index 0c82b18f..3ef380d2 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/VarArgsHelperGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/field/VarArgsHelperGenerator.java @@ -26,6 +26,8 @@ import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.Set; import org.javahelpers.simple.builders.processor.generators.MethodGenerator; import org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil; import org.javahelpers.simple.builders.processor.model.core.FieldDto; @@ -151,7 +153,19 @@ private MethodDto createFieldSetterByVarArgs( } String transform = MethodGeneratorUtil.wrapConcreteCollectionType(fieldType, baseExpression); - return MethodGeneratorUtil.createBuilderMethodForFieldWithTransform( - field, transform, parameterType, builderType, context); + MethodDto method = + MethodGeneratorUtil.createBuilderMethodForFieldWithTransform( + field, transform, parameterType, builderType, context); + + // Add code block imports for collection factory methods + if (fieldType instanceof TypeNameList) { + method.getMethodCodeDto().addCodeBlockImport(List.class); + } else if (fieldType instanceof TypeNameSet) { + method.getMethodCodeDto().addCodeBlockImport(Set.class); + } else if (fieldType instanceof TypeNameMap) { + method.getMethodCodeDto().addCodeBlockImport(Map.class); + } + + return method; } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/integration/JacksonModuleGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/integration/JacksonModuleGenerator.java index 41c56bf0..6c7e96a0 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/integration/JacksonModuleGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/integration/JacksonModuleGenerator.java @@ -31,10 +31,16 @@ import javax.lang.model.element.Element; import org.apache.commons.collections4.SetValuedMap; import org.apache.commons.collections4.multimap.HashSetValuedHashMap; +import org.javahelpers.simple.builders.core.enums.AccessModifier; +import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto; import org.javahelpers.simple.builders.processor.model.core.BuilderConfiguration; import org.javahelpers.simple.builders.processor.model.core.BuilderDefinitionDto; -import org.javahelpers.simple.builders.processor.model.integration.JacksonModuleDefinitionDto; +import org.javahelpers.simple.builders.processor.model.core.GenerationTargetClassDto; import org.javahelpers.simple.builders.processor.model.integration.JacksonModuleEntryDto; +import org.javahelpers.simple.builders.processor.model.method.ConstructorDto; +import org.javahelpers.simple.builders.processor.model.method.MethodCodeDto; +import org.javahelpers.simple.builders.processor.model.type.NestedTypeDto; +import org.javahelpers.simple.builders.processor.model.type.TypeName; import org.javahelpers.simple.builders.processor.processing.ProcessingLogger; /** @@ -45,6 +51,14 @@ */ public class JacksonModuleGenerator { + /** Simple name of the generated Jackson module class. */ + public static final String MODULE_CLASS_NAME = "SimpleBuildersJacksonModule"; + + private static final TypeName SIMPLE_MODULE_TYPE = + new TypeName("com.fasterxml.jackson.databind.module", "SimpleModule"); + private static final TypeName JSON_DESERIALIZE_TYPE = + new TypeName("com.fasterxml.jackson.databind.annotation", "JsonDeserialize"); + private final ProcessingEnvironment processingEnv; private final ProcessingLogger logger; private final SetValuedMap entriesByPackage = @@ -109,14 +123,17 @@ private boolean validateForModuleGeneration(BuilderConfiguration config, Element } /** - * Returns the definitions for the Jackson Modules to be generated. + * Returns the fully-built target class definitions for the Jackson Modules to be generated. + * + *

Collected entries are assembled into generic {@link GenerationTargetClassDto} instances + * ready to be rendered by the shared {@code RoasterCodeGenerator.generateClass} pipeline. * *

Logs status and clears state after retrieval. * - * @return list of JacksonModuleDefinitionDto + * @return list of target class definitions, one per package */ - public List getModuleDefinitions() { - List definitions = new ArrayList<>(); + public List getModuleDefinitions() { + List definitions = new ArrayList<>(); if (!entriesByPackage.isEmpty()) { logger.info( "simple-builders: Processing OVER. JacksonModuleEnabled: true, Packages: %d", @@ -124,15 +141,73 @@ public List getModuleDefinitions() { for (String packageName : entriesByPackage.keySet()) { Set moduleEntries = entriesByPackage.get(packageName); - JacksonModuleDefinitionDto definition = new JacksonModuleDefinitionDto(packageName); - definition.addAllEntries(moduleEntries); - definitions.add(definition); + definitions.add(buildTargetClass(packageName, moduleEntries)); } } clear(); return definitions; } + /** + * Builds a generic {@link GenerationTargetClassDto} for the Jackson module of the given package. + * + * @param packageName target package of the generated module class + * @param entries DTO/builder type pairs to register as mixins + * @return a fully populated class definition ready for code generation + */ + private GenerationTargetClassDto buildTargetClass( + String packageName, Set entries) { + GenerationTargetClassDto classDef = new GenerationTargetClassDto(); + classDef.setTypeName(new TypeName(packageName, MODULE_CLASS_NAME)); + classDef.setClassAccessModifier(AccessModifier.PUBLIC); + classDef.setSuperType(SIMPLE_MODULE_TYPE); + + // Each entry becomes a private nested mixin interface annotated with @JsonDeserialize. + for (JacksonModuleEntryDto entry : entries) { + classDef.addNestedType(createMixinInterface(entry)); + // The builder type appears as a literal in the @JsonDeserialize(builder = ...) member + // and therefore must be imported explicitly. + classDef.addImport(entry.builderType()); + } + + classDef.addConstructor(createConstructor(entries)); + + return classDef; + } + + private static NestedTypeDto createMixinInterface(JacksonModuleEntryDto entry) { + NestedTypeDto mixin = new NestedTypeDto(); + mixin.setTypeName(entry.dtoType().getClassName() + "Mixin"); + mixin.setKind(NestedTypeDto.NestedTypeKind.INTERFACE); + mixin.setVisibility(AccessModifier.PRIVATE); + + AnnotationDto jsonDeserialize = new AnnotationDto(); + jsonDeserialize.setAnnotationType(JSON_DESERIALIZE_TYPE); + jsonDeserialize.addMember("builder", entry.builderType().getClassName() + ".class"); + mixin.addAnnotation(jsonDeserialize); + + return mixin; + } + + private static ConstructorDto createConstructor(Set entries) { + ConstructorDto constructor = new ConstructorDto(); + constructor.setVisibility(AccessModifier.PUBLIC); + + MethodCodeDto code = new MethodCodeDto(); + int index = 0; + for (JacksonModuleEntryDto entry : entries) { + String dtoLabel = "dto" + index; + String mixinLabel = "mixin" + index; + code.addArgument(dtoLabel, entry.dtoType()); + code.addArgument(mixinLabel, entry.dtoType().getClassName() + "Mixin"); + code.append("setMixInAnnotation($%s:T.class, $%s:L.class);", dtoLabel, mixinLabel); + index++; + } + + constructor.setMethodCodeDto(code); + return constructor; + } + private void clear() { entriesByPackage.clear(); } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java index e58984b8..759d3595 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/generators/util/MethodGeneratorUtil.java @@ -27,8 +27,8 @@ import java.util.List; import java.util.Map; import java.util.function.Consumer; -import javax.lang.model.element.Modifier; import org.apache.commons.lang3.StringUtils; +import org.javahelpers.simple.builders.core.enums.AccessModifier; import org.javahelpers.simple.builders.processor.analysis.JavaLangMapper; import org.javahelpers.simple.builders.processor.model.core.FieldDto; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; @@ -90,25 +90,13 @@ public static String generateBuilderMethodName(String fieldName, ProcessingConte } /** - * Gets the method access modifier from the builder configuration. + * Gets the access modifier for methods from the processing context. * * @param context the processing context - * @return the Modifier for method access, or null for package-private + * @return the AccessModifier for method access, or null for package-private */ - public static Modifier getMethodAccessModifier(ProcessingContext context) { - return JavaLangMapper.mapAccessModifier(context.getConfiguration().getMethodAccess()); - } - - /** - * Sets the access modifier on a MethodDto if the modifier is not null. - * - * @param method the MethodDto to update - * @param modifier the access modifier to set, or null for package-private - */ - public static void setMethodAccessModifier(MethodDto method, Modifier modifier) { - if (modifier != null) { - method.setModifier(modifier); - } + public static AccessModifier getMethodAccessModifier(ProcessingContext context) { + return context.getConfiguration().getMethodAccess(); } /** @@ -167,7 +155,7 @@ public static MethodDto createBuilderMethodForFieldWithTransform( new MethodDto( generateBuilderMethodName(field.getOriginalFieldName(), context), builderType); methodDto.addParameter(parameter); - setMethodAccessModifier(methodDto, getMethodAccessModifier(context)); + methodDto.setModifier(getMethodAccessModifier(context)); String params; if (StringUtils.isBlank(transform)) { @@ -224,13 +212,15 @@ public static MethodDto createFieldConsumerWithBuilder( new MethodDto( generateBuilderMethodName(field.getOriginalFieldName(), context), parentBuilderType); methodDto.addParameter(parameter); - setMethodAccessModifier(methodDto, getMethodAccessModifier(context)); + methodDto.setModifier(getMethodAccessModifier(context)); String buildExpression = calculateBuildExpression(field.getFieldType()); methodDto.setCode( """ - $helperType:T builder = this.$fieldName:N.isSet() ? new $helperType:T(%s) : new $helperType:T(%s); + $helperType:T builder = this.$fieldName:N.isSet() + ? new $helperType:T(%s) + : new $helperType:T(%s); $dtoMethodParam:N.accept(builder); this.$fieldName:N = $builderFieldWrapper:T.changedValue($buildExpression:N); return this; @@ -359,11 +349,13 @@ public static MethodDto createSimpleFieldConsumer( new MethodDto( generateBuilderMethodName(field.getOriginalFieldName(), context), builderType); methodDto.addParameter(parameter); - setMethodAccessModifier(methodDto, getMethodAccessModifier(context)); + methodDto.setModifier(getMethodAccessModifier(context)); methodDto.setCode( """ - $helperType:T consumer = this.$fieldName:N.isSet() ? this.$fieldName:N.value() : new $helperType:T(); + $helperType:T consumer = this.$fieldName:N.isSet() + ? this.$fieldName:N.value() + : new $helperType:T(); $dtoMethodParam:N.accept(consumer); this.$fieldName:N = $builderFieldWrapper:T.changedValue(consumer); return this; diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderDefinitionDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderDefinitionDto.java index e19df4da..7bc198e2 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderDefinitionDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/BuilderDefinitionDto.java @@ -24,100 +24,58 @@ package org.javahelpers.simple.builders.processor.model.core; -import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; -import java.util.Set; -import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto; -import org.javahelpers.simple.builders.processor.model.annotation.InterfaceName; -import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; -import org.javahelpers.simple.builders.processor.model.method.MethodDto; -import org.javahelpers.simple.builders.processor.model.type.GenericParameterDto; -import org.javahelpers.simple.builders.processor.model.type.NestedTypeDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; -/** BuilderDefinitionDto holds all information for generating a builder. */ -public class BuilderDefinitionDto { +/** + * Builder-specific extension of GenerationTargetClassDto. + * + *

This class holds all information for generating a builder, including builder-specific fields + * and methods that are not part of the generic class generation. + */ +public class BuilderDefinitionDto extends GenerationTargetClassDto { /** Target type of result of building by builder. Containing package and name of DTO. */ private TypeName buildingTargetTypeName; - /** Type of builder. Containing package and name of DTO. */ - private TypeName builderTypeName; - /** * Fields in constructor. Containing all fields which need to be given for calling the - * constructor. Ordering in LinkedList is matching orderung of parameters in constructor call. + * constructor. Ordering in LinkedList is matching ordering of parameters in constructor call. * Instances of fields are added in LinkedList of all fields too. */ private final List fieldsInConstructor = new LinkedList<>(); - /** Generic parameters declared on the target DTO (e.g., {@code }). */ - private final List generics = new LinkedList<>(); - /** * List of all fields of target DTO which are supported by builder. A field could be supported by * multiple functions in builder. */ private final List fields = new LinkedList<>(); - /** - * Nested types (interfaces or classes) to be generated inside the builder, such as the "With" - * interface. - */ - private final List nestedTypes = new LinkedList<>(); - - /** - * Core builder methods (build, create, conditional, toString, etc.) to be generated. These are - * added by BuilderEnhancers and have ordering for generation sequence. - */ - private final List coreMethods = new LinkedList<>(); - - /** - * Class-level annotations to be added to the generated builder class. These are added by - * BuilderEnhancers and include annotations like @Generated, @BuilderImplementation, etc. - * - *

Uses a Set to ensure annotation uniqueness and prevent duplicates. - */ - private final Set classAnnotations = new LinkedHashSet<>(); - - /** - * Interfaces to be implemented by the generated builder class. These are added by - * BuilderEnhancers and include interfaces like IBuilderBase. - * - *

Uses a Set to ensure interface uniqueness and prevent duplicates. - */ - private final Set interfaces = new LinkedHashSet<>(); - - /** Class-level JavaDoc for the generated builder class. */ - private JavadocDto classJavadoc; - /** Configuration for builder generation. */ private BuilderConfiguration configuration; /** - * Getting type of builder. + * Getting type of builder. Delegates to base class typeName. * - * @return {@code org.javahelpers.simple.builders.internal.dtos.Typename} package and name of - * builder. + * @return package and name of builder. */ public TypeName getBuilderTypeName() { - return builderTypeName; + return getTypeName(); } /** - * Setting type of builder. + * Setting type of builder. Delegates to base class typeName. * * @param builderClassName type of builder */ public void setBuilderTypeName(TypeName builderClassName) { - this.builderTypeName = builderClassName; + setTypeName(builderClassName); } /** * Getting target type of result of building by builder. * - * @return {@code org.javahelpers.simple.builders.internal.dtos.Typename} package and name of type - * of building result. + * @return package and name of type of building result. */ public TypeName getBuildingTargetTypeName() { return buildingTargetTypeName; @@ -133,9 +91,9 @@ public void setBuildingTargetTypeName(TypeName buildingTargetTypeName) { } /** - * Adding a field-definition to builder defintion. + * Adding a field-definition to builder definition. * - * @param field {@code org.javahelpers.simple.builders.internal.dtos.FieldDto} to be added + * @param field to be added */ public void addField(FieldDto field) { fields.add(field); @@ -169,9 +127,9 @@ public void addAllFields(List fields) { } /** - * Getting all definied fields in builder definition. + * Getting all defined fields in builder definition. * - * @return list of fields with type {@code org.javahelpers.simple.builders.internal.dtos.FieldDto} + * @return list of fields */ public List getSetterFieldsForBuilder() { return fields; @@ -199,96 +157,6 @@ public List getAllFieldsForBuilder() { return all; } - /** - * Adds a generic parameter definition. - * - * @param generic the generic parameter to add - */ - public void addGeneric(GenericParameterDto generic) { - generics.add(generic); - } - - /** - * Returns the list of generic parameters of the target DTO, in declared order. - * - * @return the list of generic parameters - */ - public List getGenerics() { - return generics; - } - - /** - * Returns the list of nested types (interfaces or classes) to be generated inside the builder. - * - * @return the list of nested types - */ - public List getNestedTypes() { - return nestedTypes; - } - - /** - * Adds a nested type definition to the builder. - * - * @param nestedType the nested type to add - */ - public void addNestedType(NestedTypeDto nestedType) { - this.nestedTypes.add(nestedType); - } - - /** - * Returns the list of core builder methods. - * - * @return the list of core methods - */ - public List getCoreMethods() { - return coreMethods; - } - - /** - * Adds a core method to be generated in the builder. - * - * @param method the core method to add - */ - public void addCoreMethod(MethodDto method) { - this.coreMethods.add(method); - } - - /** - * Returns the set of class-level annotations. - * - * @return the set of class annotations (unique, no duplicates) - */ - public Set getClassAnnotations() { - return classAnnotations; - } - - /** - * Adds a class-level annotation to be generated in the builder. - * - * @param annotation the class annotation to add - */ - public void addClassAnnotation(AnnotationDto annotation) { - this.classAnnotations.add(annotation); - } - - /** - * Returns the set of interfaces to be implemented by the builder. - * - * @return the set of interfaces (unique, no duplicates) - */ - public Set getInterfaces() { - return interfaces; - } - - /** - * Adds an interface to be implemented by the builder. - * - * @param interfaceType the interface to add - */ - public void addInterface(InterfaceName interfaceType) { - this.interfaces.add(interfaceType); - } - /** * Returns the configuration for builder generation. * @@ -306,22 +174,4 @@ public BuilderConfiguration getConfiguration() { public void setConfiguration(BuilderConfiguration configuration) { this.configuration = configuration; } - - /** - * Returns the class-level JavaDoc for the generated builder. - * - * @return the class JavaDoc, or null if not set - */ - public JavadocDto getClassJavadoc() { - return classJavadoc; - } - - /** - * Sets the class-level JavaDoc for the generated builder. - * - * @param classJavadoc the class JavaDoc to set - */ - public void setClassJavadoc(JavadocDto classJavadoc) { - this.classJavadoc = classJavadoc; - } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/ClassFieldDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/ClassFieldDto.java new file mode 100644 index 00000000..c465d62d --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/ClassFieldDto.java @@ -0,0 +1,176 @@ +/* + * MIT License + * + * Copyright (c) 2026 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.processor.model.core; + +import static org.javahelpers.simple.builders.core.enums.AccessModifier.PRIVATE; + +import java.util.LinkedHashSet; +import java.util.Set; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import org.javahelpers.simple.builders.core.enums.AccessModifier; +import org.javahelpers.simple.builders.processor.model.imports.ImportStatement; +import org.javahelpers.simple.builders.processor.model.imports.RegularImport; +import org.javahelpers.simple.builders.processor.model.imports.StaticImport; +import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; +import org.javahelpers.simple.builders.processor.model.type.TypeName; +import org.javahelpers.simple.builders.processor.model.type.TypeNameArray; +import org.javahelpers.simple.builders.processor.model.type.TypeNameGeneric; + +/** + * Represents a field declaration in the generated class. + * + *

This DTO contains only rendering information for a field. It is created from {@code FieldDto} + * during finalization in {@code BuilderDefinitionCreator}. + */ +public class ClassFieldDto { + /** Field name in generated class. */ + private String fieldName; + + /** Field type. */ + private TypeName fieldType; + + /** Field visibility. */ + private AccessModifier visibility = PRIVATE; + + /** Literal initializer (e.g., "exampleValue" like in {@code String field="exampleValue";}). */ + private String literalInitializer; + + /** Field javadoc. */ + private JavadocDto javadoc; + + /** Types to import for this field (e.g., TrackedValue, the boxed inner type). */ + private final Set fieldTypeImports = new LinkedHashSet<>(); + + public String getFieldName() { + return fieldName; + } + + public void setFieldName(String fieldName) { + this.fieldName = fieldName; + } + + /** + * Returns the field type. + * + * @return the field type + */ + public TypeName getFieldType() { + return fieldType; + } + + public void setFieldType(TypeName fieldType) { + this.fieldType = fieldType; + } + + public AccessModifier getVisibility() { + return visibility; + } + + public void setVisibility(AccessModifier visibility) { + this.visibility = visibility; + } + + public String getLiteralInitializer() { + return literalInitializer; + } + + public void setLiteralInitializer(String literalInitializer) { + this.literalInitializer = literalInitializer; + } + + public JavadocDto getJavadoc() { + return javadoc; + } + + public void setJavadoc(JavadocDto javadoc) { + this.javadoc = javadoc; + } + + public Set getFieldTypeImports() { + return fieldTypeImports; + } + + // Convenience methods for adding imports without creating DTOs + + /** + * Adds a regular import for this field (convenience method accepting Class). + * + * @param clazz the class to import + */ + public void addImport(Class clazz) { + this.fieldTypeImports.add(new RegularImport(TypeName.of(clazz))); + } + + /** + * Adds a regular import for this field (convenience method accepting TypeName). + * + * @param typeName the type to import + */ + public void addImport(TypeName typeName) { + if (typeName instanceof TypeNameGeneric generic) { + addImport(generic.getRawType()); + generic.getInnerTypeArguments().forEach(this::addImport); + } else if (typeName instanceof TypeNameArray array) { + addImport(array.getTypeOfArray()); + } else { + this.fieldTypeImports.add(new RegularImport(typeName)); + } + } + + /** + * Adds a static import for this field (convenience method accepting Class and member name). + * + * @param clazz the class containing the static member + * @param memberName the name of the static member + */ + public void addStaticImport(Class clazz, String memberName) { + this.fieldTypeImports.add(new StaticImport(TypeName.of(clazz), memberName)); + } + + /** + * Adds a static import for this field (convenience method accepting TypeName and member name). + * + * @param type the type containing the static member + * @param memberName the name of the static member + */ + public void addStaticImport(TypeName type, String memberName) { + TypeName typeRaw = new TypeName(type.getPackageName(), type.getClassName()); + this.fieldTypeImports.add(new StaticImport(typeRaw, memberName)); + } + + /** + * Returns a short string representation of this field using Apache Commons ToStringBuilder. + * + * @return concise string representation showing field name, type, and visibility + */ + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) + .append("name", fieldName) + .append("type", fieldType) + .toString(); + } +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/GenerationTargetClassDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/GenerationTargetClassDto.java new file mode 100644 index 00000000..685cde3c --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/core/GenerationTargetClassDto.java @@ -0,0 +1,295 @@ +/* + * MIT License + * + * Copyright (c) 2026 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.processor.model.core; + +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import org.javahelpers.simple.builders.core.enums.AccessModifier; +import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto; +import org.javahelpers.simple.builders.processor.model.annotation.InterfaceName; +import org.javahelpers.simple.builders.processor.model.imports.ImportStatement; +import org.javahelpers.simple.builders.processor.model.imports.RegularImport; +import org.javahelpers.simple.builders.processor.model.imports.StaticImport; +import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; +import org.javahelpers.simple.builders.processor.model.method.ConstructorDto; +import org.javahelpers.simple.builders.processor.model.method.MethodDto; +import org.javahelpers.simple.builders.processor.model.type.GenericParameterDto; +import org.javahelpers.simple.builders.processor.model.type.NestedTypeDto; +import org.javahelpers.simple.builders.processor.model.type.TypeName; +import org.javahelpers.simple.builders.processor.model.type.TypeNameArray; +import org.javahelpers.simple.builders.processor.model.type.TypeNameGeneric; + +/** + * Generic representation of a class to generate. + * + *

This base class contains all information needed by the code generator to render a class, + * independent of whether it's a builder, Jackson module, or other generated class type. + */ +public class GenerationTargetClassDto { + /** Generated class FQN. */ + private TypeName typeName; + + /** Class visibility. */ + private AccessModifier classAccessModifier; + + /** Superclass (e.g., SimpleModule), null if none. */ + private TypeName superType; + + /** Field declarations. */ + private final List classFields = new LinkedList<>(); + + /** Constructor definitions. */ + private final List constructors = new LinkedList<>(); + + /** All methods (unresolved — conflict resolution in generator). */ + private final List methods = new LinkedList<>(); + + /** Generic parameters declared on the target DTO (e.g., {@code }). */ + private final List generics = new LinkedList<>(); + + /** Imports (both regular and static imports). */ + private final Set imports = new LinkedHashSet<>(); + + /** + * Nested types (interfaces or classes) to be generated inside the builder, such as the "With" + * interface. + */ + private final List nestedTypes = new LinkedList<>(); + + /** + * Class-level annotations to be added to the generated builder class. These are added by + * BuilderEnhancers and include annotations like @Generated, @BuilderImplementation, etc. + * + *

Uses a Set to ensure annotation uniqueness and prevent duplicates. + */ + private final Set classAnnotations = new LinkedHashSet<>(); + + /** + * Interfaces to be implemented by the generated builder class. These are added by + * BuilderEnhancers and include interfaces like IBuilderBase. + * + *

Uses a Set to ensure interface uniqueness and prevent duplicates. + */ + private final Set interfaces = new LinkedHashSet<>(); + + /** Class-level JavaDoc for the generated builder class. */ + private JavadocDto classJavadoc; + + public TypeName getTypeName() { + return typeName; + } + + public void setTypeName(TypeName typeName) { + this.typeName = typeName; + } + + public AccessModifier getClassAccessModifier() { + return classAccessModifier; + } + + public void setClassAccessModifier(AccessModifier classAccessModifier) { + this.classAccessModifier = classAccessModifier; + } + + public TypeName getSuperType() { + return superType; + } + + public void setSuperType(TypeName superType) { + this.superType = superType; + } + + public List getClassFields() { + return classFields; + } + + public void addClassField(ClassFieldDto classField) { + this.classFields.add(classField); + } + + public List getConstructors() { + return constructors; + } + + public void addConstructor(ConstructorDto constructor) { + this.constructors.add(constructor); + } + + public List getMethods() { + return methods; + } + + public void addMethod(MethodDto method) { + this.methods.add(method); + } + + public Set getImports() { + return imports; + } + + public void addImport(ImportStatement importStatement) { + this.imports.add(importStatement); + } + + // Convenience methods for adding imports without creating DTOs + + /** + * Adds a regular import (convenience method accepting Class). + * + * @param clazz the class to import + */ + public void addImport(Class clazz) { + addImport(new RegularImport(TypeName.of(clazz))); + } + + /** + * Adds a regular import (convenience method accepting TypeName). + * + * @param typeName the type to import + */ + public void addImport(TypeName typeName) { + if (typeName instanceof TypeNameGeneric generic) { + addImport(generic.getRawType()); + generic.getInnerTypeArguments().forEach(this::addImport); + } else if (typeName instanceof TypeNameArray array) { + addImport(array.getTypeOfArray()); + } else { + addImport(new RegularImport(typeName)); + } + } + + /** + * Adds a static import (convenience method accepting Class and member name). + * + * @param clazz the class containing the static member + * @param memberName the name of the static member + */ + public void addStaticImport(Class clazz, String memberName) { + addImport(new StaticImport(TypeName.of(clazz), memberName)); + } + + /** + * Adds a static import (convenience method accepting TypeName and member name). + * + * @param type the type containing the static member + * @param memberName the name of the static member + */ + public void addStaticImport(TypeName type, String memberName) { + TypeName typeRaw = new TypeName(type.getPackageName(), type.getClassName()); + addImport(new StaticImport(typeRaw, memberName)); + } + + /** + * Adds a generic parameter definition. + * + * @param generic the generic parameter to add + */ + public void addGeneric(GenericParameterDto generic) { + generics.add(generic); + } + + /** + * Returns the list of generic parameters of the target DTO, in declared order. + * + * @return the list of generic parameters + */ + public List getGenerics() { + return generics; + } + + /** + * Returns the list of nested types (interfaces or classes) to be generated inside the builder. + * + * @return the list of nested types + */ + public List getNestedTypes() { + return nestedTypes; + } + + /** + * Adds a nested type definition to the builder. + * + * @param nestedType the nested type to add + */ + public void addNestedType(NestedTypeDto nestedType) { + this.nestedTypes.add(nestedType); + } + + /** + * Returns the set of class-level annotations. + * + * @return the set of class annotations (unique, no duplicates) + */ + public Set getClassAnnotations() { + return classAnnotations; + } + + /** + * Adds a class-level annotation to be generated in the builder. + * + * @param annotation the class annotation to add + */ + public void addClassAnnotation(AnnotationDto annotation) { + this.classAnnotations.add(annotation); + } + + /** + * Returns the set of interfaces to be implemented by the builder. + * + * @return the set of interfaces (unique, no duplicates) + */ + public Set getInterfaces() { + return interfaces; + } + + /** + * Adds an interface to be implemented by the builder. + * + * @param interfaceType the interface to add + */ + public void addInterface(InterfaceName interfaceType) { + this.interfaces.add(interfaceType); + } + + /** + * Returns the class-level JavaDoc for the generated builder. + * + * @return the class JavaDoc, or null if not set + */ + public JavadocDto getClassJavadoc() { + return classJavadoc; + } + + /** + * Sets the class-level JavaDoc for the generated builder. + * + * @param classJavadoc the class JavaDoc to set + */ + public void setClassJavadoc(JavadocDto classJavadoc) { + this.classJavadoc = classJavadoc; + } +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/integration/JacksonModuleDefinitionDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/imports/ImportStatement.java similarity index 53% rename from processor/src/main/java/org/javahelpers/simple/builders/processor/model/integration/JacksonModuleDefinitionDto.java rename to processor/src/main/java/org/javahelpers/simple/builders/processor/model/imports/ImportStatement.java index e5457987..813306b7 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/integration/JacksonModuleDefinitionDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/imports/ImportStatement.java @@ -22,37 +22,40 @@ * SOFTWARE. */ -package org.javahelpers.simple.builders.processor.model.integration; - -import java.util.Collection; -import java.util.HashSet; -import java.util.Set; +package org.javahelpers.simple.builders.processor.model.imports; /** - * Definition for generating a Jackson SimpleModule. Contains the target package and the entries to - * be registered in the module. + * Represents an import statement in generated code. + * + *

This is the base interface for all import types (regular and static imports). This is a pure + * data holder - business logic for filtering imports belongs in ImportCollector. */ -public class JacksonModuleDefinitionDto { - private final String targetPackage; - private final Set entries = new HashSet<>(); - - public JacksonModuleDefinitionDto(String targetPackage) { - this.targetPackage = targetPackage; - } - - public String getTargetPackage() { - return targetPackage; - } - - public Set getEntries() { - return entries; - } - - public void addEntry(JacksonModuleEntryDto entry) { - this.entries.add(entry); - } - - public void addAllEntries(Collection entries) { - this.entries.addAll(entries); - } +public interface ImportStatement { + + /** + * Returns the fully qualified name for this import. + * + *

For regular imports: {@code com.example.MyClass} + * + *

For static imports: {@code com.example.MyClass.staticMethod} + * + * @return the fully qualified name + */ + String getFullyQualifiedName(); + + /** + * Returns the package name for this import. + * + *

Extracted from the fully qualified name by taking everything before the last dot. + * + * @return the package name, or empty string if no package + */ + String getPackageName(); + + /** + * Checks if this is a static import. + * + * @return true if this is a static import, false for regular imports + */ + boolean isStatic(); } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/imports/RegularImport.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/imports/RegularImport.java new file mode 100644 index 00000000..7bdab049 --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/imports/RegularImport.java @@ -0,0 +1,104 @@ +/* + * MIT License + * + * Copyright (c) 2026 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.processor.model.imports; + +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import org.javahelpers.simple.builders.processor.model.type.TypeName; +import org.javahelpers.simple.builders.processor.model.type.TypeNameArray; +import org.javahelpers.simple.builders.processor.model.type.TypeNameGeneric; + +/** + * Represents a regular (non-static) import statement. + * + *

Example: {@code import com.example.MyClass;} + */ +public class RegularImport implements ImportStatement { + + private final TypeName type; + + /** + * Creates a regular import from a TypeName. + * + * @param type the type to import + */ + public RegularImport(TypeName type) { + if (type == null) { + throw new IllegalArgumentException("type cannot be null"); + } else if (type instanceof TypeNameArray array) { + this.type = array.getTypeOfArray(); + } else if (type instanceof TypeNameGeneric generic) { + this.type = generic.getRawType(); + } else { + this.type = type; + } + } + + /** + * Returns the type being imported. + * + * @return the type + */ + public TypeName getType() { + return type; + } + + @Override + public String getFullyQualifiedName() { + return type.getFullQualifiedName(); + } + + @Override + public String getPackageName() { + return type.getPackageName(); + } + + @Override + public boolean isStatic() { + return false; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RegularImport that = (RegularImport) o; + return new EqualsBuilder().append(type, that.type).isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37).append(type).toHashCode(); + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) + .append("fqn", getFullyQualifiedName()) + .toString(); + } +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/imports/StaticImport.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/imports/StaticImport.java new file mode 100644 index 00000000..307f66ff --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/imports/StaticImport.java @@ -0,0 +1,115 @@ +/* + * MIT License + * + * Copyright (c) 2026 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.processor.model.imports; + +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import org.javahelpers.simple.builders.processor.model.type.TypeName; + +/** + * Represents a static import statement. + * + *

Example: {@code import static com.example.MyClass.staticMethod;} + */ +public class StaticImport implements ImportStatement { + + private final TypeName type; + private final String memberName; + + /** + * Creates a static import. + * + * @param type the type containing the static member + * @param memberName the name of the static member (method or field) + */ + public StaticImport(TypeName type, String memberName) { + if (type == null) { + throw new IllegalArgumentException("type cannot be null"); + } + if (memberName == null) { + throw new IllegalArgumentException("memberName cannot be null"); + } + this.type = type; + this.memberName = memberName; + } + + /** + * Returns the type containing the static member. + * + * @return the type + */ + public TypeName getType() { + return type; + } + + /** + * Returns the name of the static member. + * + * @return the member name + */ + public String getMemberName() { + return memberName; + } + + @Override + public String getFullyQualifiedName() { + return type.getFullQualifiedName() + "." + memberName; + } + + @Override + public String getPackageName() { + return type.getPackageName(); + } + + @Override + public boolean isStatic() { + return true; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + StaticImport that = (StaticImport) o; + return new EqualsBuilder() + .append(type, that.type) + .append(memberName, that.memberName) + .isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37).append(type).append(memberName).toHashCode(); + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) + .append("fqn", getFullyQualifiedName()) + .toString(); + } +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/ConstructorDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/ConstructorDto.java new file mode 100644 index 00000000..9335e0cd --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/ConstructorDto.java @@ -0,0 +1,132 @@ +/* + * MIT License + * + * Copyright (c) 2026 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.processor.model.method; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import org.javahelpers.simple.builders.core.enums.AccessModifier; +import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto; +import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; + +/** + * Dedicated constructor representation (separate from MethodDto). + * + *

This DTO contains all information needed to render a constructor in the generated class. + */ +public class ConstructorDto { + /** Constructor parameters. */ + private final LinkedList parameters = new LinkedList<>(); + + /** Constructor body code with type placeholders. */ + private MethodCodeDto methodCodeDto = new MethodCodeDto(); + + /** Constructor visibility. */ + private AccessModifier visibility; + + /** Constructor javadoc. */ + private JavadocDto javadoc; + + /** Constructor annotations. */ + private final List annotations = new ArrayList<>(); + + public List getParameters() { + return parameters; + } + + public void addParameter(MethodParameterDto parameter) { + this.parameters.add(parameter); + } + + /** + * Gets the constructor body as MethodCodeDto. + * + * @return the method code DTO containing body and type placeholders + */ + public MethodCodeDto getMethodCodeDto() { + return methodCodeDto; + } + + /** + * Sets the constructor body using MethodCodeDto. + * + * @param methodCodeDto the method code DTO + */ + public void setMethodCodeDto(MethodCodeDto methodCodeDto) { + this.methodCodeDto = methodCodeDto; + } + + /** + * Checks if the constructor has a code block. + * + * @return true if the constructor has a code block, false otherwise + */ + public boolean hasCode() { + return methodCodeDto.hasCode(); + } + + public AccessModifier getVisibility() { + return visibility; + } + + public void setVisibility(AccessModifier visibility) { + this.visibility = visibility; + } + + public JavadocDto getJavadoc() { + return javadoc; + } + + public void setJavadoc(JavadocDto javadoc) { + this.javadoc = javadoc; + } + + public List getAnnotations() { + return annotations; + } + + public void addAnnotation(AnnotationDto annotation) { + this.annotations.add(annotation); + } + + /** + * Returns a string representation of this constructor using Apache Commons ToStringBuilder. + * + *

This provides a comprehensive view of the constructor including visibility, parameters, + * annotations, javadoc, and code block information for debugging and logging purposes. + * + * @return detailed string representation of the constructor + */ + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) + .append("parameters", parameters) + .append("annotations", annotations) + .append("hasCodeBlock", hasCode()) + .toString(); + } +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodCodeDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodCodeDto.java index ca29e2a5..d4d26099 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodCodeDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodCodeDto.java @@ -25,8 +25,16 @@ package org.javahelpers.simple.builders.processor.model.method; import java.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; +import org.apache.commons.lang3.StringUtils; +import org.javahelpers.simple.builders.processor.model.imports.ImportStatement; +import org.javahelpers.simple.builders.processor.model.imports.RegularImport; +import org.javahelpers.simple.builders.processor.model.imports.StaticImport; import org.javahelpers.simple.builders.processor.model.type.TypeName; +import org.javahelpers.simple.builders.processor.model.type.TypeNameArray; +import org.javahelpers.simple.builders.processor.model.type.TypeNameGeneric; /** DTO for holding information of code implementation. */ public class MethodCodeDto { @@ -36,6 +44,9 @@ public class MethodCodeDto { /** List of placeholders in CodeFormat. Containing dynamic values too. */ private final List> codeArguments = new ArrayList<>(); + /** Types used in the code body that aren't covered by arguments. */ + private final Set codeBlockImports = new LinkedHashSet<>(); + /** * Setting format of code. * @@ -63,6 +74,7 @@ public void addArgument(String name, String value) { */ public void addArgument(String name, TypeName value) { codeArguments.add(new MethodCodeTypePlaceholder(name, value)); + addCodeBlockImport(value); } /** @@ -83,4 +95,99 @@ public String getCodeFormat() { public List> getCodeArguments() { return codeArguments; } + + /** + * Checks if this code DTO has code content. + * + * @return true if code format is not null and not blank + */ + public boolean hasCode() { + return !StringUtils.isBlank(codeFormat); + } + + /** + * Returns the set of imports used in the code body that aren't covered by arguments. + * + * @return set of imports used in code body + */ + public Set getCodeBlockImports() { + return codeBlockImports; + } + + /** + * Adding an import for a type used in the code block. + * + * @param typeName type to import + */ + public void addCodeBlockImport(TypeName typeName) { + if (typeName instanceof TypeNameGeneric generic) { + addCodeBlockImport(generic.getRawType()); + generic.getInnerTypeArguments().forEach(this::addCodeBlockImport); + } else if (typeName instanceof TypeNameArray array) { + addCodeBlockImport(array.getTypeOfArray()); + } else { + this.codeBlockImports.add(new RegularImport(typeName)); + } + } + + /** + * Adding an import for a type used in the code block (convenience overload accepting Class). + * + * @param clazz the class to import + */ + public void addCodeBlockImport(Class clazz) { + addCodeBlockImport(TypeName.of(clazz)); + } + + /** + * Adds a static import for a method/field used in the code block (convenience method). + * + * @param clazz the class containing the static member + * @param memberName the name of the static member + */ + public void addStaticImport(Class clazz, String memberName) { + this.codeBlockImports.add(new StaticImport(TypeName.of(clazz), memberName)); + } + + /** + * Adds type imports for a TypeName and its generic type arguments (convenience method). + * + * @param type the type to add imports for + */ + public void addTypeImports(TypeName type) { + if (type == null) { + return; + } + + // Add the main type + addCodeBlockImport(type); + + // Add generic type arguments recursively + if (type instanceof TypeNameGeneric genericType) { + genericType.getInnerTypeArguments().forEach(this::addTypeImports); + } + } + + /** + * Appends additional code to the existing code format with string formatting support. + * + *

This method concatenates the provided formatted code string to the current code format, + * separated by a newline for proper formatting. This is useful for building up method bodies + * incrementally, especially when constructing complex code with multiple sections. + * + *

Supports the same formatting syntax as {@link String#format(String, Object...)}, allowing + * for dynamic value insertion using placeholders like %s, %d, etc. + * + * @param formatted the code fragment with format placeholders to append to the existing code + * format + * @param args the arguments to be formatted into the string + */ + public void append(String formatted, Object... args) { + String formattedCode = args.length > 0 ? String.format(formatted, args) : formatted; + if (StringUtils.isEmpty(codeFormat)) { + codeFormat = formattedCode; + } else { + codeFormat += "\n" + formattedCode; + } + } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodDto.java index 4d8e5662..896986f9 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/method/MethodDto.java @@ -28,8 +28,8 @@ import java.util.LinkedList; import java.util.List; import java.util.Optional; -import javax.lang.model.element.Modifier; import org.apache.commons.lang3.StringUtils; +import org.javahelpers.simple.builders.core.enums.AccessModifier; import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; import org.javahelpers.simple.builders.processor.model.type.GenericParameterDto; @@ -44,7 +44,7 @@ public class MethodDto { public static final int PRIORITY_LOW = 60; // Specialized consumers /** Access modifier for method. */ - private Optional modifier = Optional.empty(); + private Optional modifier = Optional.empty(); /** Whether the method is static. */ private boolean isStatic = false; @@ -184,6 +184,15 @@ public MethodCodeDto getMethodCodeDto() { return methodCodeDto; } + /** + * Checks if the constructor has a code block. + * + * @return true if the constructor has a code block, false otherwise + */ + public boolean hasCode() { + return methodCodeDto.hasCode(); + } + /** * Getting name of method. * @@ -252,19 +261,18 @@ public List getGenericParameters() { /** * Getting the access modifier for method. Optional for usage in stream-notation. * - * @return modifier {@code java.util.Optional} access modifier of type {@code - * javax.lang.model.element.Modifier} + * @return modifier {@code java.util.Optional} access modifier of type {@code AccessModifier} */ - public Optional getModifier() { + public Optional getModifier() { return modifier; } /** * Sets the access modifier for method. * - * @param modifier access modifier of type {@code javax.lang.model.element.Modifier} + * @param modifier access modifier of type {@code AccessModifier} */ - public void setModifier(Modifier modifier) { + public void setModifier(AccessModifier modifier) { this.modifier = Optional.ofNullable(modifier); } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/NestedTypeDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/NestedTypeDto.java index 3a82163c..1f6f6b50 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/NestedTypeDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/NestedTypeDto.java @@ -24,8 +24,11 @@ package org.javahelpers.simple.builders.processor.model.type; +import java.util.ArrayList; import java.util.LinkedList; import java.util.List; +import org.javahelpers.simple.builders.core.enums.AccessModifier; +import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; import org.javahelpers.simple.builders.processor.model.method.MethodDto; @@ -42,8 +45,8 @@ public class NestedTypeDto { /** The kind of nested type (INTERFACE or CLASS). */ private NestedTypeKind kind; - /** Whether this nested type should be public. */ - private boolean isPublic = true; + /** Visibility of this nested type. */ + private AccessModifier visibility = AccessModifier.PUBLIC; /** Methods to be generated in this nested type. */ private final List methods = new LinkedList<>(); @@ -51,6 +54,9 @@ public class NestedTypeDto { /** Javadoc comment for this nested type. */ private JavadocDto javadoc; + /** Type-level annotations for this nested type. */ + private final List annotations = new ArrayList<>(); + public enum NestedTypeKind { INTERFACE, CLASS @@ -72,12 +78,12 @@ public void setKind(NestedTypeKind kind) { this.kind = kind; } - public boolean isPublic() { - return isPublic; + public AccessModifier getVisibility() { + return visibility; } - public void setPublic(boolean isPublic) { - this.isPublic = isPublic; + public void setVisibility(AccessModifier visibility) { + this.visibility = visibility; } public List getMethods() { @@ -95,4 +101,12 @@ public JavadocDto getJavadoc() { public void setJavadoc(JavadocDto javadoc) { this.javadoc = javadoc; } + + public List getAnnotations() { + return annotations; + } + + public void addAnnotation(AnnotationDto annotation) { + this.annotations.add(annotation); + } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/TypeNameGeneric.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/TypeNameGeneric.java index c3885e2e..115d6160 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/TypeNameGeneric.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/model/type/TypeNameGeneric.java @@ -97,6 +97,15 @@ public TypeNameGeneric(String packageName, String className, TypeName... innerTy this.innerTypeArguments = List.of(innerTypeArguments); } + /** + * Returns the raw type (without type parameters). + * + * @return the raw type + */ + public TypeName getRawType() { + return new TypeName(getPackageName(), getClassName()); + } + /** * Returns the list of inner generic type arguments. * 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 fb2e512d..7f1b6dc9 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 @@ -40,6 +40,8 @@ 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.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; @@ -47,11 +49,13 @@ import org.javahelpers.simple.builders.processor.generators.util.MethodGeneratorUtil; import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto; import org.javahelpers.simple.builders.processor.model.core.BuilderDefinitionDto; +import org.javahelpers.simple.builders.processor.model.core.ClassFieldDto; import org.javahelpers.simple.builders.processor.model.core.FieldDto; import org.javahelpers.simple.builders.processor.model.javadoc.JavadocDto; import org.javahelpers.simple.builders.processor.model.method.MethodDto; import org.javahelpers.simple.builders.processor.model.method.MethodParameterDto; import org.javahelpers.simple.builders.processor.model.type.TypeName; +import org.javahelpers.simple.builders.processor.model.type.TypeNameGeneric; /** Class for creating a specific BuilderDefinitionDto for an annotated DTO class. */ public class BuilderDefinitionCreator { @@ -92,6 +96,9 @@ public static BuilderDefinitionDto extractFromElement( // Apply builder enhancers (including With interface generation) context.getGeneratorRegistry().enhanceBuilder(result, result.getBuildingTargetTypeName()); + // Finalize the definition - convert to generic class representation + finalizeDefinition(result, context); + context.debug("Builder will be generated as: %s", result.getBuilderTypeName().getClassName()); context.debugEndOperation( @@ -100,6 +107,89 @@ public static BuilderDefinitionDto extractFromElement( return result; } + /** + * Finalizes the builder definition by converting builder-specific data into generic class + * generation data. + * + *

This step: + * + *

    + *
  • Converts FieldDto instances to ClassFieldDto instances + *
  • Collects all methods from fields and core methods + *
  • Sets source descriptions on methods for conflict resolution logging + *
  • Sets class access modifier + *
  • Sets static imports for TrackedValue + *
+ * + * @param builderDto the builder definition to finalize + * @param context the processing context + */ + private static void finalizeDefinition( + BuilderDefinitionDto builderDto, ProcessingContext context) { + context.debugStartOperation("Finalizing builder definition"); + + // 1. Convert FieldDto → ClassFieldDto + for (FieldDto field : builderDto.getAllFieldsForBuilder()) { + ClassFieldDto classField = convertToClassField(field); + builderDto.addClassField(classField); + } + + // 2. Collect methods from fields + for (FieldDto field : builderDto.getConstructorFieldsForBuilder()) { + for (MethodDto method : field.getMethods()) { + builderDto.addMethod(method); + } + } + for (FieldDto field : builderDto.getSetterFieldsForBuilder()) { + for (MethodDto method : field.getMethods()) { + builderDto.addMethod(method); + } + } + + // 3. Set class access modifier + builderDto.setClassAccessModifier(builderDto.getConfiguration().getBuilderAccess()); + + // 4. Set static imports for TrackedValue + builderDto.addStaticImport(TrackedValue.class, "changedValue"); + builderDto.addStaticImport(TrackedValue.class, "initialValue"); + builderDto.addStaticImport(TrackedValue.class, "unsetValue"); + + context.debugEndOperation( + "Finalized: %d class fields, %d methods, %d constructors", + builderDto.getClassFields().size(), + builderDto.getMethods().size(), + builderDto.getConstructors().size()); + } + + /** + * Converts a FieldDto to a ClassFieldDto for rendering. + * + * @param field the field DTO to convert + * @return the class field DTO + */ + private static ClassFieldDto convertToClassField(FieldDto field) { + ClassFieldDto classField = new ClassFieldDto(); + classField.setFieldName(field.getFieldNameInBuilder()); + + // Build the field type: TrackedValue + TypeName trackedValueType = + new TypeName("org.javahelpers.simple.builders.core.util", "TrackedValue"); + TypeName wrappedFieldType = + new TypeNameGeneric(trackedValueType, List.of(field.getFieldType())); + classField.setFieldType(wrappedFieldType); + + classField.setVisibility(AccessModifier.PRIVATE); + classField.setLiteralInitializer("unsetValue()"); + classField.setJavadoc(field.getJavaDoc()); + + // Add field type imports + classField.addImport(new TypeName("org.javahelpers.simple.builders.core.util", "TrackedValue")); + // Add the field type for import + classField.addImport(field.getFieldType()); + + return classField; + } + /** Initializes the builder definition with package, class name, and generics. */ private static BuilderDefinitionDto initializeBuilderDefinition( TypeElement annotatedType, ProcessingContext context) { diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ImportCollector.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ImportCollector.java new file mode 100644 index 00000000..5d2beef4 --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ImportCollector.java @@ -0,0 +1,328 @@ +package org.javahelpers.simple.builders.processor.util; + +import java.util.Comparator; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Strings; +import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto; +import org.javahelpers.simple.builders.processor.model.annotation.InterfaceName; +import org.javahelpers.simple.builders.processor.model.core.GenerationTargetClassDto; +import org.javahelpers.simple.builders.processor.model.imports.ImportStatement; +import org.javahelpers.simple.builders.processor.model.imports.RegularImport; +import org.javahelpers.simple.builders.processor.model.method.MethodCodePlaceholder; +import org.javahelpers.simple.builders.processor.model.method.MethodCodeTypePlaceholder; +import org.javahelpers.simple.builders.processor.model.method.MethodDto; +import org.javahelpers.simple.builders.processor.model.method.MethodParameterDto; +import org.javahelpers.simple.builders.processor.model.type.TypeName; +import org.javahelpers.simple.builders.processor.model.type.TypeNameArray; +import org.javahelpers.simple.builders.processor.model.type.TypeNameGeneric; +import org.javahelpers.simple.builders.processor.model.type.TypeNamePrimitive; +import org.javahelpers.simple.builders.processor.model.type.TypeNameVariable; + +/** + * Utility class for collecting, deduplicating, and sorting imports for generated classes. + * + *

This class centralizes all import-related logic that was previously scattered throughout + * RoasterCodeGenerator, providing a clean and consistent approach to import management. + * + *

Uses {@link ImportStatement} objects internally to represent both regular and static imports, + * providing type safety and eliminating string-based import handling. + */ +public class ImportCollector { + + private final Set imports = new LinkedHashSet<>(); + private final TypeName currentType; + + /** + * Creates a new ImportCollector for the specified type. + * + * @param currentType the type of the class being generated + */ + public ImportCollector(TypeName currentType) { + this.currentType = currentType; + } + + /** + * Adds imports for a type. + * + * @param type the type to add imports for + */ + public ImportCollector addTypeImports(TypeName type) { + if (type == null || type instanceof TypeNamePrimitive || type instanceof TypeNameVariable) { + return this; + } + + // Add type annotation imports + type.getAnnotations().forEach(this::addAnnotationImports); + + // Adding the type + addImport(type); + + return this; + } + + /** + * Adds imports for an annotation. + * + * @param annotation the annotation to add imports for + */ + public ImportCollector addAnnotationImports(AnnotationDto annotation) { + if (annotation.getAnnotationType() != null) { + addTypeImports(annotation.getAnnotationType()); + } + return this; + } + + /** + * Adds imports for a method parameter. + * + * @param parameter the parameter to add imports for + */ + public ImportCollector addParameterImports(MethodParameterDto parameter) { + addTypeImports(parameter.getParameterType()); + parameter.getAnnotations().forEach(this::addAnnotationImports); + return this; + } + + /** + * Adds imports for an interface. + * + * @param interfaceName the interface to add imports for + */ + public ImportCollector addInterfaceImports(InterfaceName interfaceName) { + // Create a TypeName from the interface's package and simple name + TypeName interfaceType = + new TypeName(interfaceName.getPackageName(), interfaceName.getSimpleName()); + addTypeImports(interfaceType); + interfaceName.getAnnotations().forEach(this::addAnnotationImports); + interfaceName.getTypeParameters().forEach(this::addTypeImports); + return this; + } + + /** + * Adds imports for a method's body and components. + * + * @param method the method to add imports for + */ + public ImportCollector addMethodImports(MethodDto method) { + if (method.getReturnType() != null) { + addTypeImports(method.getReturnType()); + } + + // Add generic parameter imports + method + .getGenericParameters() + .forEach(generic -> generic.getUpperBounds().forEach(this::addTypeImports)); + + // Add code block imports + method.getMethodCodeDto().getCodeBlockImports().forEach(this::addImport); + + // Add annotation imports + method.getAnnotations().forEach(this::addAnnotationImports); + + // Add parameter imports + method.getParameters().forEach(this::addParameterImports); + + // Add body argument imports + addBodyImports(method); + + return this; + } + + /** + * Collects all imports from a GenerationTargetClassDto. + * + * @param classDef the class definition to extract imports from + */ + public ImportCollector collectImports(GenerationTargetClassDto classDef) { + // Add imports from the DTO (including static imports like TrackedValue) + classDef.getImports().forEach(this::addImport); + + // Add superclass imports + if (classDef.getSuperType() != null) { + addTypeImports(classDef.getSuperType()); + } + + // Add class type and generics + addTypeImports(classDef.getTypeName()); + classDef + .getGenerics() + .forEach(generic -> generic.getUpperBounds().forEach(this::addTypeImports)); + + // Add interface imports + classDef.getInterfaces().forEach(this::addInterfaceImports); + + // Add class annotation imports + classDef.getClassAnnotations().forEach(this::addAnnotationImports); + + // Add field imports from ClassFieldDto + classDef.getClassFields().forEach(field -> addTypeImports(field.getFieldType())); + + // Add all additional imports defined for that field + classDef.getClassFields().stream() + .flatMap(field -> field.getFieldTypeImports().stream()) + .forEach(this::addImport); + + // Add constructor parameter imports + classDef.getConstructors().stream() + .flatMap(constructor -> constructor.getParameters().stream()) + .forEach(this::addParameterImports); + + // Add constructor code block imports + classDef.getConstructors().stream() + .flatMap(constructor -> constructor.getMethodCodeDto().getCodeBlockImports().stream()) + .forEach(this::addImport); + + // Add nested-type annotation imports + classDef.getNestedTypes().stream() + .flatMap(nested -> nested.getAnnotations().stream()) + .forEach(this::addAnnotationImports); + + // Add nested-type method imports + classDef.getNestedTypes().stream() + .flatMap(nested -> nested.getMethods().stream()) + .forEach(this::addMethodImports); + + // Add method imports + classDef.getMethods().forEach(this::addMethodImports); + + return this; + } + + /** + * Adds imports from method body arguments. + * + * @param method the method to analyze + */ + private void addBodyImports(MethodDto method) { + if (!method.hasCode()) { + return; + } + + for (MethodCodePlaceholder argument : method.getMethodCodeDto().getCodeArguments()) { + if (argument instanceof MethodCodeTypePlaceholder typePlaceholder) { + addTypeImports(typePlaceholder.getValue()); + } + } + } + + /** + * Adds an import statement if it should not be skipped. + * + *

This method contains the business logic for filtering imports. + * + * @param importStatement the import statement to add + */ + private void addImport(ImportStatement importStatement) { + if (importStatement == null || shouldSkipImport(importStatement)) { + return; + } + imports.add(importStatement); + } + + /** + * Determines if an import should be skipped (business logic). + * + * @param importStatement the import to check + * @return true if the import should be skipped + */ + private boolean shouldSkipImport(ImportStatement importStatement) { + String fqn = importStatement.getFullyQualifiedName(); + + if (StringUtils.isBlank(fqn)) { + return true; + } + + // For regular imports, skip if in exact same package or java.lang (per Java rules) + if (!importStatement.isStatic()) { + // Skip if it's in the exact same package (but not sub-packages) - Java doesn't require + // imports for same-package classes + if (Strings.CI.equals(importStatement.getPackageName(), currentType.getPackageName())) { + return true; + } + + // Skip java.lang package (auto-imported), but NOT subpackages like java.lang.annotation + if (Strings.CI.equals(importStatement.getPackageName(), "java.lang")) { + return true; + } + } + + // Static imports are never skipped based on package + // (they're always needed to use the static member without qualification) + return false; + } + + /** + * Adds a regular import for a type. + * + * @param type the type to import + */ + private void addImport(TypeName type) { + if (type == null) { + return; + } + + if (type instanceof TypeNameArray arrayType) { + addImport(arrayType.getTypeOfArray()); + return; + } + + if (type instanceof TypeNameGeneric genericType) { + // Add the raw generic type (without type parameters) + addImport(genericType.getRawType()); + + // Add inner type arguments + genericType.getInnerTypeArguments().forEach(this::addTypeImports); + return; + } + + // Add the concrete type + addImport(new RegularImport(type)); + } + + /** + * Returns the collected import statements as a sorted set. + * + *

Use this method when you want imports in alphabetical order. The sorting is done here to + * provide flexibility for the calling code. + * + *

Static imports are placed before regular imports, following Java conventions. Within each + * group, imports are sorted alphabetically by fully qualified name. + * + * @return sorted set of import statements + */ + public Set getSortedImports() { + Comparator staticFirstComparator = + Comparator.comparing(ImportStatement::isStatic).reversed(); + Comparator alphabeticalComparator = + Comparator.comparing(ImportStatement::getFullyQualifiedName); + + return getImports().stream() + .sorted(staticFirstComparator.thenComparing(alphabeticalComparator)) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } + + /** + * Returns the collected import statements in insertion order. + * + *

This preserves the order in which imports were added, which can be useful for maintaining a + * specific import ordering strategy. + * + * @return set of import statements in insertion order + */ + public Set getImports() { + return new LinkedHashSet<>(imports); + } + + /** + * Collects and sorts imports for a given class definition. + * + * @param classDef the class definition to collect imports from + * @return sorted set of import statements + */ + public static Set collectAndSortImports(GenerationTargetClassDto classDef) { + return new ImportCollector(classDef.getTypeName()).collectImports(classDef).getSortedImports(); + } +} diff --git a/processor/src/main/resources/META-INF/services/org.javahelpers.simple.builders.processor.generators.Generator b/processor/src/main/resources/META-INF/services/org.javahelpers.simple.builders.processor.generators.Generator index e859fec3..b72cbd9f 100644 --- a/processor/src/main/resources/META-INF/services/org.javahelpers.simple.builders.processor.generators.Generator +++ b/processor/src/main/resources/META-INF/services/org.javahelpers.simple.builders.processor.generators.Generator @@ -25,5 +25,6 @@ org.javahelpers.simple.builders.processor.generators.builder.JacksonAnnotationEn org.javahelpers.simple.builders.processor.generators.builder.InterfaceEnhancer org.javahelpers.simple.builders.processor.generators.builder.ClassJavaDocEnhancer org.javahelpers.simple.builders.processor.generators.builder.CoreMethodsEnhancer +org.javahelpers.simple.builders.processor.generators.builder.ConstructorEnhancer org.javahelpers.simple.builders.processor.generators.builder.WithInterfaceEnhancer org.javahelpers.simple.builders.processor.generators.builder.ConditionalEnhancer diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderConfigurationReaderTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderConfigurationReaderTest.java index 9deb48e6..f88df074 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderConfigurationReaderTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderConfigurationReaderTest.java @@ -463,8 +463,6 @@ public class PersonDto { import static org.javahelpers.simple.builders.core.util.TrackedValue.unsetValue; import java.util.List; - import java.util.function.Consumer; - import java.util.function.Supplier; import org.apache.commons.lang3.builder.ToStringBuilder; import org.javahelpers.simple.builders.core.interfaces.IBuilderBase; import org.javahelpers.simple.builders.core.util.BuilderToStringStyle; diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java index cbb3f980..026fc763 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderProcessorTest.java @@ -92,8 +92,8 @@ void shouldLogDebugMessagesWhenVerboseModeEnabled() { "[DEBUG] Starting BuilderProcessor...", "[DEBUG] Loaded global configuration from compiler arguments: BuilderConfiguration[]", "[DEBUG] Initializing generator registry", - "[DEBUG] ├─ Loaded 14 method generators and 8 builder enhancers total", - "[DEBUG] └─ Initialized GeneratorRegistry with 14 method generators and 8 builder", + "[DEBUG] ├─ Loaded 14 method generators and 9 builder enhancers total", + "[DEBUG] └─ Initialized GeneratorRegistry with 14 method generators and 9 builder", "simple-builders: PROCESSING ROUND START", "[DEBUG] simple-builders: Processing round started. Found 1 annotated elements.", "[DEBUG] Processing element: VerboseTest", @@ -120,25 +120,29 @@ void shouldLogDebugMessagesWhenVerboseModeEnabled() { "[DEBUG] │ │ ├─ Applying: WithInterfaceEnhancer (priority: 95)", "[DEBUG] │ │ ├─ Applying: InterfaceEnhancer (priority: 90)", "[DEBUG] │ │ ├─ Applying: ConditionalEnhancer (priority: 80)", - "[DEBUG] │ │ └─ Applied 7 builder enhancers", + "[DEBUG] │ │ └─ Applied 8 builder enhancers", + "[DEBUG] │ ├─ Finalizing builder definition", + "[DEBUG] │ │ └─ Finalized: 1 class fields, 9 methods, 2 constructors", + "[DEBUG] │ ├─ Builder will be generated as: VerboseTestBuilder", "[DEBUG] │ └─ Builder definition extracted: VerboseTestBuilder", - "[DEBUG] ├─ Code generation for builder: VerboseTestBuilder", - "[DEBUG] │ ├─ Class builder created", + "[DEBUG] ├─ Code generation for class: VerboseTestBuilder", + "[DEBUG] │ ├─ JavaClassSource created", "[DEBUG] │ ├─ Class metadata added", - "[DEBUG] │ ├─ Generating 0 constructor fields and 1 setter fields", + "[DEBUG] │ ├─ Generating 1 fields", "[DEBUG] │ │ └─ Fields added: 1 fields", - "[DEBUG] │ ├─ Adding Methods for 9 candidates", - "[DEBUG] │ │ ├─ Resolved 9 methods after conflict resolution", - "[DEBUG] │ │ └─ 9 Methods added", - "[DEBUG] │ ├─ Constructors added", + "[DEBUG] │ ├─ Generating 2 constructors", + "[DEBUG] │ │ └─ Constructors added: 2", + "[DEBUG] │ ├─ Generating 9 method candidates", + "[DEBUG] │ │ ├─ Resolved to 9 methods after conflict resolution", + "[DEBUG] │ │ └─ Methods added: 9", "[DEBUG] │ ├─ Generating 1 nested type(s)", "[DEBUG] │ │ ├─ Generated nested type: With", "[DEBUG] │ │ └─ Nested types added", "[DEBUG] │ ├─ Class-level annotations added", - "[DEBUG] │ ├─ Writing builder class to file: test.VerboseTestBuilder", - "[DEBUG] │ └─ Successfully generated builder: VerboseTestBuilder", + "[DEBUG] │ ├─ Writing class to file: test.VerboseTestBuilder", + "[DEBUG] │ └─ Successfully generated class: VerboseTestBuilder", "[DEBUG] ├─ Jackson module entry added", - "[DEBUG] └─ Generated builder with 1 fields and 5 methods for VerboseTestBuilder", + "[DEBUG] └─ Generated builder with 1 fields and 9 methods for VerboseTestBuilder", "simple-builders: Successfully generated 1 builder(s) in this processing round", ""); } @@ -1134,6 +1138,96 @@ void shouldGenerateBothConstructorsWithJavadocs() { "private TrackedValue x = unsetValue();"); } + @Test + void shouldValidateNonNullFieldsInFromInstanceConstructor() { + // Given + String className = "NonNullValidation"; + String builderClassName = className + "Builder"; + + JavaFileObject notNullAnnotation = + ProcessorTestUtils.forSource( + """ + package jakarta.validation.constraints; + import java.lang.annotation.*; + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.FIELD, ElementType.PARAMETER}) + public @interface NotNull { + String message() default ""; + } + """); + + JavaFileObject sourceFile = + ProcessorTestUtils.forSource( + """ + package test; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + import jakarta.validation.constraints.NotNull; + + @SimpleBuilder + public class NonNullValidation { + private String requiredField; + private String optionalField; + private int primitiveField; + + public String getRequiredField() { return requiredField; } + public void setRequiredField(@NotNull String requiredField) { + this.requiredField = requiredField; + } + + public String getOptionalField() { return optionalField; } + public void setOptionalField(String optionalField) { + this.optionalField = optionalField; + } + + public int getPrimitiveField() { return primitiveField; } + public void setPrimitiveField(int primitiveField) { + this.primitiveField = primitiveField; + } + } + """); + + // When + Compilation compilation = compile(notNullAnnotation, sourceFile); + + // Then + String generatedCode = loadGeneratedSource(compilation, builderClassName); + assertGenerationSucceeded(compilation, builderClassName, generatedCode); + + // Verify from-instance constructor validates @NotNull fields but not nullable/primitive fields + ProcessorAsserts.assertContaining( + generatedCode, + """ + public NonNullValidationBuilder(NonNullValidation instance) { + this.optionalField = initialValue(instance.getOptionalField()); + this.primitiveField = initialValue(instance.getPrimitiveField()); + if (instance.getRequiredField() == null) { + throw new IllegalArgumentException( + "Field 'requiredField' is non-null but instance.getRequiredField() returned null"); + } + this.requiredField = initialValue(instance.getRequiredField()); + } + """); + + // Verify build() method has validation for @NotNull field + ProcessorAsserts.assertContaining( + generatedCode, + """ + public NonNullValidation build() { + if (this.primitiveField.isSet() && this.primitiveField.value() == null) { + throw new IllegalStateException("Field 'primitiveField' is marked as non-null but null value was provided"); + } + if (this.requiredField.isSet() && this.requiredField.value() == null) { + throw new IllegalStateException("Field 'requiredField' is marked as non-null but null value was provided"); + } + NonNullValidation result = new NonNullValidation(); + this.optionalField.ifSet(result::setOptionalField); + this.primitiveField.ifSet(result::setPrimitiveField); + this.requiredField.ifSet(result::setRequiredField); + return result; + } + """); + } + @Test void shouldGenerateMethodJavadocsForAllMethodTypes() { // Given diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/ComprehensiveFeatureIntegrationTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/ComprehensiveFeatureIntegrationTest.java index 86a4fe7b..4cd832b2 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/ComprehensiveFeatureIntegrationTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/ComprehensiveFeatureIntegrationTest.java @@ -180,6 +180,7 @@ public PersonDto(String name, int age, Optional email, @Generated("Generated by org.javahelpers.simple.builders.processor.BuilderProcessor") @BuilderImplementation(forClass = PersonDto.class) public class PersonDtoBuilder implements IBuilderBase { + /** * Tracked value for name: name. */ @@ -239,10 +240,6 @@ public PersonDtoBuilder() { public PersonDtoBuilder(PersonDto instance) { this.name = initialValue(instance.getName()); this.age = initialValue(instance.getAge()); - if (this.age.value() == null) { - throw new IllegalArgumentException( - "Cannot initialize builder from instance: field 'age' is marked as non-null but source object has null value"); - } this.email = initialValue(instance.getEmail()); this.nicknames = initialValue(instance.getNicknames()); this.tags = initialValue(instance.getTags()); @@ -352,8 +349,8 @@ public PersonDtoBuilder address(AddressDto address) { */ public PersonDtoBuilder address(Consumer addressBuilderConsumer) { AddressDtoBuilder builder = this.address.isSet() - ? new AddressDtoBuilder(this.address.value()) - : new AddressDtoBuilder(); + ? new AddressDtoBuilder(this.address.value()) + : new AddressDtoBuilder(); addressBuilderConsumer.accept(builder); this.address = changedValue(builder.build()); return this; @@ -481,8 +478,8 @@ public PersonDtoBuilder metadata(Map metadata) { */ public PersonDtoBuilder metadata(Consumer> metadataBuilderConsumer) { HashMapBuilder builder = this.metadata.isSet() - ? new HashMapBuilder(this.metadata.value()) - : new HashMapBuilder(); + ? new HashMapBuilder(this.metadata.value()) + : new HashMapBuilder(); metadataBuilderConsumer.accept(builder); this.metadata = changedValue(builder.build()); return this; @@ -577,8 +574,8 @@ public PersonDtoBuilder nicknames(List nicknames) { */ public PersonDtoBuilder nicknames(Consumer> nicknamesBuilderConsumer) { ArrayListBuilder builder = this.nicknames.isSet() - ? new ArrayListBuilder(this.nicknames.value()) - : new ArrayListBuilder(); + ? new ArrayListBuilder(this.nicknames.value()) + : new ArrayListBuilder(); nicknamesBuilderConsumer.accept(builder); this.nicknames = changedValue(builder.build()); return this; @@ -623,11 +620,10 @@ public PersonDtoBuilder phoneNumbers(LinkedList phoneNumbers) { * @param phoneNumbersBuilderConsumer consumer providing an instance of a builder for phoneNumbers * @return current instance of builder */ - public PersonDtoBuilder phoneNumbers( - Consumer> phoneNumbersBuilderConsumer) { + public PersonDtoBuilder phoneNumbers(Consumer> phoneNumbersBuilderConsumer) { ArrayListBuilder builder = this.phoneNumbers.isSet() - ? new ArrayListBuilder(this.phoneNumbers.value()) - : new ArrayListBuilder(); + ? new ArrayListBuilder(this.phoneNumbers.value()) + : new ArrayListBuilder(); phoneNumbersBuilderConsumer.accept(builder); this.phoneNumbers = changedValue(new LinkedList<>(builder.build())); return this; @@ -675,8 +671,9 @@ public PersonDtoBuilder previousAddresses(List previousAddresses) { public PersonDtoBuilder previousAddresses( Consumer> previousAddressesBuilderConsumer) { ArrayListBuilderWithElementBuilders builder = this.previousAddresses.isSet() - ? new ArrayListBuilderWithElementBuilders(this.previousAddresses.value(), AddressDtoBuilder::create) - : new ArrayListBuilderWithElementBuilders(AddressDtoBuilder::create); + ? new ArrayListBuilderWithElementBuilders(this.previousAddresses.value(), + AddressDtoBuilder::create) + : new ArrayListBuilderWithElementBuilders(AddressDtoBuilder::create); previousAddressesBuilderConsumer.accept(builder); this.previousAddresses = changedValue(builder.build()); return this; @@ -723,8 +720,8 @@ public PersonDtoBuilder tags(Set tags) { */ public PersonDtoBuilder tags(Consumer> tagsBuilderConsumer) { HashSetBuilder builder = this.tags.isSet() - ? new HashSetBuilder(this.tags.value()) - : new HashSetBuilder(); + ? new HashSetBuilder(this.tags.value()) + : new HashSetBuilder(); tagsBuilderConsumer.accept(builder); this.tags = changedValue(builder.build()); return this; @@ -760,11 +757,12 @@ public PersonDtoBuilder conditional(BooleanSupplier condition, Consumer trueCase, Consumer falseCase) { + public PersonDtoBuilder conditional(BooleanSupplier condition, Consumer trueCase, + Consumer falseCase) { if (condition.getAsBoolean()) { - trueCase.accept(this); + trueCase.accept(this); } else if (falseCase != null) { - falseCase.accept(this); + falseCase.accept(this); } return this; } @@ -781,14 +779,14 @@ public PersonDto build() { throw new IllegalStateException("Field 'age' is marked as non-null but null value was provided"); } PersonDto result = new PersonDto(this.name.value(), - this.age.value(), - this.email.value(), - this.nicknames.value(), - this.tags.value(), - this.metadata.value(), - this.address.value(), - this.previousAddresses.value(), - this.phoneNumbers.value()); + this.age.value(), + this.email.value(), + this.nicknames.value(), + this.tags.value(), + this.metadata.value(), + this.address.value(), + this.previousAddresses.value(), + this.phoneNumbers.value()); return result; } @@ -800,15 +798,15 @@ public PersonDto build() { @Override public String toString() { return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE).append("name", this.name) - .append("age", this.age) - .append("email", this.email) - .append("nicknames", this.nicknames) - .append("tags", this.tags) - .append("metadata", this.metadata) - .append("address", this.address) - .append("previousAddresses", this.previousAddresses) - .append("phoneNumbers", this.phoneNumbers) - .toString(); + .append("age", this.age) + .append("email", this.email) + .append("nicknames", this.nicknames) + .append("tags", this.tags) + .append("metadata", this.metadata) + .append("address", this.address) + .append("previousAddresses", this.previousAddresses) + .append("phoneNumbers", this.phoneNumbers) + .toString(); } /** @@ -853,8 +851,6 @@ default PersonDtoBuilder with() { } """; - // For now, just use the actual generated code as the expected template - // This ensures the test passes and will catch any future changes ProcessorAsserts.assertNormalizedEquals( expectedCode, generatedCode, diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/CustomizingDocumentationTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/CustomizingDocumentationTest.java index 7faacda1..91781cbb 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/CustomizingDocumentationTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/CustomizingDocumentationTest.java @@ -486,7 +486,7 @@ public boolean appliesTo(BuilderDefinitionDto builderDto, TypeName dtoType, Proc public void enhanceBuilder(BuilderDefinitionDto builderDto, ProcessingContext context) { // Add validation method to builder MethodDto validateMethod = createValidateMethod(); - builderDto.addCoreMethod(validateMethod); + builderDto.addMethod(validateMethod); // Add @Valid annotation if available if (isValidationAvailable(context)) { @@ -665,7 +665,7 @@ public void enhanceBuilder(BuilderDefinitionDto builderDto, ProcessingContext co factoryMethod.setCode(String.format("return new %s();", builderType.getClassName())); // Note: addStaticMethod is conceptual - shows the pattern - builderDto.addCoreMethod(factoryMethod); + builderDto.addMethod(factoryMethod); context.debug("Added factory method to builder %s", builderType.getClassName()); } diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/MethodConflictResolutionTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/MethodConflictResolutionTest.java index e8811508..9ccb1665 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/MethodConflictResolutionTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/MethodConflictResolutionTest.java @@ -390,7 +390,7 @@ public void setValue(java.util.function.Supplier valueSupplier) { .filter( d -> d.getMessage(null).contains("Method conflict") - && d.getMessage(null).contains("dropped in favor of field")) + && d.getMessage(null).contains("dropped in favor of")) .filter(d -> d.getMessage(null).contains("priority 80")) .filter(d -> d.getMessage(null).contains("priority 100")) .count();