diff --git a/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilder.java b/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilder.java
index 2667deab..78c30449 100644
--- a/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilder.java
+++ b/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilder.java
@@ -331,6 +331,16 @@
*/
OptionState generateUnboxedOptional() default OptionState.UNSET;
+ /**
+ * Copy type annotations from the DTO fields to the builder fields/methods.
+ * Useful for validation annotations (e.g. @NotNull, @Size) or other metadata that should be
+ * preserved.
+ *
+ *
Default: ENABLED
+ * Compiler option: -Asimplebuilder.copyTypeAnnotations
+ */
+ OptionState copyTypeAnnotations() default OptionState.UNSET;
+
/**
* Generate helper methods with a ArrayListBuilder supplier for lists instead of simple
* supplier, which would not allow to use in a chanined way:
diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md
index 16be437a..103166a9 100644
--- a/docs/CONFIGURATION.md
+++ b/docs/CONFIGURATION.md
@@ -461,6 +461,26 @@ PersonDto person = PersonDtoBuilder.create()
---
+#### `copyTypeAnnotations`
+
+**Default**: `ENABLED` | **Compiler Option**: `-Asimplebuilder.copyTypeAnnotations=ENABLED|DISABLED`
+
+Copies type annotations (TYPE_USE) from the DTO fields to the builder fields and methods. This is useful for validation annotations (e.g. `@NotNull`, `@Size`) or other metadata that should be preserved.
+
+**When ENABLED**:
+```java
+// DTO
+private List<@NotNull String> items;
+
+// Generated Builder
+private TrackedValue> items;
+public Builder items(List<@NotNull String> items) { ... }
+```
+
+**When DISABLED**: Type annotations are stripped from the builder.
+
+---
+
### Collection Helpers
#### `usingArrayListBuilder`
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/AnnotationDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/AnnotationDto.java
index b06cad0c..04d9f9e3 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/AnnotationDto.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/AnnotationDto.java
@@ -26,6 +26,8 @@
import java.util.LinkedHashMap;
import java.util.Map;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
/**
* DTO representing an annotation to be copied from the target class field to the builder class
@@ -79,4 +81,27 @@ public Map getMembers() {
public void addMember(String name, String value) {
this.members.put(name, value);
}
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ AnnotationDto that = (AnnotationDto) o;
+
+ return new EqualsBuilder()
+ .append(annotationType, that.annotationType)
+ .append(members, that.members)
+ .isEquals();
+ }
+
+ @Override
+ public int hashCode() {
+ return new HashCodeBuilder(17, 37).append(annotationType).append(members).toHashCode();
+ }
}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java
index 8ebb91a8..3f3f4b93 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java
@@ -76,6 +76,7 @@ public record BuilderConfiguration(
OptionState generateStringFormatHelpers,
OptionState generateAddToCollectionHelpers,
OptionState generateUnboxedOptional,
+ OptionState copyTypeAnnotations,
OptionState usingArrayListBuilder,
OptionState usingArrayListBuilderWithElementBuilders,
OptionState usingHashSetBuilder,
@@ -101,6 +102,7 @@ public record BuilderConfiguration(
.generateStringFormatHelpers(ENABLED)
.generateAddToCollectionHelpers(ENABLED)
.generateUnboxedOptional(ENABLED)
+ .copyTypeAnnotations(ENABLED)
.usingArrayListBuilder(ENABLED)
.usingArrayListBuilderWithElementBuilders(ENABLED)
.usingHashSetBuilder(ENABLED)
@@ -171,6 +173,10 @@ public boolean shouldGenerateUnboxedOptional() {
return generateUnboxedOptional == ENABLED;
}
+ public boolean shouldCopyTypeAnnotations() {
+ return copyTypeAnnotations == ENABLED;
+ }
+
public boolean shouldUseGeneratedAnnotation() {
return usingGeneratedAnnotation == ENABLED;
}
@@ -239,6 +245,7 @@ public BuilderConfiguration merge(BuilderConfiguration other) {
other.generateAddToCollectionHelpers, this.generateAddToCollectionHelpers))
.generateUnboxedOptional(
mergeOptionState(other.generateUnboxedOptional, this.generateUnboxedOptional))
+ .copyTypeAnnotations(mergeOptionState(other.copyTypeAnnotations, this.copyTypeAnnotations))
.usingArrayListBuilder(
mergeOptionState(other.usingArrayListBuilder, this.usingArrayListBuilder))
.usingArrayListBuilderWithElementBuilders(
@@ -328,6 +335,12 @@ public String toString() {
if (generateVarArgsHelpers != UNSET) {
builder.append("generateVarArgsHelpers", generateVarArgsHelpers);
}
+ if (generateUnboxedOptional != UNSET) {
+ builder.append("generateUnboxedOptional", generateUnboxedOptional);
+ }
+ if (copyTypeAnnotations != UNSET) {
+ builder.append("copyTypeAnnotations", copyTypeAnnotations);
+ }
if (usingArrayListBuilder != UNSET) {
builder.append("usingArrayListBuilder", usingArrayListBuilder);
}
@@ -379,6 +392,7 @@ public static class Builder {
private OptionState generateStringFormatHelpers = OptionState.UNSET;
private OptionState generateAddToCollectionHelpers = OptionState.UNSET;
private OptionState generateUnboxedOptional = OptionState.UNSET;
+ private OptionState copyTypeAnnotations = OptionState.UNSET;
private OptionState usingArrayListBuilder = OptionState.UNSET;
private OptionState usingArrayListBuilderWithElementBuilders = OptionState.UNSET;
private OptionState usingHashSetBuilder = OptionState.UNSET;
@@ -488,6 +502,16 @@ public Builder generateUnboxedOptional(boolean value) {
return this;
}
+ public Builder copyTypeAnnotations(OptionState value) {
+ this.copyTypeAnnotations = value;
+ return this;
+ }
+
+ public Builder copyTypeAnnotations(boolean value) {
+ this.copyTypeAnnotations = value ? ENABLED : DISABLED;
+ return this;
+ }
+
public Builder usingArrayListBuilder(OptionState value) {
this.usingArrayListBuilder = value;
return this;
@@ -621,6 +645,7 @@ public BuilderConfiguration build() {
generateStringFormatHelpers,
generateAddToCollectionHelpers,
generateUnboxedOptional,
+ copyTypeAnnotations,
usingArrayListBuilder,
usingArrayListBuilderWithElementBuilders,
usingHashSetBuilder,
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/TypeName.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/TypeName.java
index 01e772e4..c42f999d 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/TypeName.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/TypeName.java
@@ -27,6 +27,8 @@
import static java.util.Objects.requireNonNull;
import java.util.Optional;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
/**
* TypeName is the unambiguously definition of a type. Holding name of class and package. Could be
@@ -39,6 +41,9 @@ public class TypeName {
/** Name of class. */
private final String className;
+ /** Annotations on this type (TYPE_USE). */
+ private final java.util.List annotations = new java.util.ArrayList<>();
+
/**
* Constructor for TypeName.
*
@@ -69,6 +74,24 @@ public String getClassName() {
return className;
}
+ /**
+ * Returns the list of annotations on this type.
+ *
+ * @return list of annotations
+ */
+ public java.util.List getAnnotations() {
+ return annotations;
+ }
+
+ /**
+ * Adds an annotation to this type.
+ *
+ * @param annotation the annotation to add
+ */
+ public void addAnnotation(AnnotationDto annotation) {
+ this.annotations.add(annotation);
+ }
+
/**
* Helper function to hold a specific inner type.Is empty if this is a class without generic parts
* or a class has multiple generics.
@@ -89,4 +112,27 @@ public Optional getInnerType() {
public static TypeName of(Class> clazz) {
return new TypeName(clazz.getPackage().getName(), clazz.getSimpleName());
}
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ TypeName typeName = (TypeName) o;
+
+ return new EqualsBuilder()
+ .append(packageName, typeName.packageName)
+ .append(className, typeName.className)
+ .isEquals();
+ }
+
+ @Override
+ public int hashCode() {
+ return new HashCodeBuilder(17, 37).append(packageName).append(className).toHashCode();
+ }
}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/TypeNamePrimitive.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/TypeNamePrimitive.java
index 2e109867..4cc58add 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/TypeNamePrimitive.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/TypeNamePrimitive.java
@@ -65,7 +65,7 @@ public class TypeNamePrimitive extends TypeName {
*
* @param primitiveType primitive enum type
*/
- protected TypeNamePrimitive(PrimitiveTypeEnum primitiveType) {
+ public TypeNamePrimitive(PrimitiveTypeEnum primitiveType) {
super("", primitiveType.name().toLowerCase());
this.type = primitiveType;
}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java
index 77f8b1b4..61d93b32 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java
@@ -75,6 +75,9 @@ public enum CompilerArgumentsEnum {
/** Option for unboxed optional generation. */
GENERATE_UNBOXED_OPTIONAL("generateUnboxedOptional"),
+ /** Option for copying type annotations. */
+ COPY_TYPE_ANNOTATIONS("copyTypeAnnotations"),
+
/** Option for ArrayList builder usage. */
USING_ARRAY_LIST_BUILDER("usingArrayListBuilder"),
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java
index fa94c6c7..70b113e9 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java
@@ -175,6 +175,7 @@ private BuilderConfiguration parseOptionsFromMirror(AnnotationMirror optionsMirr
builder.generateAddToCollectionHelpers(OptionState.valueOf(enumValue));
case "generateUnboxedOptional" ->
builder.generateUnboxedOptional(OptionState.valueOf(enumValue));
+ case "copyTypeAnnotations" -> builder.copyTypeAnnotations(OptionState.valueOf(enumValue));
case "usingArrayListBuilder" ->
builder.usingArrayListBuilder(OptionState.valueOf(enumValue));
case "usingArrayListBuilderWithElementBuilders" ->
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java
index f4d0c1ce..a943e912 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java
@@ -842,6 +842,10 @@ private static Optional createFieldDto(
// Extract annotations from the field parameter
List annotations = FieldAnnotationExtractor.extractAnnotations(param, context);
+ // Annotations could be assigned to Type or Parameter.
+ // To avoid duplication in generated code, we need to remove the duplications here.
+ annotations.removeAll(fieldType.getAnnotations());
+
// Check if field has non-null constraint (annotation or primitive type)
if (FieldAnnotationExtractor.hasNonNullConstraint(param)
|| fieldTypeMirror.getKind().isPrimitive()) {
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java
index 10497705..585883f4 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java
@@ -156,6 +156,7 @@ public BuilderConfiguration readBuilderConfiguration() {
.generateAddToCollectionHelpers(
readOptionState(CompilerArgumentsEnum.GENERATE_ADD_TO_COLLECTION_HELPERS))
.generateUnboxedOptional(readOptionState(CompilerArgumentsEnum.GENERATE_UNBOXED_OPTIONAL))
+ .copyTypeAnnotations(readOptionState(CompilerArgumentsEnum.COPY_TYPE_ANNOTATIONS))
.usingArrayListBuilder(readOptionState(CompilerArgumentsEnum.USING_ARRAY_LIST_BUILDER))
.usingArrayListBuilderWithElementBuilders(
readOptionState(CompilerArgumentsEnum.USING_ARRAY_LIST_BUILDER_WITH_ELEMENT_BUILDERS))
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/FieldAnnotationExtractor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/FieldAnnotationExtractor.java
index 18fd3a1d..777cd2c7 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/FieldAnnotationExtractor.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/FieldAnnotationExtractor.java
@@ -91,6 +91,30 @@ public static List extractAnnotations(
return annotations;
}
+ /**
+ * Extracts annotations from a type mirror. Filters out annotations that should not be copied to
+ * the builder.
+ *
+ * @param typeMirror the type mirror containing annotations
+ * @param context processing context
+ * @return list of annotations to be copied to the builder field
+ */
+ public static List extractAnnotations(
+ javax.lang.model.type.TypeMirror typeMirror, ProcessingContext context) {
+ List annotations = new ArrayList<>();
+ List extends AnnotationMirror> annotationMirrors = typeMirror.getAnnotationMirrors();
+
+ context.debug(
+ " -> Extracting %d annotation(s) from type %s",
+ annotationMirrors.size(), typeMirror.toString());
+
+ for (AnnotationMirror mirror : annotationMirrors) {
+ extractAnnotation(mirror, context).ifPresent(annotations::add);
+ }
+
+ return annotations;
+ }
+
/**
* Extracts a single annotation from an AnnotationMirror. Filters out annotations that should not
* be copied to the builder.
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaLangMapper.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaLangMapper.java
index bf265a70..dbad15a0 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaLangMapper.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaLangMapper.java
@@ -394,65 +394,78 @@ private static TypeMirror findSupertype(
}
private static TypeName extractType(TypeMirror typeOfParameter, ProcessingContext context) {
- return typeOfParameter.accept(
- new SimpleTypeVisitor14() {
-
- @Override
- public TypeName visitPrimitive(PrimitiveType t, Void p) {
- return switch (t.getKind()) {
- case BOOLEAN -> TypeNamePrimitive.BOOLEAN;
- case BYTE -> TypeNamePrimitive.BYTE;
- case SHORT -> TypeNamePrimitive.SHORT;
- case INT -> TypeNamePrimitive.INT;
- case LONG -> TypeNamePrimitive.LONG;
- case CHAR -> TypeNamePrimitive.CHAR;
- case FLOAT -> TypeNamePrimitive.FLOAT;
- case DOUBLE -> TypeNamePrimitive.DOUBLE;
- default -> throw new IllegalStateException("Unsupported Primitive type");
- };
- }
-
- @Override
- public TypeName visitDeclared(DeclaredType t, Void p) {
- TypeElement elementOfParameter = (TypeElement) context.asElement(typeOfParameter);
- String simpleClassName = elementOfParameter.getSimpleName().toString();
- String packageName = context.getPackageName(elementOfParameter);
- TypeName rawType = new TypeName(packageName, simpleClassName);
- TypeMirror enclosingType = t.getEnclosingType();
- TypeName enclosing =
- (enclosingType.getKind() != NONE)
- && !t.asElement().getModifiers().contains(Modifier.STATIC)
- ? enclosingType.accept(this, null)
- : null;
- if (t.getTypeArguments().isEmpty() && !(enclosing instanceof TypeNameGeneric)) {
- return wrapInCollectionTypeIfApplicable(rawType, List.of(), typeOfParameter, context);
- }
-
- List typesExtracted = new ArrayList<>(t.getTypeArguments());
- if (typesExtracted.isEmpty()) {
- return wrapInCollectionTypeIfApplicable(rawType, List.of(), typeOfParameter, context);
- } else {
- List argTypes = extractTypeForList(typesExtracted, context);
- return wrapInCollectionTypeIfApplicable(rawType, argTypes, typeOfParameter, context);
- }
- }
-
- @Override
- public TypeNameArray visitArray(ArrayType t, Void p) {
- return new TypeNameArray(extractType(t.getComponentType(), context));
- }
-
- @Override
- public TypeName visitTypeVariable(TypeVariable t, Void p) {
- String name = t.asElement().getSimpleName().toString();
- return new TypeNameVariable(name);
- }
-
- @Override
- protected TypeName defaultAction(TypeMirror e, Void p) {
- throw new IllegalArgumentException("Unexpected type mirror: " + e);
- }
- },
- null);
+ TypeName typeName =
+ typeOfParameter.accept(
+ new SimpleTypeVisitor14() {
+
+ @Override
+ public TypeName visitPrimitive(PrimitiveType t, Void p) {
+ return switch (t.getKind()) {
+ case BOOLEAN ->
+ new TypeNamePrimitive(TypeNamePrimitive.PrimitiveTypeEnum.BOOLEAN);
+ case BYTE -> new TypeNamePrimitive(TypeNamePrimitive.PrimitiveTypeEnum.BYTE);
+ case SHORT -> new TypeNamePrimitive(TypeNamePrimitive.PrimitiveTypeEnum.SHORT);
+ case INT -> new TypeNamePrimitive(TypeNamePrimitive.PrimitiveTypeEnum.INT);
+ case LONG -> new TypeNamePrimitive(TypeNamePrimitive.PrimitiveTypeEnum.LONG);
+ case CHAR -> new TypeNamePrimitive(TypeNamePrimitive.PrimitiveTypeEnum.CHAR);
+ case FLOAT -> new TypeNamePrimitive(TypeNamePrimitive.PrimitiveTypeEnum.FLOAT);
+ case DOUBLE -> new TypeNamePrimitive(TypeNamePrimitive.PrimitiveTypeEnum.DOUBLE);
+ default -> throw new IllegalStateException("Unsupported Primitive type");
+ };
+ }
+
+ @Override
+ public TypeName visitDeclared(DeclaredType t, Void p) {
+ TypeElement elementOfParameter = (TypeElement) context.asElement(typeOfParameter);
+ String simpleClassName = elementOfParameter.getSimpleName().toString();
+ String packageName = context.getPackageName(elementOfParameter);
+ TypeName rawType = new TypeName(packageName, simpleClassName);
+ TypeMirror enclosingType = t.getEnclosingType();
+ TypeName enclosing =
+ (enclosingType.getKind() != NONE)
+ && !t.asElement().getModifiers().contains(Modifier.STATIC)
+ ? enclosingType.accept(this, null)
+ : null;
+ if (t.getTypeArguments().isEmpty() && !(enclosing instanceof TypeNameGeneric)) {
+ return wrapInCollectionTypeIfApplicable(
+ rawType, List.of(), typeOfParameter, context);
+ }
+
+ List typesExtracted = new ArrayList<>(t.getTypeArguments());
+ if (typesExtracted.isEmpty()) {
+ return wrapInCollectionTypeIfApplicable(
+ rawType, List.of(), typeOfParameter, context);
+ } else {
+ List argTypes = extractTypeForList(typesExtracted, context);
+ return wrapInCollectionTypeIfApplicable(
+ rawType, argTypes, typeOfParameter, context);
+ }
+ }
+
+ @Override
+ public TypeNameArray visitArray(ArrayType t, Void p) {
+ return new TypeNameArray(extractType(t.getComponentType(), context));
+ }
+
+ @Override
+ public TypeName visitTypeVariable(TypeVariable t, Void p) {
+ String name = t.asElement().getSimpleName().toString();
+ return new TypeNameVariable(name);
+ }
+
+ @Override
+ protected TypeName defaultAction(TypeMirror e, Void p) {
+ throw new IllegalArgumentException("Unexpected type mirror: " + e);
+ }
+ },
+ null);
+
+ if (typeName != null && context.getConfiguration().shouldCopyTypeAnnotations()) {
+ List annotations =
+ FieldAnnotationExtractor.extractAnnotations(typeOfParameter, context);
+ annotations.forEach(typeName::addAnnotation);
+ }
+
+ return typeName;
}
}
diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavapoetMapper.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavapoetMapper.java
index bdc986bd..f1010e96 100644
--- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavapoetMapper.java
+++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavapoetMapper.java
@@ -72,41 +72,47 @@ public static TypeName[] map2TypeArgumentsArray(
*/
public static TypeName map2ParameterType(
org.javahelpers.simple.builders.processor.dtos.TypeName parameterType) {
- // Handle generic type variables (e.g., T, K, V)
+ TypeName typeName;
if (parameterType
instanceof org.javahelpers.simple.builders.processor.dtos.TypeNameVariable typeVariable) {
- return TypeVariableName.get(typeVariable.getClassName());
- }
- // Handle primitives BEFORE calling ClassName.get() to avoid IllegalArgumentException in
- // JavaPoet 0.9.0+
- if (parameterType instanceof TypeNamePrimitive parameterTypePrim) {
- return switch (parameterTypePrim.getType()) {
- case BOOLEAN -> TypeName.BOOLEAN;
- case BYTE -> TypeName.BYTE;
- case CHAR -> TypeName.CHAR;
- case DOUBLE -> TypeName.DOUBLE;
- case FLOAT -> TypeName.FLOAT;
- case INT -> TypeName.INT;
- case LONG -> TypeName.LONG;
- case SHORT -> TypeName.SHORT;
- default -> null;
- };
+ typeName = TypeVariableName.get(typeVariable.getClassName());
+ } else if (parameterType instanceof TypeNamePrimitive parameterTypePrim) {
+ typeName = mapPrimitive(parameterTypePrim);
+ } else if (parameterType instanceof TypeNameArray parameterTypeArray) {
+ typeName = ArrayTypeName.of(map2ParameterType(parameterTypeArray.getTypeOfArray()));
+ } else if (parameterType instanceof TypeNameGeneric parameterTypeGeneric) {
+ typeName = mapGeneric(parameterTypeGeneric);
+ } else {
+ typeName = ClassName.get(parameterType.getPackageName(), parameterType.getClassName());
}
- if (parameterType instanceof TypeNameArray parameterTypeArray) {
- return ArrayTypeName.of(map2ParameterType(parameterTypeArray.getTypeOfArray()));
+
+ if (typeName != null && !parameterType.getAnnotations().isEmpty()) {
+ typeName = typeName.annotated(map2AnnotationSpecs(parameterType.getAnnotations()));
}
- // Now safe to call ClassName.get() for non-primitive types
- ClassName classNameParameter =
- ClassName.get(parameterType.getPackageName(), parameterType.getClassName());
- if (parameterType instanceof TypeNameGeneric param) {
- // Handle raw types (e.g., List without ) - return just the class name
- if (param.getInnerTypeArguments().isEmpty()) {
- return classNameParameter;
- }
- TypeName[] typeArgs = map2TypeArgumentsArray(param.getInnerTypeArguments());
- return ParameterizedTypeName.get(classNameParameter, typeArgs);
+ return typeName;
+ }
+
+ private static TypeName mapPrimitive(TypeNamePrimitive parameterTypePrim) {
+ return switch (parameterTypePrim.getType()) {
+ case BOOLEAN -> TypeName.BOOLEAN;
+ case BYTE -> TypeName.BYTE;
+ case CHAR -> TypeName.CHAR;
+ case DOUBLE -> TypeName.DOUBLE;
+ case FLOAT -> TypeName.FLOAT;
+ case INT -> TypeName.INT;
+ case LONG -> TypeName.LONG;
+ case SHORT -> TypeName.SHORT;
+ default -> null;
+ };
+ }
+
+ private static TypeName mapGeneric(TypeNameGeneric param) {
+ ClassName classNameParameter = ClassName.get(param.getPackageName(), param.getClassName());
+ if (param.getInnerTypeArguments().isEmpty()) {
+ return classNameParameter;
}
- return classNameParameter;
+ TypeName[] typeArgs = map2TypeArgumentsArray(param.getInnerTypeArguments());
+ return ParameterizedTypeName.get(classNameParameter, typeArgs);
}
/**
diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/CompilerArgumentsReaderTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/CompilerArgumentsReaderTest.java
index 0edef695..b0d68f97 100644
--- a/processor/src/test/java/org/javahelpers/simple/builders/processor/CompilerArgumentsReaderTest.java
+++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/CompilerArgumentsReaderTest.java
@@ -350,6 +350,7 @@ void readBuilderConfiguration_AllOptionsSet_ReadsCorrectly() {
.put("simplebuilder.builderConstructorAccess", "private")
.put("simplebuilder.methodAccess", "package-private")
.put("simplebuilder.generateVarArgsHelpers", "disabled")
+ .put("simplebuilder.copyTypeAnnotations", "enabled")
.put("simplebuilder.builderSuffix", "Factory")
.put("simplebuilder.setterSuffix", "with")
.build();
@@ -364,6 +365,7 @@ void readBuilderConfiguration_AllOptionsSet_ReadsCorrectly() {
assertEquals(AccessModifier.PRIVATE, config.getBuilderConstructorAccess());
assertEquals(AccessModifier.PACKAGE_PRIVATE, config.getMethodAccess());
assertEquals(OptionState.DISABLED, config.generateVarArgsHelpers());
+ assertEquals(OptionState.ENABLED, config.copyTypeAnnotations());
assertEquals("Factory", config.getBuilderSuffix());
assertEquals("with", config.getSetterSuffix());
}
diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java
index 1c284424..6ddaf5fd 100644
--- a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java
+++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java
@@ -66,6 +66,7 @@ void allConfigurationOptions_MustBeSettableViaBuilder() {
.generateVarArgsHelpers(OptionState.ENABLED)
.generateStringFormatHelpers(OptionState.ENABLED)
.generateUnboxedOptional(OptionState.ENABLED)
+ .copyTypeAnnotations(OptionState.ENABLED)
// Collection builder options
.usingArrayListBuilder(OptionState.ENABLED)
.usingArrayListBuilderWithElementBuilders(OptionState.ENABLED)
@@ -95,6 +96,7 @@ void allConfigurationOptions_MustBeSettableViaBuilder() {
assertEquals(OptionState.ENABLED, config.generateVarArgsHelpers());
assertEquals(OptionState.ENABLED, config.generateStringFormatHelpers());
assertEquals(OptionState.ENABLED, config.generateUnboxedOptional());
+ assertEquals(OptionState.ENABLED, config.copyTypeAnnotations());
assertEquals(OptionState.ENABLED, config.usingArrayListBuilder());
assertEquals(OptionState.ENABLED, config.usingArrayListBuilderWithElementBuilders());
assertEquals(OptionState.ENABLED, config.usingHashSetBuilder());
@@ -198,6 +200,7 @@ public Address() {}
"-Asimplebuilder.generateVarArgsHelpers=false",
"-Asimplebuilder.generateStringFormatHelpers=false",
"-Asimplebuilder.generateUnboxedOptional=false",
+ "-Asimplebuilder.copyTypeAnnotations=false",
"-Asimplebuilder.usingArrayListBuilder=false",
"-Asimplebuilder.usingArrayListBuilderWithElementBuilders=false",
"-Asimplebuilder.usingHashSetBuilder=false",
diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/TypeUseAnnotationTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/TypeUseAnnotationTest.java
new file mode 100644
index 00000000..ff052cf8
--- /dev/null
+++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/TypeUseAnnotationTest.java
@@ -0,0 +1,417 @@
+package org.javahelpers.simple.builders.processor;
+
+import static org.javahelpers.simple.builders.processor.testing.ProcessorAsserts.contains;
+import static org.javahelpers.simple.builders.processor.testing.ProcessorAsserts.notContains;
+import static org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils.createCompiler;
+import static org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils.createMockAnnotation;
+import static org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils.loadGeneratedSource;
+import static org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils.printDiagnosticsOnVerbose;
+
+import com.google.testing.compile.Compilation;
+import com.google.testing.compile.JavaFileObjects;
+import javax.tools.JavaFileObject;
+import org.javahelpers.simple.builders.processor.testing.ProcessorAsserts;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test that TYPE_USE annotations are properly handled in generated builders.
+ *
+ * TYPE_USE annotations apply to type usage, not declarations:
+ *
+ *
+ * List<@NotNull String> items;
+ *
+ *
+ * These annotations should be preserved on:
+ *
+ *
+ * - Builder field member variables (TrackedValue fields)
+ *
- Method parameters in setter methods
+ *
- Method parameters in helper methods (add2FieldName, etc.)
+ *
- Return types where applicable
+ *
+ *
+ * @see Issue #92
+ */
+class TypeUseAnnotationTest {
+
+ private Compilation compileSources(JavaFileObject... sources) {
+ Compilation compilation = createCompiler().compile(sources);
+ printDiagnosticsOnVerbose(compilation);
+ return compilation;
+ }
+
+ @Test
+ void typeUseAnnotations_onListElements_copiedToBuilderMethods() {
+ String packageName = "test.typeuse.list";
+
+ JavaFileObject notNullAnnotation =
+ createMockAnnotation(packageName + ".annotations", "NotNull", "ElementType.TYPE_USE");
+
+ JavaFileObject person =
+ JavaFileObjects.forSourceString(
+ packageName + ".Person",
+ """
+ package test.typeuse.list;
+ import org.javahelpers.simple.builders.core.annotations.SimpleBuilder;
+ import test.typeuse.list.annotations.NotNull;
+ import java.util.List;
+
+ @SimpleBuilder
+ public class Person {
+ private final List<@NotNull String> nicknames;
+
+ public Person(List<@NotNull String> nicknames) {
+ this.nicknames = nicknames;
+ }
+
+ public List<@NotNull String> getNicknames() { return nicknames; }
+ }
+ """);
+
+ Compilation compilation = compileSources(notNullAnnotation, person);
+ String generatedCode = loadGeneratedSource(compilation, "PersonBuilder");
+ ProcessorAsserts.assertGenerationSucceeded(compilation, "PersonBuilder", generatedCode);
+
+ // Verify that TYPE_USE annotations are preserved in builder field
+ ProcessorAsserts.assertingResult(
+ generatedCode,
+ // Builder field should have TYPE_USE annotation
+ contains("TrackedValue> nicknames"),
+ // Setter method parameter should have TYPE_USE annotation
+ contains("nicknames(List<@NotNull String> nicknames)"),
+ // VarArgs method parameter should have TYPE_USE annotation
+ contains("nicknames(@NotNull String... nicknames)"),
+ // add2Nicknames method parameter should have TYPE_USE annotation
+ contains("add2Nicknames(@NotNull String element)"));
+ }
+
+ @Test
+ void typeUseAnnotations_onSetElements_copiedToBuilderMethods() {
+ String packageName = "test.typeuse.set";
+
+ JavaFileObject notNullAnnotation =
+ createMockAnnotation(packageName + ".annotations", "NotNull", "ElementType.TYPE_USE");
+
+ JavaFileObject product =
+ JavaFileObjects.forSourceString(
+ packageName + ".Product",
+ """
+ package test.typeuse.set;
+ import org.javahelpers.simple.builders.core.annotations.SimpleBuilder;
+ import test.typeuse.set.annotations.NotNull;
+ import java.util.Set;
+
+ @SimpleBuilder
+ public class Product {
+ private final Set<@NotNull String> tags;
+
+ public Product(Set<@NotNull String> tags) {
+ this.tags = tags;
+ }
+
+ public Set<@NotNull String> getTags() { return tags; }
+ }
+ """);
+
+ Compilation compilation = compileSources(notNullAnnotation, product);
+ String generatedCode = loadGeneratedSource(compilation, "ProductBuilder");
+ ProcessorAsserts.assertGenerationSucceeded(compilation, "ProductBuilder", generatedCode);
+
+ // Verify that TYPE_USE annotations are preserved
+ ProcessorAsserts.assertingResult(
+ generatedCode,
+ // Builder field should have TYPE_USE annotation
+ contains("TrackedValue> tags"),
+ // Setter method parameter should have TYPE_USE annotation
+ contains("tags(Set<@NotNull String> tags)"),
+ // VarArgs method parameter should have TYPE_USE annotation
+ contains("tags(@NotNull String... tags)"),
+ // add2Tags method parameter should have TYPE_USE annotation
+ contains("add2Tags(@NotNull String element)"));
+ }
+
+ @Test
+ void typeUseAnnotations_onMapValues_copiedToBuilderMethods() {
+ String packageName = "test.typeuse.map";
+
+ JavaFileObject notNullAnnotation =
+ createMockAnnotation(packageName + ".annotations", "NotNull", "ElementType.TYPE_USE");
+
+ JavaFileObject config =
+ JavaFileObjects.forSourceString(
+ packageName + ".Config",
+ """
+ package test.typeuse.map;
+ import org.javahelpers.simple.builders.core.annotations.SimpleBuilder;
+ import test.typeuse.map.annotations.NotNull;
+ import java.util.Map;
+
+ @SimpleBuilder
+ public class Config {
+ private final Map properties;
+
+ public Config(Map properties) {
+ this.properties = properties;
+ }
+
+ public Map getProperties() { return properties; }
+ }
+ """);
+
+ Compilation compilation = compileSources(notNullAnnotation, config);
+ String generatedCode = loadGeneratedSource(compilation, "ConfigBuilder");
+ ProcessorAsserts.assertGenerationSucceeded(compilation, "ConfigBuilder", generatedCode);
+
+ // Verify that TYPE_USE annotations are preserved
+ ProcessorAsserts.assertingResult(
+ generatedCode,
+ // Builder field should have TYPE_USE annotation
+ contains("TrackedValue