diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/TypeNameArray.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/TypeNameArray.java index 24a9bf2c..6fa34811 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/TypeNameArray.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/TypeNameArray.java @@ -29,31 +29,26 @@ /** TypeName is a specific array type. Holding name of class and package of inner type. */ public class TypeNameArray extends TypeName { private final TypeName typeOfArray; - private final boolean fillsSet; /** * Constructor for simple classes. * * @param packageName name of package * @param className name of class, could not be null - * @param isSet {@code true} if this array represents a set */ - public TypeNameArray(String packageName, String className, boolean isSet) { + public TypeNameArray(String packageName, String className) { super(packageName, className); this.typeOfArray = new TypeName(packageName, className); - this.fillsSet = isSet; } /** * Constructor with innerType. * * @param innerType name of package - * @param isSet {@code true} if this array represents a set */ - public TypeNameArray(TypeName innerType, boolean isSet) { + public TypeNameArray(TypeName innerType) { super(innerType.getPackageName(), innerType.getClassName()); this.typeOfArray = innerType; - this.fillsSet = isSet; } /** @@ -76,13 +71,4 @@ public Optional getInnerType() { public TypeName getTypeOfArray() { return typeOfArray; } - - /** - * Getter for fillsSet. - * - * @return {@code true}, if this is array that should fill a set - */ - public boolean isFillingSet() { - return fillsSet; - } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/TypeNameGeneric.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/TypeNameGeneric.java index a7f17ad1..059b9b27 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/TypeNameGeneric.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/TypeNameGeneric.java @@ -23,7 +23,6 @@ */ package org.javahelpers.simple.builders.processor.dtos; -import java.util.Collections; import java.util.List; import java.util.Optional; @@ -99,7 +98,7 @@ public TypeNameGeneric(String packageName, String className, TypeName... innerTy * @return an unmodifiable list of inner type arguments */ public List getInnerTypeArguments() { - return Collections.unmodifiableList(innerTypeArguments); + return innerTypeArguments; } /** diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/TypeNameList.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/TypeNameList.java new file mode 100644 index 00000000..0b6a9065 --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/TypeNameList.java @@ -0,0 +1,120 @@ +/* + * MIT License + * + * Copyright (c) 2025 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.javahelpers.simple.builders.processor.dtos; + +import java.util.List; +import org.apache.commons.lang3.Strings; + +/** + * Represents a type that implements the {@code java.util.List} interface. + * + *

This includes the List interface itself as well as any concrete implementations like + * ArrayList, LinkedList, Vector, Stack, or custom List implementations. + * + *

The concrete class name (e.g., "ArrayList") is preserved in the package and class name fields, + * while this subclass indicates that the type implements the List interface. + * + *

Examples: + * + *

+ */ +public class TypeNameList extends TypeNameGeneric { + + private final boolean isConcreteImplementation; + private final TypeName elementType; + + /** + * Checks if the given package and class name represent the {@code java.util.List} interface. + * + * @param packageName the package name + * @param className the class name + * @return {@code true} if this is {@code java.util.List} + */ + private static boolean isListInterface(String packageName, String className) { + return Strings.CI.equals(packageName, "java.util") && Strings.CI.equals(className, "List"); + } + + /** + * Creates a {@code TypeNameList} based on another {@code TypeName} as outer type and a list of + * inner type arguments. + * + * @param outerType the outer type to use for package and class name (the concrete List + * implementation) + * @param innerTypeArguments the list of generic type arguments (all class type parameters) + * @param elementType the actual List element type (extracted from List interface) + */ + public TypeNameList(TypeName outerType, List innerTypeArguments, TypeName elementType) { + super(outerType, innerTypeArguments); + this.isConcreteImplementation = !isListInterface(getPackageName(), getClassName()); + this.elementType = elementType; + } + + /** + * Checks if this is a concrete List implementation (not the interface itself). + * + * @return {@code true} for concrete implementations like ArrayList, LinkedList, etc., {@code + * false} if this is {@code java.util.List} + */ + public boolean isConcreteImplementation() { + return isConcreteImplementation; + } + + /** + * Checks if this List type is properly parameterized (not a raw type). + * + *

A parameterized List has an element type extracted from the List interface. A raw List has + * no element type. + * + *

This works correctly for both standard Lists like {@code List} and custom + * implementations like {@code CustomList extends ArrayList}. + * + * @return {@code true} if this List has an element type + */ + public boolean isParameterized() { + return elementType != null; + } + + /** + * Gets the element type of this List. + * + *

For a parameterized List like {@code List}, this returns the String type. + * + *

For custom implementations like {@code CustomList extends ArrayList}, this returns Y + * (the actual List element type), not X or both X and Y. + * + * @return the element type (extracted from the List interface) + * @throws IllegalStateException if this is a raw List with no type arguments + */ + public TypeName getElementType() { + if (elementType == null) { + throw new IllegalStateException( + "Cannot get element type from raw List type: " + getClassName()); + } + return elementType; + } +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/TypeNameMap.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/TypeNameMap.java new file mode 100644 index 00000000..c30b0a06 --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/TypeNameMap.java @@ -0,0 +1,132 @@ +/* + * MIT License + * + * Copyright (c) 2025 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.javahelpers.simple.builders.processor.dtos; + +import java.util.List; +import org.apache.commons.lang3.Strings; + +/** + * Represents a type that implements the {@code java.util.Map} interface. + * + *

This includes the Map interface itself as well as any concrete implementations like HashMap, + * LinkedHashMap, TreeMap, Hashtable, or custom Map implementations. + * + *

The concrete class name (e.g., "HashMap") is preserved in the package and class name fields, + * while this subclass indicates that the type implements the Map interface. + * + *

Examples: + * + *

    + *
  • {@code Map} -> TypeNameMap with 2 inner type arguments + *
  • {@code HashMap} -> TypeNameMap with 2 inner type arguments + *
  • {@code Map} (raw type) -> TypeNameMap with 0 inner type arguments + *
+ */ +public class TypeNameMap extends TypeNameGeneric { + + private final boolean isConcreteImplementation; + private final TypeName keyType; + private final TypeName valueType; + + /** + * Checks if the given package and class name represent the {@code java.util.Map} interface. + * + * @param packageName the package name + * @param className the class name + * @return {@code true} if this is {@code java.util.Map} + */ + private static boolean isMapInterface(String packageName, String className) { + return Strings.CI.equals(packageName, "java.util") && Strings.CI.equals(className, "Map"); + } + + /** + * Creates a {@code TypeNameMap} based on another {@code TypeName} as outer type and a list of + * inner type arguments. + * + * @param outerType the outer type to use for package and class name (the concrete Map + * implementation) + * @param innerTypeArguments the list of generic type arguments (all class type parameters) + * @param keyType the actual Map key type (extracted from Map interface) + * @param valueType the actual Map value type (extracted from Map interface) + */ + public TypeNameMap( + TypeName outerType, List innerTypeArguments, TypeName keyType, TypeName valueType) { + super(outerType, innerTypeArguments); + this.isConcreteImplementation = !isMapInterface(getPackageName(), getClassName()); + this.keyType = keyType; + this.valueType = valueType; + } + + /** + * Checks if this is a concrete Map implementation (not the interface itself). + * + * @return {@code true} for concrete implementations like HashMap, TreeMap, etc., {@code false} if + * this is {@code java.util.Map} + */ + public boolean isConcreteImplementation() { + return isConcreteImplementation; + } + + /** + * Checks if this Map type is properly parameterized (not a raw type). + * + *

A parameterized Map has exactly 2 type arguments (key and value types). A raw Map has 0 type + * arguments. + * + * @return {@code true} if this Map has exactly 2 type arguments + */ + public boolean isParameterized() { + return keyType != null && valueType != null; + } + + /** + * Gets the key type of this Map. + * + *

For a parameterized Map like {@code Map}, this returns the String type. + * + * @return the key type (first type argument) + * @throws IllegalStateException if this is a raw Map with no type arguments + */ + public TypeName getKeyType() { + if (keyType == null) { + throw new IllegalStateException("Cannot get key type from raw Map type: " + getClassName()); + } + return keyType; + } + + /** + * Gets the value type of this Map. + * + *

For a parameterized Map like {@code Map}, this returns the Integer type. + * + * @return the value type (second type argument) + * @throws IllegalStateException if this is a raw Map with no type arguments + */ + public TypeName getValueType() { + if (valueType == null) { + throw new IllegalStateException("Cannot get value type from raw Map type: " + getClassName()); + } + return valueType; + } +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/TypeNameSet.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/TypeNameSet.java new file mode 100644 index 00000000..5bb1add5 --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/TypeNameSet.java @@ -0,0 +1,114 @@ +/* + * MIT License + * + * Copyright (c) 2025 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.javahelpers.simple.builders.processor.dtos; + +import java.util.List; +import org.apache.commons.lang3.Strings; + +/** + * Represents a type that implements the {@code java.util.Set} interface. + * + *

This includes the Set interface itself as well as any concrete implementations like HashSet, + * LinkedHashSet, TreeSet, or custom Set implementations. + * + *

The concrete class name (e.g., "HashSet") is preserved in the package and class name fields, + * while this subclass indicates that the type implements the Set interface. + * + *

Examples: + * + *

    + *
  • {@code Set} -> TypeNameSet with 1 inner type argument + *
  • {@code HashSet} -> TypeNameSet with 1 inner type argument + *
  • {@code Set} (raw type) -> TypeNameSet with 0 inner type arguments + *
+ */ +public class TypeNameSet extends TypeNameGeneric { + + private final boolean isConcreteImplementation; + private final TypeName elementType; + + /** + * Checks if the given package and class name represent the {@code java.util.Set} interface. + * + * @param packageName the package name + * @param className the class name + * @return {@code true} if this is {@code java.util.Set} + */ + private static boolean isSetInterface(String packageName, String className) { + return Strings.CI.equals(packageName, "java.util") && Strings.CI.equals(className, "Set"); + } + + /** + * Creates a {@code TypeNameSet} based on another {@code TypeName} as outer type and a list of + * inner type arguments. + * + * @param outerType the outer type to use for package and class name (the concrete Set + * implementation) + * @param innerTypeArguments the list of generic type arguments (all class type parameters) + * @param elementType the actual Set element type (extracted from Set interface) + */ + public TypeNameSet(TypeName outerType, List innerTypeArguments, TypeName elementType) { + super(outerType, innerTypeArguments); + this.isConcreteImplementation = !isSetInterface(getPackageName(), getClassName()); + this.elementType = elementType; + } + + /** + * Checks if this is a concrete Set implementation (not the interface itself). + * + * @return {@code true} for concrete implementations like HashSet, TreeSet, etc., {@code false} if + * this is {@code java.util.Set} + */ + public boolean isConcreteImplementation() { + return isConcreteImplementation; + } + + /** + * Checks if this Set type is properly parameterized (not a raw type). + * + *

A parameterized Set has exactly 1 type argument (the element type). A raw Set has 0 type + * arguments. + * + * @return {@code true} if this Set has exactly 1 type argument + */ + public boolean isParameterized() { + return elementType != null; + } + + /** + * Gets the element type of this Set. + * + *

For a parameterized Set like {@code Set}, this returns the String type. + * + * @return the element type (the single type argument) + * @throws IllegalStateException if this is a raw Set with no type arguments + */ + public TypeName getElementType() { + if (elementType == null) { + throw new IllegalStateException( + "Cannot get element type from raw Set type: " + getClassName()); + } + return elementType; + } +} 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 4172ff36..e6c0e4a6 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 @@ -328,57 +328,34 @@ private static void addAdditionalHelperMethodsForField( } List innerTypes = fieldTypeGeneric.getInnerTypeArguments(); - int innerTypesCnt = innerTypes.size(); - if (isList(field.getFieldType()) && innerTypesCnt == 1) { + if (field.getFieldType() instanceof TypeNameList listType && listType.isParameterized()) { // Only add varargs helper if enabled in configuration if (context.getConfiguration().shouldGenerateVarArgsHelpers()) { - String fieldName = field.getFieldNameEstimated(); MethodDto method = - createFieldSetterWithTransform( - fieldName, - fieldNameInBuilder, - fieldJavaDoc, - "List.of(%s)", - new TypeNameArray(innerTypes.get(0), false), - builderType, - context); + createFieldSetterByVarArgs( + field, new TypeNameArray(listType.getElementType()), builderType, context); field.addMethod(method); } - } else if (isSet(field.getFieldType()) && innerTypesCnt == 1) { + } else if (field.getFieldType() instanceof TypeNameSet setType && setType.isParameterized()) { // Only add varargs helper if enabled in configuration if (context.getConfiguration().shouldGenerateVarArgsHelpers()) { - String fieldName = field.getFieldNameEstimated(); MethodDto method = - createFieldSetterWithTransform( - fieldName, - fieldNameInBuilder, - fieldJavaDoc, - "Set.of(%s)", - new TypeNameArray(innerTypes.get(0), true), - builderType, - context); + createFieldSetterByVarArgs( + field, new TypeNameArray(setType.getElementType()), builderType, context); field.addMethod(method); } - } else if (isMap(field.getFieldType()) && innerTypesCnt == 2) { + } else if (field.getFieldType() instanceof TypeNameMap mapType && mapType.isParameterized()) { // Only add varargs helper if enabled in configuration if (context.getConfiguration().shouldGenerateVarArgsHelpers()) { + // Use fully qualified name for Map.Entry to ensure proper import TypeName mapEntryType = new TypeNameArray( - new TypeNameGeneric("java.util", "Map.Entry", innerTypes.get(0), innerTypes.get(1)), - false); - String fieldName = field.getFieldNameEstimated(); - MethodDto method = - createFieldSetterWithTransform( - fieldName, - fieldNameInBuilder, - fieldJavaDoc, - "Map.ofEntries(%s)", - mapEntryType, - builderType, - context); + new TypeNameGeneric( + "java.util.Map", "Entry", mapType.getKeyType(), mapType.getValueType())); + MethodDto method = createFieldSetterByVarArgs(field, mapEntryType, builderType, context); field.addMethod(method); } - } else if (isOptional(field.getFieldType()) && innerTypesCnt == 1) { + } else if (isParameterizedOptional(field.getFieldType())) { String fieldName = field.getFieldNameEstimated(); // Only generate unboxed optional method if enabled in configuration @@ -452,12 +429,7 @@ private static boolean tryAddBuilderConsumer( TypeName fieldBuilderType = fieldBuilderOpt.get(); MethodDto method = BuilderDefinitionCreator.createFieldConsumerWithBuilder( - field.getFieldName(), - field.getFieldName(), - field.getJavaDoc(), - fieldBuilderType, - builderType, - context); + field, fieldBuilderType, builderType, context); field.addMethod(method); return true; } @@ -526,13 +498,12 @@ private static boolean tryAddListConsumer( VariableElement fieldParameter, TypeName builderType, ProcessingContext context) { - if (!(isList(field.getFieldType()) - && field.getFieldType() instanceof TypeNameGeneric fieldTypeGeneric - && fieldTypeGeneric.getInnerTypeArguments().size() == 1)) { + if (!(field.getFieldType() instanceof TypeNameList fieldTypeGeneric + && fieldTypeGeneric.isParameterized())) { return false; } - TypeName elementType = fieldTypeGeneric.getInnerTypeArguments().get(0); + TypeName elementType = fieldTypeGeneric.getElementType(); // Get the TypeMirror of the element type from the parameter's type TypeMirror fieldTypeMirror = fieldParameter.asType(); @@ -556,26 +527,14 @@ private static boolean tryAddListConsumer( elementBuilderType.get()); MethodDto method = createFieldConsumerWithElementBuilders( - field.getFieldName(), - field.getFieldName(), - field.getJavaDoc(), - collectionBuilderType, - elementBuilderType.get(), - builderType, - context); + field, collectionBuilderType, elementBuilderType.get(), builderType, context); field.addMethod(method); } else if (context.getConfiguration().shouldUseArrayListBuilder()) { // Regular ArrayListBuilder if enabled TypeName collectionBuilderType = map2TypeName(ArrayListBuilder.class); MethodDto method = createFieldConsumerWithBuilder( - field.getFieldName(), - field.getFieldName(), - field.getJavaDoc(), - collectionBuilderType, - elementType, - builderType, - context); + field, collectionBuilderType, elementType, builderType, context); field.addMethod(method); } else { return false; @@ -594,25 +553,18 @@ private static boolean tryAddMapConsumer( if (!context.getConfiguration().shouldUseHashMapBuilder()) { return false; } - if (!(isMap(field.getFieldType()) - && field.getFieldType() instanceof TypeNameGeneric fieldTypeGeneric - && fieldTypeGeneric.getInnerTypeArguments().size() == 2)) { + if (!(field.getFieldType() instanceof TypeNameMap fieldTypeGeneric + && fieldTypeGeneric.isParameterized())) { return false; } TypeNameGeneric builderTargetTypeName = new TypeNameGeneric( map2TypeName(HashMapBuilder.class), - fieldTypeGeneric.getInnerTypeArguments().get(0), - fieldTypeGeneric.getInnerTypeArguments().get(1)); + fieldTypeGeneric.getKeyType(), + fieldTypeGeneric.getValueType()); MethodDto mapConsumerWithBuilder = - BuilderDefinitionCreator.createFieldConsumerWithBuilder( - field.getFieldName(), - field.getFieldName(), - field.getJavaDoc(), - builderTargetTypeName, - builderType, - context); + createFieldConsumerWithBuilder(field, builderTargetTypeName, builderType, context); field.addMethod(mapConsumerWithBuilder); return true; } @@ -623,13 +575,12 @@ private static boolean tryAddSetConsumer( VariableElement fieldParameter, TypeName builderType, ProcessingContext context) { - if (!(isSet(field.getFieldType()) - && field.getFieldType() instanceof TypeNameGeneric fieldTypeGeneric - && fieldTypeGeneric.getInnerTypeArguments().size() == 1)) { + if (!(field.getFieldType() instanceof TypeNameSet fieldTypeGeneric + && fieldTypeGeneric.isParameterized())) { return false; } - TypeName elementType = fieldTypeGeneric.getInnerTypeArguments().get(0); + TypeName elementType = fieldTypeGeneric.getElementType(); // Get the TypeMirror of the element type from the parameter's type TypeMirror fieldTypeMirror = fieldParameter.asType(); @@ -653,26 +604,14 @@ private static boolean tryAddSetConsumer( elementBuilderType.get()); MethodDto method = createFieldConsumerWithElementBuilders( - field.getFieldName(), - field.getFieldName(), - field.getJavaDoc(), - collectionBuilderType, - elementBuilderType.get(), - builderType, - context); + field, collectionBuilderType, elementBuilderType.get(), builderType, context); field.addMethod(method); } else if (context.getConfiguration().shouldUseHashSetBuilder()) { // Regular HashSetBuilder if enabled TypeName collectionBuilderType = map2TypeName(HashSetBuilder.class); MethodDto method = createFieldConsumerWithBuilder( - field.getFieldName(), - field.getFieldName(), - field.getJavaDoc(), - collectionBuilderType, - elementType, - builderType, - context); + field, collectionBuilderType, elementType, builderType, context); field.addMethod(method); } else { return false; @@ -944,6 +883,91 @@ private static MethodDto createFieldSetterWithTransform( context); } + /** + * Creates a field setter method for collection varargs with automatic transform calculation. The + * transform is calculated based on the original field type to preserve specific collection + * implementations (e.g., ArrayList, LinkedList, HashSet, TreeSet, HashMap, TreeMap). + * + * @param field the field definition containing name, type, and javadoc + * @param parameterType the type of the method parameter (varargs array type) + * @param builderType the builder type for the return type + * @param context processing context + * @return the method DTO for the setter + */ + private static MethodDto createFieldSetterByVarArgs( + FieldDto field, TypeName parameterType, TypeName builderType, ProcessingContext context) { + String baseExpression; + TypeName fieldType = field.getFieldType(); + + // Use simple names for interface types (already imported), fully qualified for concrete types + if (fieldType instanceof TypeNameList listType) { + baseExpression = + listType.isConcreteImplementation() ? "java.util.List.of(%s)" : "List.of(%s)"; + } else if (fieldType instanceof TypeNameSet setType) { + baseExpression = setType.isConcreteImplementation() ? "java.util.Set.of(%s)" : "Set.of(%s)"; + } else if (fieldType instanceof TypeNameMap mapType) { + baseExpression = + mapType.isConcreteImplementation() ? "java.util.Map.ofEntries(%s)" : "Map.ofEntries(%s)"; + } else { + return null; + } + String transform = wrapConcreteCollectionType(fieldType, baseExpression); + + return createFieldSetterWithTransform( + field.getFieldNameEstimated(), + field.getFieldName(), + field.getJavaDoc(), + transform, + parameterType, + List.of(), + builderType, + context); + } + + /** + * Wraps an expression with a concrete collection constructor if needed to preserve the specific + * collection type. Only wraps concrete implementations (ArrayList, LinkedList, HashSet, TreeSet, + * HashMap, TreeMap, etc.). Returns the base expression unchanged for interface types (List, Set, + * Map), non-collection types. + * + *

Examples: + * + *

    + *
  • ArrayList + "List.of(%s)" → "new ArrayList<>(List.of(%s))" + *
  • List + "List.of(%s)" → "List.of(%s)" + *
  • ArrayList + "builder.build()" → "new ArrayList<>(builder.build())" + *
  • List + "builder.build()" → "builder.build()" + *
  • String + "value" → "value" + *
+ * + * @param fieldType the field type to check + * @param baseExpression the base expression to potentially wrap + * @return the wrapped expression for concrete collections, or base expression otherwise + */ + private static String wrapConcreteCollectionType(TypeName fieldType, String baseExpression) { + // TypeNameList/Set/Map are only created for types we can work with, so no additional checks + // needed + if (fieldType instanceof TypeNameList listType && listType.isConcreteImplementation()) { + return "new " + listType.getClassName() + "<>(" + baseExpression + ")"; + } else if (fieldType instanceof TypeNameSet setType && setType.isConcreteImplementation()) { + return "new " + setType.getClassName() + "<>(" + baseExpression + ")"; + } else if (fieldType instanceof TypeNameMap mapType && mapType.isConcreteImplementation()) { + return "new " + mapType.getClassName() + "<>(" + baseExpression + ")"; + } + + return baseExpression; + } + + /** + * Calculates the build expression wrapper for builder consumers. + * + * @param fieldType the original field type + * @return the wrapped expression or the base expression if no wrapping needed + */ + private static String calculateBuildExpression(TypeName fieldType) { + return wrapConcreteCollectionType(fieldType, "builder.build()"); + } + /** * Creates a field setter method with optional transform and annotations. * @@ -1082,9 +1106,7 @@ private static MethodDto createStringBuilderConsumer( } private static MethodDto createFieldConsumerWithBuilder( - String fieldName, - String fieldNameInBuilder, - String fieldJavadoc, + FieldDto field, TypeName consumerBuilderType, TypeName builderTargetType, TypeName returnBuilderType, @@ -1092,25 +1114,16 @@ private static MethodDto createFieldConsumerWithBuilder( TypeNameGeneric builderTypeGeneric = new TypeNameGeneric(consumerBuilderType, builderTargetType); return BuilderDefinitionCreator.createFieldConsumerWithBuilder( - fieldName, - fieldNameInBuilder, - fieldJavadoc, - builderTypeGeneric, - returnBuilderType, - context); + field, builderTypeGeneric, returnBuilderType, context); } private static MethodDto createFieldConsumerWithBuilder( - String fieldName, - String fieldNameInBuilder, - String fieldJavaDoc, + FieldDto field, TypeName consumerBuilderType, TypeName returnBuilderType, ProcessingContext context) { return createFieldConsumerWithBuilder( - fieldName, - fieldNameInBuilder, - fieldJavaDoc, + field, consumerBuilderType, "this.$fieldName:N.value()", "", @@ -1124,17 +1137,13 @@ private static MethodDto createFieldConsumerWithBuilder( * ArrayListBuilderWithElementBuilders and HashSetBuilderWithElementBuilders. */ private static MethodDto createFieldConsumerWithElementBuilders( - String fieldName, - String fieldNameInBuilder, - String fieldJavaDoc, + FieldDto field, TypeName collectionBuilderType, TypeName elementBuilderType, TypeName returnBuilderType, ProcessingContext context) { return createFieldConsumerWithBuilder( - fieldName, - fieldNameInBuilder, - fieldJavaDoc, + field, collectionBuilderType, "this.$fieldName:N.value(), $elementBuilderType:T::create", "$elementBuilderType:T::create", @@ -1146,19 +1155,18 @@ private static MethodDto createFieldConsumerWithElementBuilders( /** * Creates a consumer method for a field with a builder type. * - * @param fieldName the field name - * @param builderType the builder type (e.g., ArrayListBuilder or + * @param field the field definition containing name, type, and javadoc + * @param consumerBuilderType the builder type (e.g., ArrayListBuilder or * ArrayListBuilderWithElementBuilders) * @param constructorArgsWithValue constructor arguments when field is already set - * @param constructorArgsEmpty constructor arguments when field is empty + * @param additionalConstructorArgs constructor arguments when field is empty * @param additionalArguments additional template arguments to add to the method (must be TypeName * values) + * @param returnBuilderType the builder type for the return type * @param context processing context */ private static MethodDto createFieldConsumerWithBuilder( - String fieldName, - String fieldNameInBuilder, - String fieldJavaDoc, + FieldDto field, TypeName consumerBuilderType, String constructorArgsWithValue, String additionalConstructorArgs, @@ -1168,24 +1176,29 @@ private static MethodDto createFieldConsumerWithBuilder( TypeNameGeneric consumerType = new TypeNameGeneric(map2TypeName(Consumer.class), consumerBuilderType); MethodParameterDto parameter = new MethodParameterDto(); - parameter.setParameterName(fieldName + BUILDER_SUFFIX + SUFFIX_CONSUMER); + parameter.setParameterName(field.getFieldName() + BUILDER_SUFFIX + SUFFIX_CONSUMER); parameter.setParameterTypeName(consumerType); MethodDto methodDto = new MethodDto(); - methodDto.setMethodName(generateSetterName(fieldName, context)); + methodDto.setMethodName(generateSetterName(field.getFieldName(), context)); methodDto.setReturnType(returnBuilderType); methodDto.addParameter(parameter); setMethodAccessModifier(methodDto, getMethodAccessModifier(context)); + + // Wrap the builder result with a specific collection constructor if needed + String buildExpression = calculateBuildExpression(field.getFieldType()); + methodDto.setCode( """ $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(builder.build()); + this.$fieldName:N = $builderFieldWrapper:T.changedValue($buildExpression:N); return this; """ .formatted(constructorArgsWithValue, additionalConstructorArgs)); - methodDto.addArgument(ARG_FIELD_NAME, fieldNameInBuilder); + methodDto.addArgument(ARG_FIELD_NAME, field.getFieldName()); methodDto.addArgument(ARG_DTO_METHOD_PARAM, parameter.getParameterName()); methodDto.addArgument(ARG_HELPER_TYPE, consumerBuilderType); + methodDto.addArgument("buildExpression", buildExpression); additionalArguments.forEach(methodDto::addArgument); methodDto.addArgument(ARG_BUILDER_FIELD_WRAPPER, TRACKED_VALUE_TYPE); methodDto.setPriority(MethodDto.PRIORITY_MEDIUM); @@ -1196,7 +1209,7 @@ private static MethodDto createFieldConsumerWithBuilder( @param %s consumer providing an instance of a builder for %s @return current instance of builder """ - .formatted(fieldName, parameter.getParameterName(), fieldJavaDoc)); + .formatted(field.getFieldName(), parameter.getParameterName(), field.getJavaDoc())); return methodDto; } @@ -1254,7 +1267,7 @@ private static MethodDto createStringFormatMethodWithTransform( MethodParameterDto argsParam = new MethodParameterDto(); argsParam.setParameterName("args"); - argsParam.setParameterTypeName(new TypeNameArray(TypeName.of(Object.class), false)); + argsParam.setParameterTypeName(new TypeNameArray(TypeName.of(Object.class))); MethodDto methodDto = new MethodDto(); methodDto.setMethodName(generateSetterName(fieldName, context)); 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 2ceb74cc..bf265a70 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 @@ -143,6 +143,256 @@ private static List extractTypeForList( return result; } + /** + * Checks if the given type implements List, Set, or Map interfaces and wraps it in the + * appropriate specialized TypeName class. + * + *

This method correctly extracts the type arguments used by the collection interface, not the + * class's declared type parameters. For example, {@code CustomList extends ArrayList} + * will extract Y as the List element type, not both X and Y. + * + * @param rawType the base TypeName (preserves concrete class information) + * @param argTypes the generic type arguments from the class declaration (may not match interface) + * @param typeMirror the TypeMirror to check for interface implementation + * @param context the processing context + * @return a specialized TypeName (TypeNameList, TypeNameSet, TypeNameMap) if applicable, or a + * TypeNameGeneric/rawType otherwise + */ + private static TypeName wrapInCollectionTypeIfApplicable( + TypeName rawType, List argTypes, TypeMirror typeMirror, ProcessingContext context) { + TypeName listWrapper = tryWrapAsList(rawType, argTypes, typeMirror, context); + if (listWrapper != null) { + return listWrapper; + } + + TypeName setWrapper = tryWrapAsSet(rawType, argTypes, typeMirror, context); + if (setWrapper != null) { + return setWrapper; + } + + TypeName mapWrapper = tryWrapAsMap(rawType, argTypes, typeMirror, context); + if (mapWrapper != null) { + return mapWrapper; + } + + // Not a collection type - return generic or raw type + return createFallbackTypeName(rawType, argTypes); + } + + /** + * Attempts to wrap the type as a TypeNameList if it implements List and has appropriate + * constructor. + * + * @return TypeNameList if applicable, null otherwise + */ + private static TypeName tryWrapAsList( + TypeName rawType, List argTypes, TypeMirror typeMirror, ProcessingContext context) { + TypeElement listElement = context.getTypeElement("java.util.List"); + if (listElement == null) { + return null; + } + + TypeMirror listType = context.erasure(listElement.asType()); + if (!context.isAssignable(context.erasure(typeMirror), listType)) { + return null; + } + + TypeElement typeElement = (TypeElement) context.asElement(typeMirror); + if (!shouldWrapAsCollectionType(typeElement, listElement, context)) { + return createFallbackTypeName(rawType, argTypes); + } + + List interfaceTypeArgs = + extractInterfaceTypeArguments(typeMirror, listElement, context); + TypeName elementType = interfaceTypeArgs.isEmpty() ? null : interfaceTypeArgs.get(0); + return new TypeNameList(rawType, argTypes, elementType); + } + + /** + * Attempts to wrap the type as a TypeNameSet if it implements Set and has appropriate + * constructor. + * + * @return TypeNameSet if applicable, null otherwise + */ + private static TypeName tryWrapAsSet( + TypeName rawType, List argTypes, TypeMirror typeMirror, ProcessingContext context) { + TypeElement setElement = context.getTypeElement("java.util.Set"); + if (setElement == null) { + return null; + } + + TypeMirror setType = context.erasure(setElement.asType()); + if (!context.isAssignable(context.erasure(typeMirror), setType)) { + return null; + } + + TypeElement typeElement = (TypeElement) context.asElement(typeMirror); + if (!shouldWrapAsCollectionType(typeElement, setElement, context)) { + return createFallbackTypeName(rawType, argTypes); + } + + List interfaceTypeArgs = + extractInterfaceTypeArguments(typeMirror, setElement, context); + TypeName elementType = interfaceTypeArgs.isEmpty() ? null : interfaceTypeArgs.get(0); + return new TypeNameSet(rawType, argTypes, elementType); + } + + /** + * Attempts to wrap the type as a TypeNameMap if it implements Map and has appropriate + * constructor. + * + * @return TypeNameMap if applicable, null otherwise + */ + private static TypeName tryWrapAsMap( + TypeName rawType, List argTypes, TypeMirror typeMirror, ProcessingContext context) { + TypeElement mapElement = context.getTypeElement("java.util.Map"); + if (mapElement == null) { + return null; + } + + TypeMirror mapType = context.erasure(mapElement.asType()); + if (!context.isAssignable(context.erasure(typeMirror), mapType)) { + return null; + } + + TypeElement typeElement = (TypeElement) context.asElement(typeMirror); + boolean isInterface = typeElement.equals(mapElement); + boolean hasConstructor = + !isInterface && hasConstructorWithParameterOfType(typeElement, "java.util.Map", context); + + if (!isInterface && !hasConstructor) { + return createFallbackTypeName(rawType, argTypes); + } + + List interfaceTypeArgs = + extractInterfaceTypeArguments(typeMirror, mapElement, context); + TypeName keyType = interfaceTypeArgs.isEmpty() ? null : interfaceTypeArgs.get(0); + TypeName valueType = interfaceTypeArgs.size() < 2 ? null : interfaceTypeArgs.get(1); + return new TypeNameMap(rawType, argTypes, keyType, valueType); + } + + /** + * Checks if a type should be wrapped as a specialized collection type (List or Set). + * + * @param typeElement the type to check + * @param interfaceElement the collection interface (List or Set) + * @param context the processing context + * @return true if the type is the interface itself or has a Collection constructor + */ + private static boolean shouldWrapAsCollectionType( + TypeElement typeElement, TypeElement interfaceElement, ProcessingContext context) { + boolean isInterface = typeElement.equals(interfaceElement); + boolean hasConstructor = + !isInterface + && hasConstructorWithParameterOfType(typeElement, "java.util.Collection", context); + return isInterface || hasConstructor; + } + + /** + * Creates a fallback TypeName when a type cannot be wrapped as a specialized collection type. + * + * @param rawType the raw type + * @param argTypes the type arguments + * @return TypeNameGeneric if argTypes is not empty, otherwise rawType + */ + private static TypeName createFallbackTypeName(TypeName rawType, List argTypes) { + return argTypes.isEmpty() ? rawType : new TypeNameGeneric(rawType, argTypes); + } + + /** + * Checks if a type has a constructor that accepts a parameter of the specified type. + * + *

This allows us to detect if we can generate helper methods for collection/map types by + * checking if it has a constructor like {@code ArrayList(Collection)} or {@code + * HashMap(Map)}. + * + * @param typeElement the type to check + * @param parameterTypeName the fully qualified name of the parameter type (e.g., + * "java.util.Collection") + * @param context the processing context + * @return true if the type has a constructor accepting the specified parameter type + */ + private static boolean hasConstructorWithParameterOfType( + TypeElement typeElement, String parameterTypeName, ProcessingContext context) { + TypeElement parameterElement = context.getTypeElement(parameterTypeName); + if (parameterElement == null) { + return false; + } + + TypeMirror parameterType = context.erasure(parameterElement.asType()); + + return typeElement.getEnclosedElements().stream() + .filter(element -> element.getKind() == javax.lang.model.element.ElementKind.CONSTRUCTOR) + .map(element -> (javax.lang.model.element.ExecutableElement) element) + .anyMatch( + constructor -> { + if (constructor.getParameters().size() != 1) { + return false; + } + TypeMirror constructorParamType = constructor.getParameters().get(0).asType(); + TypeMirror erasedConstructorParamType = context.erasure(constructorParamType); + // Parameter should match the specified type or be assignable to it + return context.isSameType(erasedConstructorParamType, parameterType) + || context.isAssignable(erasedConstructorParamType, parameterType); + }); + } + + /** + * Extracts the type arguments used by a specific interface from a type's supertype hierarchy. + * + *

For example, given {@code CustomList extends ArrayList} and the {@code List} + * interface, this returns [Y], not [X,Y]. + * + * @param typeMirror the type to examine + * @param targetInterface the interface element (e.g., List, Set, Map) + * @param context the processing context + * @return list of type arguments used by the interface, or empty list if raw type + */ + public static List extractInterfaceTypeArguments( + TypeMirror typeMirror, TypeElement targetInterface, ProcessingContext context) { + // Walk the supertype hierarchy to find the specific instantiation of the target interface + TypeMirror found = findSupertype(typeMirror, targetInterface, context); + + if (found instanceof DeclaredType declaredType) { + List typeArgs = declaredType.getTypeArguments(); + if (!typeArgs.isEmpty()) { + return extractTypeForList(new ArrayList<>(typeArgs), context); + } + } + + return List.of(); // Raw type + } + + /** + * Finds the specific supertype that matches the target interface in the type hierarchy. + * + * @param typeMirror the type to search from + * @param targetInterface the interface to find + * @param context the processing context + * @return the matching supertype, or null if not found + */ + private static TypeMirror findSupertype( + TypeMirror typeMirror, TypeElement targetInterface, ProcessingContext context) { + if (!(typeMirror instanceof DeclaredType)) { + return null; + } + + TypeElement typeElement = (TypeElement) context.asElement(typeMirror); + if (typeElement.equals(targetInterface)) { + return typeMirror; + } + + // Check direct supertypes (superclass and interfaces) + for (TypeMirror supertype : context.directSupertypes(typeMirror)) { + TypeMirror found = findSupertype(supertype, targetInterface, context); + if (found != null) { + return found; + } + } + + return null; + } + private static TypeName extractType(TypeMirror typeOfParameter, ProcessingContext context) { return typeOfParameter.accept( new SimpleTypeVisitor14() { @@ -175,22 +425,21 @@ public TypeName visitDeclared(DeclaredType t, Void p) { ? enclosingType.accept(this, null) : null; if (t.getTypeArguments().isEmpty() && !(enclosing instanceof TypeNameGeneric)) { - return rawType; + return wrapInCollectionTypeIfApplicable(rawType, List.of(), typeOfParameter, context); } List typesExtracted = new ArrayList<>(t.getTypeArguments()); if (typesExtracted.isEmpty()) { - return rawType; + return wrapInCollectionTypeIfApplicable(rawType, List.of(), typeOfParameter, context); } else { List argTypes = extractTypeForList(typesExtracted, context); - // Represent all generics uniformly - return new TypeNameGeneric(rawType, argTypes); + return wrapInCollectionTypeIfApplicable(rawType, argTypes, typeOfParameter, context); } } @Override public TypeNameArray visitArray(ArrayType t, Void p) { - return new TypeNameArray(extractType(t.getComponentType(), context), false); + return new TypeNameArray(extractType(t.getComponentType(), context)); } @Override 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 7f4b5a60..7a407497 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 @@ -94,6 +94,10 @@ public static TypeName map2ParameterType( default -> null; }; } else 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); } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ProcessingContext.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ProcessingContext.java index 2d927c70..5cbc6217 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ProcessingContext.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/ProcessingContext.java @@ -150,6 +150,37 @@ public boolean isSameType(TypeMirror type1, TypeMirror type2) { return typeUtils.isSameType(type1, type2); } + /** + * Get the erasure of a type (removes generic type information). + * + * @param typeMirror the type to erase + * @return the erasure of the type + */ + public TypeMirror erasure(TypeMirror typeMirror) { + return typeUtils.erasure(typeMirror); + } + + /** + * Check if one type is assignable to another. + * + * @param type1 the type to check + * @param type2 the target type + * @return true if type1 is assignable to type2 + */ + public boolean isAssignable(TypeMirror type1, TypeMirror type2) { + return typeUtils.isAssignable(type1, type2); + } + + /** + * Returns the direct supertypes of a type. + * + * @param typeMirror the type + * @return list of direct supertypes + */ + public java.util.List directSupertypes(TypeMirror typeMirror) { + return typeUtils.directSupertypes(typeMirror); + } + /** * Logs an info-level message that appears in normal Maven output. * diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/TypeNameAnalyser.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/TypeNameAnalyser.java index 3d86108c..436ce029 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/TypeNameAnalyser.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/TypeNameAnalyser.java @@ -34,17 +34,6 @@ public class TypeNameAnalyser { private TypeNameAnalyser() {} - /** - * Helper to check if the type is a {@code java.util.Map}. - * - * @param typeName Type to be validated - * @return {@code true}, if it is a {@code java.util.Map} - */ - public static boolean isMap(TypeName typeName) { - return Strings.CI.equals(typeName.getPackageName(), JAVA_UTIL_PACKAGE) - && Strings.CI.equals(typeName.getClassName(), "Map"); - } - /** * Helper to check if the type is a java-base class. Check is done by comparing the package name. * @@ -56,28 +45,6 @@ public static boolean isJavaClass(TypeName typeName) { typeName.getPackageName(), "java.lang", "java.time", JAVA_UTIL_PACKAGE); } - /** - * Helper to check if the type is a {@code java.util.Set}. - * - * @param typeName Type to be validated - * @return {@code true}, if it is a {@code java.util.Set} - */ - public static boolean isSet(TypeName typeName) { - return Strings.CI.equals(typeName.getPackageName(), JAVA_UTIL_PACKAGE) - && Strings.CI.equals(typeName.getClassName(), "Set"); - } - - /** - * Helper to check if the type is a {@code java.util.List}. - * - * @param typeName Type to be validated - * @return {@code true}, if it is a {@code java.util.List} - */ - public static boolean isList(TypeName typeName) { - return Strings.CI.equals(typeName.getPackageName(), JAVA_UTIL_PACKAGE) - && Strings.CI.equals(typeName.getClassName(), "List"); - } - /** * Helper to check if the type is a {@code java.util.Optional}. * @@ -114,4 +81,19 @@ && isOptional(fieldType) } return false; } + + /** + * Checks if an Optional type is properly parameterized (not a raw type). + * + *

A parameterized Optional has exactly 1 type argument (the wrapped type). A raw Optional has + * 0 type arguments. + * + * @param typeName Type to be validated + * @return {@code true} if it's an Optional with exactly 1 type argument + */ + public static boolean isParameterizedOptional(TypeName typeName) { + return typeName instanceof TypeNameGeneric genericType + && isOptional(typeName) + && genericType.getInnerTypeArguments().size() == 1; + } } 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 8f115631..180ba745 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 @@ -2850,4 +2850,121 @@ public PlainClass() {} notContains("ArrayListBuilderWithElementBuilders"), notContains("PlainClassBuilder")); } + + @Test + void shouldPreserveSpecificCollectionTypesWithVarargsAndConsumerMethods() { + // Given: A DTO with specific collection implementations (LinkedList, ArrayList, HashSet, etc.) + String packageName = "test"; + String className = "SpecificCollectionsDto"; + String builderClassName = className + "Builder"; + + JavaFileObject dto = + ProcessorTestUtils.simpleBuilderClass( + packageName, + className, + """ + private java.util.LinkedList linkedList; + private java.util.ArrayList arrayList; + private java.util.HashSet hashSet; + private java.util.TreeSet treeSet; + private java.util.HashMap hashMap; + private java.util.TreeMap treeMap; + + public java.util.LinkedList getLinkedList() { return linkedList; } + public void setLinkedList(java.util.LinkedList linkedList) { this.linkedList = linkedList; } + public java.util.ArrayList getArrayList() { return arrayList; } + public void setArrayList(java.util.ArrayList arrayList) { this.arrayList = arrayList; } + public java.util.HashSet getHashSet() { return hashSet; } + public void setHashSet(java.util.HashSet hashSet) { this.hashSet = hashSet; } + public java.util.TreeSet getTreeSet() { return treeSet; } + public void setTreeSet(java.util.TreeSet treeSet) { this.treeSet = treeSet; } + public java.util.HashMap getHashMap() { return hashMap; } + public void setHashMap(java.util.HashMap hashMap) { this.hashMap = hashMap; } + public java.util.TreeMap getTreeMap() { return treeMap; } + public void setTreeMap(java.util.TreeMap treeMap) { this.treeMap = treeMap; } + """); + + // When + Compilation compilation = compile(dto); + + // Then + String generatedCode = loadGeneratedSource(compilation, builderClassName); + assertGenerationSucceeded(compilation, builderClassName, generatedCode); + + // Verify LinkedList methods: setter, supplier, varargs with LinkedList constructor + ProcessorAsserts.assertingResult( + generatedCode, + // Direct setter + contains("public SpecificCollectionsDtoBuilder linkedList(LinkedList linkedList)"), + // Supplier + contains( + "public SpecificCollectionsDtoBuilder linkedList(Supplier> linkedListSupplier)"), + // Varargs with LinkedList wrapper + contains("public SpecificCollectionsDtoBuilder linkedList(String... linkedList)"), + contains("new LinkedList<>(java.util.List.of(linkedList))"), + // Consumer with LinkedList wrapper + contains( + "public SpecificCollectionsDtoBuilder linkedList(Consumer> linkedListBuilderConsumer)"), + contains("new LinkedList<>(builder.build())")); + + // Verify ArrayList methods: setter, supplier, varargs with ArrayList constructor + ProcessorAsserts.assertingResult( + generatedCode, + contains("public SpecificCollectionsDtoBuilder arrayList(ArrayList arrayList)"), + contains( + "public SpecificCollectionsDtoBuilder arrayList(Supplier> arrayListSupplier)"), + contains("public SpecificCollectionsDtoBuilder arrayList(Integer... arrayList)"), + contains("new ArrayList<>(java.util.List.of(arrayList))"), + contains( + "public SpecificCollectionsDtoBuilder arrayList(Consumer> arrayListBuilderConsumer)"), + contains("new ArrayList<>(builder.build())")); + + // Verify HashSet methods: setter, supplier, varargs with HashSet constructor + ProcessorAsserts.assertingResult( + generatedCode, + contains("public SpecificCollectionsDtoBuilder hashSet(HashSet hashSet)"), + contains( + "public SpecificCollectionsDtoBuilder hashSet(Supplier> hashSetSupplier)"), + contains("public SpecificCollectionsDtoBuilder hashSet(Double... hashSet)"), + contains("new HashSet<>(java.util.Set.of(hashSet))"), + contains( + "public SpecificCollectionsDtoBuilder hashSet(Consumer> hashSetBuilderConsumer)"), + contains("new HashSet<>(builder.build())")); + + // Verify TreeSet methods: setter, supplier, varargs with TreeSet constructor + ProcessorAsserts.assertingResult( + generatedCode, + contains("public SpecificCollectionsDtoBuilder treeSet(TreeSet treeSet)"), + contains( + "public SpecificCollectionsDtoBuilder treeSet(Supplier> treeSetSupplier)"), + contains("public SpecificCollectionsDtoBuilder treeSet(Long... treeSet)"), + contains("new TreeSet<>(java.util.Set.of(treeSet))"), + contains( + "public SpecificCollectionsDtoBuilder treeSet(Consumer> treeSetBuilderConsumer)"), + contains("new TreeSet<>(builder.build())")); + + // Verify HashMap methods: setter, supplier, varargs with HashMap constructor + ProcessorAsserts.assertingResult( + generatedCode, + contains("public SpecificCollectionsDtoBuilder hashMap(HashMap hashMap)"), + contains( + "public SpecificCollectionsDtoBuilder hashMap(Supplier> hashMapSupplier)"), + contains("public SpecificCollectionsDtoBuilder hashMap(Entry... hashMap)"), + contains("new HashMap<>(java.util.Map.ofEntries(hashMap))"), + contains( + "public SpecificCollectionsDtoBuilder hashMap(Consumer> hashMapBuilderConsumer)"), + contains("new HashMap<>(builder.build())")); + + // Verify TreeMap methods: setter, supplier, varargs with TreeMap constructor + ProcessorAsserts.assertingResult( + generatedCode, + contains("public SpecificCollectionsDtoBuilder treeMap(TreeMap treeMap)"), + contains( + "public SpecificCollectionsDtoBuilder treeMap(Supplier> treeMapSupplier)"), + contains("public SpecificCollectionsDtoBuilder treeMap(Entry... treeMap)"), + contains("new TreeMap<>(java.util.Map.ofEntries(treeMap))"), + contains( + "public SpecificCollectionsDtoBuilder treeMap(Consumer> treeMapBuilderConsumer)"), + contains("new TreeMap<>(builder.build())")); + } } diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/CustomCollectionTypeTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/CustomCollectionTypeTest.java new file mode 100644 index 00000000..2b33fcf0 --- /dev/null +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/CustomCollectionTypeTest.java @@ -0,0 +1,393 @@ +/* + * MIT License + * + * Copyright (c) 2025 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.javahelpers.simple.builders.processor; + +import static org.javahelpers.simple.builders.processor.testing.ProcessorAsserts.assertGenerationSucceeded; +import static org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils.loadGeneratedSource; + +import com.google.testing.compile.Compilation; +import javax.tools.JavaFileObject; +import org.javahelpers.simple.builders.processor.testing.ProcessorAsserts; +import org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils; +import org.junit.jupiter.api.Test; + +/** + * Tests for custom collection implementations with multiple type parameters where only a subset is + * used for the collection interface. + * + *

This tests the critical edge case where a class like {@code ExampleClass implements + * List} should correctly extract Y as the List element type, not both X and Y. + */ +class CustomCollectionTypeTest { + + private static Compilation compile(JavaFileObject... sources) { + return ProcessorTestUtils.createCompiler().compile(sources); + } + + @Test + void customListWithMultipleTypeParameters_shouldNotGenerateVarargsHelper() { + // Custom List with 2 type parameters - should be treated as generic type, not TypeNameList + // This means NO varargs helper methods should be generated + JavaFileObject customList = + ProcessorTestUtils.forSource( + """ + package test; + import java.util.ArrayList; + public class CustomList extends ArrayList { + private X metadata; + public CustomList(X metadata) { this.metadata = metadata; } + public X getMetadata() { return metadata; } + } + """); + + JavaFileObject dto = + ProcessorTestUtils.simpleBuilderClass( + "test", + "CustomListDto", + """ + private final CustomList numbers; + + public CustomListDto(CustomList numbers) { + this.numbers = numbers; + } + + public CustomList getNumbers() { + return numbers; + } + """); + + Compilation compilation = compile(customList, dto); + String generatedCode = loadGeneratedSource(compilation, "CustomListDtoBuilder"); + assertGenerationSucceeded(compilation, "CustomListDtoBuilder", generatedCode); + + // Should NOT generate varargs method for custom collection types + ProcessorAsserts.assertNotContaining( + generatedCode, "public CustomListDtoBuilder numbers(Integer... numbers)"); + + // Should still have the basic setter + ProcessorAsserts.assertContaining( + generatedCode, "public CustomListDtoBuilder numbers(CustomList numbers)"); + } + + @Test + void standardArrayListWithSingleTypeParameter_shouldGenerateVarargsHelper() { + // Standard ArrayList should generate varargs helper + JavaFileObject dto = + ProcessorTestUtils.simpleBuilderClass( + "test", + "ArrayListDto", + """ + private final java.util.ArrayList numbers; + + public ArrayListDto(java.util.ArrayList numbers) { + this.numbers = numbers; + } + + public java.util.ArrayList getNumbers() { + return numbers; + } + """); + + Compilation compilation = compile(dto); + String generatedCode = loadGeneratedSource(compilation, "ArrayListDtoBuilder"); + assertGenerationSucceeded(compilation, "ArrayListDtoBuilder", generatedCode); + + // Should generate varargs method for standard ArrayList + ProcessorAsserts.assertContaining( + generatedCode, "public ArrayListDtoBuilder numbers(Integer... numbers)"); + } + + @Test + void customSetWithMultipleTypeParameters_shouldNotGenerateVarargsHelper() { + // Custom Set with 2 type parameters - should be treated as generic type, not TypeNameSet + JavaFileObject customSet = + ProcessorTestUtils.forSource( + """ + package test; + import java.util.HashSet; + public class CustomSet extends HashSet { + private X metadata; + public CustomSet(X metadata) { this.metadata = metadata; } + public X getMetadata() { return metadata; } + } + """); + + JavaFileObject dto = + ProcessorTestUtils.simpleBuilderClass( + "test", + "CustomSetDto", + """ + private final CustomSet tags; + + public CustomSetDto(CustomSet tags) { + this.tags = tags; + } + + public CustomSet getTags() { + return tags; + } + """); + + Compilation compilation = compile(customSet, dto); + String generatedCode = loadGeneratedSource(compilation, "CustomSetDtoBuilder"); + assertGenerationSucceeded(compilation, "CustomSetDtoBuilder", generatedCode); + + // Should NOT generate varargs method for custom collection types + ProcessorAsserts.assertNotContaining( + generatedCode, "public CustomSetDtoBuilder tags(String... tags)"); + + // Should still have the basic setter + ProcessorAsserts.assertContaining( + generatedCode, "public CustomSetDtoBuilder tags(CustomSet tags)"); + } + + @Test + void customMapWithMultipleTypeParameters_shouldNotGenerateVarargsHelper() { + // Custom Map with 3 type parameters - should be treated as generic type, not TypeNameMap + JavaFileObject customMap = + ProcessorTestUtils.forSource( + """ + package test; + import java.util.HashMap; + public class CustomMap extends HashMap { + private X metadata; + public CustomMap(X metadata) { this.metadata = metadata; } + public X getMetadata() { return metadata; } + } + """); + + JavaFileObject dto = + ProcessorTestUtils.simpleBuilderClass( + "test", + "CustomMapDto", + """ + private final CustomMap data; + + public CustomMapDto(CustomMap data) { + this.data = data; + } + + public CustomMap getData() { + return data; + } + """); + + Compilation compilation = compile(customMap, dto); + String generatedCode = loadGeneratedSource(compilation, "CustomMapDtoBuilder"); + assertGenerationSucceeded(compilation, "CustomMapDtoBuilder", generatedCode); + + // Should NOT generate varargs method for custom collection types + ProcessorAsserts.assertNotContaining( + generatedCode, "public CustomMapDtoBuilder data(Map.Entry... data)"); + + // Should still have the basic setter + ProcessorAsserts.assertContaining( + generatedCode, "public CustomMapDtoBuilder data(CustomMap data)"); + } + + @Test + void customMapWithSingleTypeParameter_shouldGenerateVarargsHelperWithSameKeyValue() { + // CustomMap implements Map with a Map constructor + // This should be detected as a valid Map type and generate helpers + JavaFileObject customMap = + ProcessorTestUtils.forSource( + """ + package test; + import java.util.HashMap; + import java.util.Map; + public class SymmetricMap extends HashMap { + public SymmetricMap() { } + public SymmetricMap(Map m) { super(m); } + } + """); + + JavaFileObject dto = + ProcessorTestUtils.simpleBuilderClass( + "test", + "SymmetricMapDto", + """ + private final SymmetricMap data; + + public SymmetricMapDto(SymmetricMap data) { + this.data = data; + } + + public SymmetricMap getData() { + return data; + } + """); + + Compilation compilation = compile(customMap, dto); + String generatedCode = loadGeneratedSource(compilation, "SymmetricMapDtoBuilder"); + assertGenerationSucceeded(compilation, "SymmetricMapDtoBuilder", generatedCode); + + // Should generate varargs method with String for both key and value + // (extracted from Map where T=String) + // Note: uses imported Entry, not Map.Entry + ProcessorAsserts.assertContaining( + generatedCode, "public SymmetricMapDtoBuilder data(Entry... data)"); + + // Should still have the basic setter with the single type parameter + ProcessorAsserts.assertContaining( + generatedCode, "public SymmetricMapDtoBuilder data(SymmetricMap data)"); + } + + @Test + void rawListType_shouldNotGenerateVarargsHelper() { + // Raw List (no type parameters) should not generate varargs methods + JavaFileObject dto = + ProcessorTestUtils.simpleBuilderClass( + "test", + "RawListDto", + """ + @SuppressWarnings("rawtypes") + private final java.util.List items; + + public RawListDto(java.util.List items) { + this.items = items; + } + + @SuppressWarnings("rawtypes") + public java.util.List getItems() { + return items; + } + """); + + Compilation compilation = compile(dto); + String generatedCode = loadGeneratedSource(compilation, "RawListDtoBuilder"); + assertGenerationSucceeded(compilation, "RawListDtoBuilder", generatedCode); + + // Should NOT generate varargs method for raw types + ProcessorAsserts.assertNotContaining(generatedCode, "items(Object... items)"); + + // Should still have the basic setter + ProcessorAsserts.assertContaining(generatedCode, "public RawListDtoBuilder items(List items)"); + } + + @Test + void rawSetType_shouldNotGenerateVarargsHelper() { + // Raw Set (no type parameters) should not generate varargs methods + JavaFileObject dto = + ProcessorTestUtils.simpleBuilderClass( + "test", + "RawSetDto", + """ + @SuppressWarnings("rawtypes") + private final java.util.Set tags; + + public RawSetDto(java.util.Set tags) { + this.tags = tags; + } + + @SuppressWarnings("rawtypes") + public java.util.Set getTags() { + return tags; + } + """); + + Compilation compilation = compile(dto); + String generatedCode = loadGeneratedSource(compilation, "RawSetDtoBuilder"); + assertGenerationSucceeded(compilation, "RawSetDtoBuilder", generatedCode); + + // Should NOT generate varargs method for raw types + ProcessorAsserts.assertNotContaining(generatedCode, "tags(Object... tags)"); + + // Should still have the basic setter + ProcessorAsserts.assertContaining(generatedCode, "public RawSetDtoBuilder tags(Set tags)"); + } + + @Test + void rawMapType_shouldNotGenerateVarargsHelper() { + // Raw Map (no type parameters) should not generate varargs methods + JavaFileObject dto = + ProcessorTestUtils.simpleBuilderClass( + "test", + "RawMapDto", + """ + @SuppressWarnings("rawtypes") + private final java.util.Map config; + + public RawMapDto(java.util.Map config) { + this.config = config; + } + + @SuppressWarnings("rawtypes") + public java.util.Map getConfig() { + return config; + } + """); + + Compilation compilation = compile(dto); + String generatedCode = loadGeneratedSource(compilation, "RawMapDtoBuilder"); + assertGenerationSucceeded(compilation, "RawMapDtoBuilder", generatedCode); + + // Should NOT generate varargs method for raw types + ProcessorAsserts.assertNotContaining(generatedCode, "config(Entry... config)"); + ProcessorAsserts.assertNotContaining(generatedCode, "config(Map.Entry... config)"); + + // Should still have the basic setter + ProcessorAsserts.assertContaining(generatedCode, "public RawMapDtoBuilder config(Map config)"); + } + + @Test + void unmodifiableListInConstructor_shouldHandleCorrectly() { + // DTO that creates unmodifiable list in constructor - builder should handle this safely + // The DTO internally uses List.copyOf() which creates an unmodifiable list + JavaFileObject dto = + ProcessorTestUtils.simpleBuilderClass( + "test", + "UnmodifiableListDto", + """ + private final java.util.List items; + + public UnmodifiableListDto(java.util.List items) { + this.items = java.util.List.copyOf(items); + } + + public java.util.List getItems() { + return items; + } + """); + + Compilation compilation = compile(dto); + String generatedCode = loadGeneratedSource(compilation, "UnmodifiableListDtoBuilder"); + assertGenerationSucceeded(compilation, "UnmodifiableListDtoBuilder", generatedCode); + + // SAFE: Builder constructor only stores reference to the unmodifiable list + // It does NOT attempt to modify it - just wraps it in TrackedValue + ProcessorAsserts.assertContaining( + generatedCode, "this.items = initialValue(instance.getItems())"); + + // SAFE: Varargs helper creates a NEW list using List.of() + // It does NOT try to modify any existing unmodifiable list + ProcessorAsserts.assertContaining( + generatedCode, "public UnmodifiableListDtoBuilder items(String... items)"); + ProcessorAsserts.assertContaining(generatedCode, "this.items = changedValue(List.of(items))"); + + // SAFE: Direct setter just stores the reference + ProcessorAsserts.assertContaining( + generatedCode, "public UnmodifiableListDtoBuilder items(List items)"); + ProcessorAsserts.assertContaining(generatedCode, "this.items = changedValue(items)"); + } +}