From 9297269228d5fb038668ae508180fb2a20e9a7f4 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Mon, 22 Dec 2025 23:45:56 +0100 Subject: [PATCH 01/19] Adding tests for Helpers should support special collections --- .../processor/BuilderProcessorTest.java | 129 +++++++++++++++++- 1 file changed, 123 insertions(+), 6 deletions(-) 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..423ee0f9 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 @@ -567,12 +567,12 @@ void shouldGenerateCollectionSettersAndProviders() { "public WithCollectionsBuilder names(List names)", "public WithCollectionsBuilder names(Supplier> namesSupplier)", "public WithCollectionsBuilder names(String... names)", - "this.names = changedValue(List.of(names));", + "this.names = changedValue(java.util.List.of(names));", "private TrackedValue> names = unsetValue();", "public WithCollectionsBuilder tags(Set tags)", "public WithCollectionsBuilder tags(Supplier> tagsSupplier)", "public WithCollectionsBuilder tags(String... tags)", - "this.tags = changedValue(Set.of(tags));", + "this.tags = changedValue(java.util.Set.of(tags));", "private TrackedValue> tags = unsetValue();", "public WithCollectionsBuilder map(Map map)", "public WithCollectionsBuilder map(Supplier> mapSupplier)", @@ -1506,7 +1506,7 @@ void shouldHandleSetOfStrings() { "public HasSetStringBuilder tags(Consumer> tagsBuilderConsumer)", "this.tags = changedValue(builder.build());", "public HasSetStringBuilder tags(String... tags)", - "this.tags = changedValue(Set.of(tags));", + "this.tags = changedValue(java.util.Set.of(tags));", "public HasSetStringBuilder tags(Supplier> tagsSupplier)", "this.tags = changedValue(tagsSupplier.get());"); } @@ -1551,7 +1551,7 @@ public Helper() {} "public HasSetCustomBuilder helpers(Consumer> helpersBuilderConsumer)", "this.helpers = changedValue(builder.build());", "public HasSetCustomBuilder helpers(Helper... helpers)", - "this.helpers = changedValue(Set.of(helpers));", + "this.helpers = changedValue(java.util.Set.of(helpers));", "public HasSetCustomBuilder helpers(Supplier> helpersSupplier)", "this.helpers = changedValue(helpersSupplier.get());"); } @@ -1810,8 +1810,8 @@ void shouldGenerateVarargsConvenienceForCollections() { assertGenerationSucceeded(compilation, builderClassName, generatedCode); ProcessorAsserts.assertContaining( generatedCode, - "public HasCollectionsConvenienceBuilder names(String... names) { this.names = changedValue(List.of(names));", - "public HasCollectionsConvenienceBuilder tags(String... tags) { this.tags = changedValue(Set.of(tags));"); + "public HasCollectionsConvenienceBuilder names(String... names) { this.names = changedValue(java.util.List.of(names));", + "public HasCollectionsConvenienceBuilder tags(String... tags) { this.tags = changedValue(java.util.Set.of(tags));"); } @Test @@ -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())")); + } } From cefa3e0bb7d3423e25038a447cbb81c5347b5fcd Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Tue, 23 Dec 2025 00:21:23 +0100 Subject: [PATCH 02/19] Refactoring in BuilderDefinitionCreator to use FieldDto instead of independend field-properties --- .../util/BuilderDefinitionCreator.java | 83 ++++--------------- 1 file changed, 17 insertions(+), 66 deletions(-) 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..e1bf2171 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 @@ -452,12 +452,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; } @@ -556,26 +551,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; @@ -606,13 +589,7 @@ private static boolean tryAddMapConsumer( fieldTypeGeneric.getInnerTypeArguments().get(0), fieldTypeGeneric.getInnerTypeArguments().get(1)); MethodDto mapConsumerWithBuilder = - BuilderDefinitionCreator.createFieldConsumerWithBuilder( - field.getFieldName(), - field.getFieldName(), - field.getJavaDoc(), - builderTargetTypeName, - builderType, - context); + createFieldConsumerWithBuilderForMap(field, builderTargetTypeName, builderType, context); field.addMethod(mapConsumerWithBuilder); return true; } @@ -653,26 +630,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; @@ -1082,9 +1047,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 +1055,19 @@ private static MethodDto createFieldConsumerWithBuilder( TypeNameGeneric builderTypeGeneric = new TypeNameGeneric(consumerBuilderType, builderTargetType); return BuilderDefinitionCreator.createFieldConsumerWithBuilder( - fieldName, - fieldNameInBuilder, - fieldJavadoc, + 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 +1081,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", @@ -1156,9 +1109,7 @@ private static MethodDto createFieldConsumerWithElementBuilders( * @param context processing context */ private static MethodDto createFieldConsumerWithBuilder( - String fieldName, - String fieldNameInBuilder, - String fieldJavaDoc, + FieldDto field, TypeName consumerBuilderType, String constructorArgsWithValue, String additionalConstructorArgs, @@ -1168,10 +1119,10 @@ 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)); @@ -1183,7 +1134,7 @@ private static MethodDto createFieldConsumerWithBuilder( 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); additionalArguments.forEach(methodDto::addArgument); @@ -1196,7 +1147,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; } From 3f28683300cb757dc1eb6dbbda1bb8c44924be0a Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Tue, 23 Dec 2025 11:39:49 +0100 Subject: [PATCH 03/19] Adding new functionality to detect lists, sets and maps --- .../util/BuilderDefinitionCreator.java | 12 +-- .../processor/util/TypeNameAnalyser.java | 84 +++++++++++-------- 2 files changed, 57 insertions(+), 39 deletions(-) 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 e1bf2171..bb2c8d47 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 @@ -329,7 +329,7 @@ private static void addAdditionalHelperMethodsForField( List innerTypes = fieldTypeGeneric.getInnerTypeArguments(); int innerTypesCnt = innerTypes.size(); - if (isList(field.getFieldType()) && innerTypesCnt == 1) { + if (isListLike(field.getFieldType()) && innerTypesCnt == 1) { // Only add varargs helper if enabled in configuration if (context.getConfiguration().shouldGenerateVarArgsHelpers()) { String fieldName = field.getFieldNameEstimated(); @@ -344,7 +344,7 @@ private static void addAdditionalHelperMethodsForField( context); field.addMethod(method); } - } else if (isSet(field.getFieldType()) && innerTypesCnt == 1) { + } else if (isSetLike(field.getFieldType()) && innerTypesCnt == 1) { // Only add varargs helper if enabled in configuration if (context.getConfiguration().shouldGenerateVarArgsHelpers()) { String fieldName = field.getFieldNameEstimated(); @@ -359,7 +359,7 @@ private static void addAdditionalHelperMethodsForField( context); field.addMethod(method); } - } else if (isMap(field.getFieldType()) && innerTypesCnt == 2) { + } else if (isMapLike(field.getFieldType()) && innerTypesCnt == 2) { // Only add varargs helper if enabled in configuration if (context.getConfiguration().shouldGenerateVarArgsHelpers()) { TypeName mapEntryType = @@ -521,7 +521,7 @@ private static boolean tryAddListConsumer( VariableElement fieldParameter, TypeName builderType, ProcessingContext context) { - if (!(isList(field.getFieldType()) + if (!(isListLike(field.getFieldType()) && field.getFieldType() instanceof TypeNameGeneric fieldTypeGeneric && fieldTypeGeneric.getInnerTypeArguments().size() == 1)) { return false; @@ -577,7 +577,7 @@ private static boolean tryAddMapConsumer( if (!context.getConfiguration().shouldUseHashMapBuilder()) { return false; } - if (!(isMap(field.getFieldType()) + if (!(isMapLike(field.getFieldType()) && field.getFieldType() instanceof TypeNameGeneric fieldTypeGeneric && fieldTypeGeneric.getInnerTypeArguments().size() == 2)) { return false; @@ -600,7 +600,7 @@ private static boolean tryAddSetConsumer( VariableElement fieldParameter, TypeName builderType, ProcessingContext context) { - if (!(isSet(field.getFieldType()) + if (!(isSetLike(field.getFieldType()) && field.getFieldType() instanceof TypeNameGeneric fieldTypeGeneric && fieldTypeGeneric.getInnerTypeArguments().size() == 1)) { return false; 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..f5728f1f 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,55 @@ && isOptional(fieldType) } return false; } + + /** + * Helper to check if the type is a list-like collection (List or any List implementation). + * + * @param typeName Type to be validated + * @return {@code true}, if it is a List or List implementation + */ + public static boolean isListLike(TypeName typeName) { + if (!Strings.CI.equals(typeName.getPackageName(), JAVA_UTIL_PACKAGE)) { + return false; + } + String className = typeName.getClassName(); + return Strings.CI.equalsAny(className, "List", "ArrayList", "LinkedList", "Vector", "Stack"); + } + + /** + * Helper to check if the type is a set-like collection (Set or any Set implementation). + * + * @param typeName Type to be validated + * @return {@code true}, if it is a Set or Set implementation + */ + public static boolean isSetLike(TypeName typeName) { + if (!Strings.CI.equals(typeName.getPackageName(), JAVA_UTIL_PACKAGE)) { + return false; + } + String className = typeName.getClassName(); + return Strings.CI.equalsAny( + className, "Set", "HashSet", "LinkedHashSet", "TreeSet", "SortedSet", "NavigableSet"); + } + + /** + * Helper to check if the type is a map-like collection (Map or any Map implementation). + * + * @param typeName Type to be validated + * @return {@code true}, if it is a Map or Map implementation + */ + public static boolean isMapLike(TypeName typeName) { + if (!Strings.CI.equals(typeName.getPackageName(), JAVA_UTIL_PACKAGE)) { + return false; + } + String className = typeName.getClassName(); + return Strings.CI.equalsAny( + className, + "Map", + "HashMap", + "LinkedHashMap", + "TreeMap", + "SortedMap", + "NavigableMap", + "Hashtable"); + } } From 7905046d309b1c3c5cc7a124a7fb5231fee7ae58 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Thu, 25 Dec 2025 21:52:02 +0100 Subject: [PATCH 04/19] Refactoring code to support wrapping of special collectiontypes --- .../util/BuilderDefinitionCreator.java | 146 +++++++++++++----- 1 file changed, 108 insertions(+), 38 deletions(-) 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 bb2c8d47..4c27427f 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 @@ -332,50 +332,29 @@ private static void addAdditionalHelperMethodsForField( if (isListLike(field.getFieldType()) && innerTypesCnt == 1) { // 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); + createFieldSetterForCollectionType( + field, new TypeNameArray(innerTypes.get(0), false), builderType, context); field.addMethod(method); } } else if (isSetLike(field.getFieldType()) && innerTypesCnt == 1) { // 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); + createFieldSetterForCollectionType( + field, new TypeNameArray(innerTypes.get(0), true), builderType, context); field.addMethod(method); } } else if (isMapLike(field.getFieldType()) && innerTypesCnt == 2) { // 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)), + 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); + createFieldSetterForCollectionType(field, mapEntryType, builderType, context); field.addMethod(method); } } else if (isOptional(field.getFieldType()) && innerTypesCnt == 1) { @@ -589,7 +568,7 @@ private static boolean tryAddMapConsumer( fieldTypeGeneric.getInnerTypeArguments().get(0), fieldTypeGeneric.getInnerTypeArguments().get(1)); MethodDto mapConsumerWithBuilder = - createFieldConsumerWithBuilderForMap(field, builderTargetTypeName, builderType, context); + createFieldConsumerWithBuilder(field, builderTargetTypeName, builderType, context); field.addMethod(mapConsumerWithBuilder); return true; } @@ -909,6 +888,95 @@ 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 createFieldSetterForCollectionType( + FieldDto field, TypeName parameterType, TypeName builderType, ProcessingContext context) { + String transform = calculateCollectionTransform(field.getFieldType()); + 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) { + String className = fieldType.getClassName(); + + // Only wrap concrete collection implementations, Interfaces should not be wrapped + if ((isListLike(fieldType) || isSetLike(fieldType) || isMapLike(fieldType)) + && !Strings.CI.equalsAny(className, "List", "Set", "Map")) { + return "new " + className + "<>(" + baseExpression + ")"; + } + + return baseExpression; + } + + /** + * Calculates the transform expression for collection varargs helpers. Uses immutable collection + * factory methods and wraps with specific collection constructors when needed. + * + * @param fieldType the original field type + * @return the transform expression with %s placeholder for the value + */ + private static String calculateCollectionTransform(TypeName fieldType) { + String baseExpression; + if (isListLike(fieldType)) { + baseExpression = "java.util.List.of(%s)"; + } else if (isSetLike(fieldType)) { + baseExpression = "java.util.Set.of(%s)"; + } else if (isMapLike(fieldType)) { + baseExpression = "java.util.Map.ofEntries(%s)"; + } else { + return "%s"; + } + return wrapConcreteCollectionType(fieldType, baseExpression); + } + + /** + * Calculates the build expression wrapper for builder consumers. + * + * @param fieldType the original field type + * @param baseExpression the base expression to wrap (e.g., "builder.build()") + * @return the wrapped expression or the base expression if no wrapping needed + */ + private static String calculateBuildExpression(TypeName fieldType, String baseExpression) { + return wrapConcreteCollectionType(fieldType, baseExpression); + } + /** * Creates a field setter method with optional transform and annotations. * @@ -1055,10 +1123,7 @@ private static MethodDto createFieldConsumerWithBuilder( TypeNameGeneric builderTypeGeneric = new TypeNameGeneric(consumerBuilderType, builderTargetType); return BuilderDefinitionCreator.createFieldConsumerWithBuilder( - field, - builderTypeGeneric, - returnBuilderType, - context); + field, builderTypeGeneric, returnBuilderType, context); } private static MethodDto createFieldConsumerWithBuilder( @@ -1099,13 +1164,14 @@ 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( @@ -1126,14 +1192,18 @@ private static MethodDto createFieldConsumerWithBuilder( 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(), "builder.build()"); + 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(%s); return this; """ - .formatted(constructorArgsWithValue, additionalConstructorArgs)); + .formatted(constructorArgsWithValue, additionalConstructorArgs, buildExpression)); methodDto.addArgument(ARG_FIELD_NAME, field.getFieldName()); methodDto.addArgument(ARG_DTO_METHOD_PARAM, parameter.getParameterName()); methodDto.addArgument(ARG_HELPER_TYPE, consumerBuilderType); From 6edd3b4a6d8e65988520b21e440b0208fe084669 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Thu, 25 Dec 2025 22:33:00 +0100 Subject: [PATCH 05/19] Refactoring createFieldSetterForCollectionType to support simple list without package too --- .../util/BuilderDefinitionCreator.java | 45 +++++++++---------- .../processor/BuilderProcessorTest.java | 12 ++--- 2 files changed, 27 insertions(+), 30 deletions(-) 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 4c27427f..7ae50149 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 @@ -901,7 +901,24 @@ private static MethodDto createFieldSetterWithTransform( */ private static MethodDto createFieldSetterForCollectionType( FieldDto field, TypeName parameterType, TypeName builderType, ProcessingContext context) { - String transform = calculateCollectionTransform(field.getFieldType()); + String baseExpression; + TypeName fieldType = field.getFieldType(); + String className = fieldType.getClassName(); + + // Use simple names for interface types (already imported), fully qualified for concrete types + if (isListLike(fieldType)) { + baseExpression = + Strings.CI.equals(className, "List") ? "List.of(%s)" : "java.util.List.of(%s)"; + } else if (isSetLike(fieldType)) { + baseExpression = Strings.CI.equals(className, "Set") ? "Set.of(%s)" : "java.util.Set.of(%s)"; + } else if (isMapLike(fieldType)) { + baseExpression = + Strings.CI.equals(className, "Map") ? "Map.ofEntries(%s)" : "java.util.Map.ofEntries(%s)"; + } else { + return null; + } + String transform = wrapConcreteCollectionType(fieldType, baseExpression); + return createFieldSetterWithTransform( field.getFieldNameEstimated(), field.getFieldName(), @@ -945,27 +962,6 @@ private static String wrapConcreteCollectionType(TypeName fieldType, String base return baseExpression; } - /** - * Calculates the transform expression for collection varargs helpers. Uses immutable collection - * factory methods and wraps with specific collection constructors when needed. - * - * @param fieldType the original field type - * @return the transform expression with %s placeholder for the value - */ - private static String calculateCollectionTransform(TypeName fieldType) { - String baseExpression; - if (isListLike(fieldType)) { - baseExpression = "java.util.List.of(%s)"; - } else if (isSetLike(fieldType)) { - baseExpression = "java.util.Set.of(%s)"; - } else if (isMapLike(fieldType)) { - baseExpression = "java.util.Map.ofEntries(%s)"; - } else { - return "%s"; - } - return wrapConcreteCollectionType(fieldType, baseExpression); - } - /** * Calculates the build expression wrapper for builder consumers. * @@ -1200,13 +1196,14 @@ private static MethodDto createFieldConsumerWithBuilder( """ $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(%s); + this.$fieldName:N = $builderFieldWrapper:T.changedValue($buildExpression:N); return this; """ - .formatted(constructorArgsWithValue, additionalConstructorArgs, buildExpression)); + .formatted(constructorArgsWithValue, additionalConstructorArgs)); 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); 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 423ee0f9..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 @@ -567,12 +567,12 @@ void shouldGenerateCollectionSettersAndProviders() { "public WithCollectionsBuilder names(List names)", "public WithCollectionsBuilder names(Supplier> namesSupplier)", "public WithCollectionsBuilder names(String... names)", - "this.names = changedValue(java.util.List.of(names));", + "this.names = changedValue(List.of(names));", "private TrackedValue> names = unsetValue();", "public WithCollectionsBuilder tags(Set tags)", "public WithCollectionsBuilder tags(Supplier> tagsSupplier)", "public WithCollectionsBuilder tags(String... tags)", - "this.tags = changedValue(java.util.Set.of(tags));", + "this.tags = changedValue(Set.of(tags));", "private TrackedValue> tags = unsetValue();", "public WithCollectionsBuilder map(Map map)", "public WithCollectionsBuilder map(Supplier> mapSupplier)", @@ -1506,7 +1506,7 @@ void shouldHandleSetOfStrings() { "public HasSetStringBuilder tags(Consumer> tagsBuilderConsumer)", "this.tags = changedValue(builder.build());", "public HasSetStringBuilder tags(String... tags)", - "this.tags = changedValue(java.util.Set.of(tags));", + "this.tags = changedValue(Set.of(tags));", "public HasSetStringBuilder tags(Supplier> tagsSupplier)", "this.tags = changedValue(tagsSupplier.get());"); } @@ -1551,7 +1551,7 @@ public Helper() {} "public HasSetCustomBuilder helpers(Consumer> helpersBuilderConsumer)", "this.helpers = changedValue(builder.build());", "public HasSetCustomBuilder helpers(Helper... helpers)", - "this.helpers = changedValue(java.util.Set.of(helpers));", + "this.helpers = changedValue(Set.of(helpers));", "public HasSetCustomBuilder helpers(Supplier> helpersSupplier)", "this.helpers = changedValue(helpersSupplier.get());"); } @@ -1810,8 +1810,8 @@ void shouldGenerateVarargsConvenienceForCollections() { assertGenerationSucceeded(compilation, builderClassName, generatedCode); ProcessorAsserts.assertContaining( generatedCode, - "public HasCollectionsConvenienceBuilder names(String... names) { this.names = changedValue(java.util.List.of(names));", - "public HasCollectionsConvenienceBuilder tags(String... tags) { this.tags = changedValue(java.util.Set.of(tags));"); + "public HasCollectionsConvenienceBuilder names(String... names) { this.names = changedValue(List.of(names));", + "public HasCollectionsConvenienceBuilder tags(String... tags) { this.tags = changedValue(Set.of(tags));"); } @Test From d9c4301543ac0b73681ac47ce08751046f795b33 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Thu, 25 Dec 2025 22:51:33 +0100 Subject: [PATCH 06/19] Removing unused parameter fillsSet / isSet --- .../builders/processor/dtos/TypeNameArray.java | 18 ++---------------- .../util/BuilderDefinitionCreator.java | 10 +++++----- .../processor/util/JavaLangMapper.java | 2 +- 3 files changed, 8 insertions(+), 22 deletions(-) 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/util/BuilderDefinitionCreator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderDefinitionCreator.java index 7ae50149..699f1bdb 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 @@ -334,7 +334,7 @@ private static void addAdditionalHelperMethodsForField( if (context.getConfiguration().shouldGenerateVarArgsHelpers()) { MethodDto method = createFieldSetterForCollectionType( - field, new TypeNameArray(innerTypes.get(0), false), builderType, context); + field, new TypeNameArray(innerTypes.get(0)), builderType, context); field.addMethod(method); } } else if (isSetLike(field.getFieldType()) && innerTypesCnt == 1) { @@ -342,7 +342,7 @@ private static void addAdditionalHelperMethodsForField( if (context.getConfiguration().shouldGenerateVarArgsHelpers()) { MethodDto method = createFieldSetterForCollectionType( - field, new TypeNameArray(innerTypes.get(0), true), builderType, context); + field, new TypeNameArray(innerTypes.get(0)), builderType, context); field.addMethod(method); } } else if (isMapLike(field.getFieldType()) && innerTypesCnt == 2) { @@ -351,8 +351,8 @@ private static void addAdditionalHelperMethodsForField( // 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); + new TypeNameGeneric( + "java.util.Map", "Entry", innerTypes.get(0), innerTypes.get(1))); MethodDto method = createFieldSetterForCollectionType(field, mapEntryType, builderType, context); field.addMethod(method); @@ -1272,7 +1272,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..fe69fd70 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 @@ -190,7 +190,7 @@ public TypeName visitDeclared(DeclaredType t, Void p) { @Override public TypeNameArray visitArray(ArrayType t, Void p) { - return new TypeNameArray(extractType(t.getComponentType(), context), false); + return new TypeNameArray(extractType(t.getComponentType(), context)); } @Override From ecc06f198ee45abe6e03b0fad8938591d1857455 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Thu, 25 Dec 2025 23:38:21 +0100 Subject: [PATCH 07/19] Refactoring code to have Lists/Maps/Sets in a specific TypeName --- .../builders/processor/dtos/TypeNameList.java | 149 ++++++++++++++++ .../builders/processor/dtos/TypeNameMap.java | 163 ++++++++++++++++++ .../builders/processor/dtos/TypeNameSet.java | 149 ++++++++++++++++ .../util/BuilderDefinitionCreator.java | 62 ++++--- .../processor/util/JavaLangMapper.java | 55 +++++- .../processor/util/JavapoetMapper.java | 4 + .../processor/util/ProcessingContext.java | 47 +++++ .../processor/util/TypeNameAnalyser.java | 52 +----- 8 files changed, 600 insertions(+), 81 deletions(-) create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/TypeNameList.java create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/TypeNameMap.java create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/TypeNameSet.java 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..81b51c5e --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/TypeNameList.java @@ -0,0 +1,149 @@ +/* + * 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: + * + *

    + *
  • {@code List} -> TypeNameList with 1 inner type argument + *
  • {@code ArrayList} -> TypeNameList with 1 inner type argument + *
  • {@code List} (raw type) -> TypeNameList with 0 inner type arguments + *
+ */ +public class TypeNameList extends TypeNameGeneric { + + private final boolean isConcreteImplementation; + + /** + * 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 (should be 0 or 1 for List) + */ + public TypeNameList(TypeName outerType, List innerTypeArguments) { + super(outerType, innerTypeArguments); + this.isConcreteImplementation = !isListInterface(getPackageName(), getClassName()); + } + + /** + * Creates a {@code TypeNameList} for the given package/class and a list of inner type arguments. + * + * @param packageName the package name + * @param className the class name (the concrete List implementation) + * @param innerTypeArguments the list of generic type arguments (should be 0 or 1 for List) + */ + public TypeNameList(String packageName, String className, List innerTypeArguments) { + super(packageName, className, innerTypeArguments); + this.isConcreteImplementation = !isListInterface(packageName, className); + } + + /** + * Varargs convenience constructor with an outer {@code TypeName} and any number of inner type + * arguments. + * + * @param outerType the outer type to use for package and class name (the concrete List + * implementation) + * @param innerTypeArguments variable number of generic type arguments (should be 0 or 1 for List) + */ + public TypeNameList(TypeName outerType, TypeName... innerTypeArguments) { + super(outerType, innerTypeArguments); + this.isConcreteImplementation = !isListInterface(getPackageName(), getClassName()); + } + + /** + * Varargs convenience constructor with package/class names and any number of inner type + * arguments. + * + * @param packageName the package name + * @param className the class name (the concrete List implementation) + * @param innerTypeArguments variable number of generic type arguments (should be 0 or 1 for List) + */ + public TypeNameList(String packageName, String className, TypeName... innerTypeArguments) { + super(packageName, className, innerTypeArguments); + this.isConcreteImplementation = !isListInterface(packageName, className); + } + + /** + * 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 exactly 1 type argument (the element type). A raw List has 0 type + * arguments. + * + * @return {@code true} if this List has exactly 1 type argument + */ + public boolean isParameterized() { + return getInnerTypeArguments().size() == 1; + } + + /** + * Gets the element type of this List. + * + *

For a parameterized List like {@code List}, this returns the String type. + * + * @return the element type (the single type argument) + * @throws IllegalStateException if this is a raw List with no type arguments + */ + public TypeName getElementType() { + if (!isParameterized()) { + throw new IllegalStateException( + "Cannot get element type from raw List type: " + getClassName()); + } + return getInnerTypeArguments().get(0); + } +} 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..56f2e404 --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/TypeNameMap.java @@ -0,0 +1,163 @@ +/* + * 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; + + /** + * 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 (should be 0 or 2 for Map) + */ + public TypeNameMap(TypeName outerType, List innerTypeArguments) { + super(outerType, innerTypeArguments); + this.isConcreteImplementation = !isMapInterface(getPackageName(), getClassName()); + } + + /** + * Creates a {@code TypeNameMap} for the given package/class and a list of inner type arguments. + * + * @param packageName the package name + * @param className the class name (the concrete Map implementation) + * @param innerTypeArguments the list of generic type arguments (should be 0 or 2 for Map) + */ + public TypeNameMap(String packageName, String className, List innerTypeArguments) { + super(packageName, className, innerTypeArguments); + this.isConcreteImplementation = !isMapInterface(packageName, className); + } + + /** + * Varargs convenience constructor with an outer {@code TypeName} and any number of inner type + * arguments. + * + * @param outerType the outer type to use for package and class name (the concrete Map + * implementation) + * @param innerTypeArguments variable number of generic type arguments (should be 0 or 2 for Map) + */ + public TypeNameMap(TypeName outerType, TypeName... innerTypeArguments) { + super(outerType, innerTypeArguments); + this.isConcreteImplementation = !isMapInterface(getPackageName(), getClassName()); + } + + /** + * Varargs convenience constructor with package/class names and any number of inner type + * arguments. + * + * @param packageName the package name + * @param className the class name (the concrete Map implementation) + * @param innerTypeArguments variable number of generic type arguments (should be 0 or 2 for Map) + */ + public TypeNameMap(String packageName, String className, TypeName... innerTypeArguments) { + super(packageName, className, innerTypeArguments); + this.isConcreteImplementation = !isMapInterface(packageName, className); + } + + /** + * 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 getInnerTypeArguments().size() == 2; + } + + /** + * 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 (!isParameterized()) { + throw new IllegalStateException("Cannot get key type from raw Map type: " + getClassName()); + } + return getInnerTypeArguments().get(0); + } + + /** + * 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 (!isParameterized()) { + throw new IllegalStateException("Cannot get value type from raw Map type: " + getClassName()); + } + return getInnerTypeArguments().get(1); + } +} 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..1596c466 --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/TypeNameSet.java @@ -0,0 +1,149 @@ +/* + * 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; + + /** + * 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 (should be 0 or 1 for Set) + */ + public TypeNameSet(TypeName outerType, List innerTypeArguments) { + super(outerType, innerTypeArguments); + this.isConcreteImplementation = !isSetInterface(getPackageName(), getClassName()); + } + + /** + * Creates a {@code TypeNameSet} for the given package/class and a list of inner type arguments. + * + * @param packageName the package name + * @param className the class name (the concrete Set implementation) + * @param innerTypeArguments the list of generic type arguments (should be 0 or 1 for Set) + */ + public TypeNameSet(String packageName, String className, List innerTypeArguments) { + super(packageName, className, innerTypeArguments); + this.isConcreteImplementation = !isSetInterface(packageName, className); + } + + /** + * Varargs convenience constructor with an outer {@code TypeName} and any number of inner type + * arguments. + * + * @param outerType the outer type to use for package and class name (the concrete Set + * implementation) + * @param innerTypeArguments variable number of generic type arguments (should be 0 or 1 for Set) + */ + public TypeNameSet(TypeName outerType, TypeName... innerTypeArguments) { + super(outerType, innerTypeArguments); + this.isConcreteImplementation = !isSetInterface(getPackageName(), getClassName()); + } + + /** + * Varargs convenience constructor with package/class names and any number of inner type + * arguments. + * + * @param packageName the package name + * @param className the class name (the concrete Set implementation) + * @param innerTypeArguments variable number of generic type arguments (should be 0 or 1 for Set) + */ + public TypeNameSet(String packageName, String className, TypeName... innerTypeArguments) { + super(packageName, className, innerTypeArguments); + this.isConcreteImplementation = !isSetInterface(packageName, className); + } + + /** + * 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 getInnerTypeArguments().size() == 1; + } + + /** + * 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 (!isParameterized()) { + throw new IllegalStateException( + "Cannot get element type from raw Set type: " + getClassName()); + } + return getInnerTypeArguments().get(0); + } +} 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 699f1bdb..41187145 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,36 +328,35 @@ private static void addAdditionalHelperMethodsForField( } List innerTypes = fieldTypeGeneric.getInnerTypeArguments(); - int innerTypesCnt = innerTypes.size(); - if (isListLike(field.getFieldType()) && innerTypesCnt == 1) { + if (field.getFieldType() instanceof TypeNameList listType && listType.isParameterized()) { // Only add varargs helper if enabled in configuration if (context.getConfiguration().shouldGenerateVarArgsHelpers()) { MethodDto method = createFieldSetterForCollectionType( - field, new TypeNameArray(innerTypes.get(0)), builderType, context); + field, new TypeNameArray(listType.getElementType()), builderType, context); field.addMethod(method); } - } else if (isSetLike(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()) { MethodDto method = createFieldSetterForCollectionType( - field, new TypeNameArray(innerTypes.get(0)), builderType, context); + field, new TypeNameArray(setType.getElementType()), builderType, context); field.addMethod(method); } - } else if (isMapLike(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))); + "java.util.Map", "Entry", mapType.getKeyType(), mapType.getValueType())); MethodDto method = createFieldSetterForCollectionType(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 @@ -500,13 +499,12 @@ private static boolean tryAddListConsumer( VariableElement fieldParameter, TypeName builderType, ProcessingContext context) { - if (!(isListLike(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,17 +554,16 @@ private static boolean tryAddMapConsumer( if (!context.getConfiguration().shouldUseHashMapBuilder()) { return false; } - if (!(isMapLike(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 = createFieldConsumerWithBuilder(field, builderTargetTypeName, builderType, context); field.addMethod(mapConsumerWithBuilder); @@ -579,13 +576,12 @@ private static boolean tryAddSetConsumer( VariableElement fieldParameter, TypeName builderType, ProcessingContext context) { - if (!(isSetLike(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(); @@ -903,17 +899,16 @@ private static MethodDto createFieldSetterForCollectionType( FieldDto field, TypeName parameterType, TypeName builderType, ProcessingContext context) { String baseExpression; TypeName fieldType = field.getFieldType(); - String className = fieldType.getClassName(); // Use simple names for interface types (already imported), fully qualified for concrete types - if (isListLike(fieldType)) { + if (fieldType instanceof TypeNameList listType) { baseExpression = - Strings.CI.equals(className, "List") ? "List.of(%s)" : "java.util.List.of(%s)"; - } else if (isSetLike(fieldType)) { - baseExpression = Strings.CI.equals(className, "Set") ? "Set.of(%s)" : "java.util.Set.of(%s)"; - } else if (isMapLike(fieldType)) { + 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 = - Strings.CI.equals(className, "Map") ? "Map.ofEntries(%s)" : "java.util.Map.ofEntries(%s)"; + mapType.isConcreteImplementation() ? "java.util.Map.ofEntries(%s)" : "Map.ofEntries(%s)"; } else { return null; } @@ -951,12 +946,13 @@ private static MethodDto createFieldSetterForCollectionType( * @return the wrapped expression for concrete collections, or base expression otherwise */ private static String wrapConcreteCollectionType(TypeName fieldType, String baseExpression) { - String className = fieldType.getClassName(); - // Only wrap concrete collection implementations, Interfaces should not be wrapped - if ((isListLike(fieldType) || isSetLike(fieldType) || isMapLike(fieldType)) - && !Strings.CI.equalsAny(className, "List", "Set", "Map")) { - return "new " + className + "<>(" + baseExpression + ")"; + 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; 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 fe69fd70..8718858f 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,54 @@ 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. + * + * @param rawType the base TypeName (preserves concrete class information) + * @param argTypes the generic type arguments + * @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) { + TypeElement listElement = context.getTypeElement("java.util.List"); + TypeElement setElement = context.getTypeElement("java.util.Set"); + TypeElement mapElement = context.getTypeElement("java.util.Map"); + + if (listElement != null) { + TypeMirror listType = context.erasure(listElement.asType()); + if (context.isAssignable(context.erasure(typeMirror), listType)) { + return argTypes.isEmpty() + ? new TypeNameList(rawType, argTypes) + : new TypeNameList(rawType, argTypes); + } + } + + if (setElement != null) { + TypeMirror setType = context.erasure(setElement.asType()); + if (context.isAssignable(context.erasure(typeMirror), setType)) { + return argTypes.isEmpty() + ? new TypeNameSet(rawType, argTypes) + : new TypeNameSet(rawType, argTypes); + } + } + + if (mapElement != null) { + TypeMirror mapType = context.erasure(mapElement.asType()); + if (context.isAssignable(context.erasure(typeMirror), mapType)) { + return argTypes.isEmpty() + ? new TypeNameMap(rawType, argTypes) + : new TypeNameMap(rawType, argTypes); + } + } + + // Not a collection type - return generic or raw type + return argTypes.isEmpty() ? rawType : new TypeNameGeneric(rawType, argTypes); + } + private static TypeName extractType(TypeMirror typeOfParameter, ProcessingContext context) { return typeOfParameter.accept( new SimpleTypeVisitor14() { @@ -175,16 +223,15 @@ 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); } } 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..e9f4a036 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 @@ -28,6 +28,7 @@ import javax.lang.model.element.Element; import javax.lang.model.element.PackageElement; import javax.lang.model.element.TypeElement; +import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Elements; import javax.lang.model.util.Types; @@ -150,6 +151,52 @@ 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 type of a member when viewed as a member of a given type. + * + *

This is useful for resolving type parameters. For example, if you have {@code + * ArrayList} and want to know what type {@code List.get(int)} returns, this method + * resolves it to {@code String}. + * + * @param containing the containing type + * @param element the member element + * @return the type of the member as viewed from the containing type + */ + public TypeMirror asMemberOf(DeclaredType containing, javax.lang.model.element.Element element) { + return typeUtils.asMemberOf(containing, element); + } + + /** + * 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 f5728f1f..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 @@ -83,53 +83,17 @@ && isOptional(fieldType) } /** - * Helper to check if the type is a list-like collection (List or any List implementation). + * Checks if an Optional type is properly parameterized (not a raw type). * - * @param typeName Type to be validated - * @return {@code true}, if it is a List or List implementation - */ - public static boolean isListLike(TypeName typeName) { - if (!Strings.CI.equals(typeName.getPackageName(), JAVA_UTIL_PACKAGE)) { - return false; - } - String className = typeName.getClassName(); - return Strings.CI.equalsAny(className, "List", "ArrayList", "LinkedList", "Vector", "Stack"); - } - - /** - * Helper to check if the type is a set-like collection (Set or any Set implementation). - * - * @param typeName Type to be validated - * @return {@code true}, if it is a Set or Set implementation - */ - public static boolean isSetLike(TypeName typeName) { - if (!Strings.CI.equals(typeName.getPackageName(), JAVA_UTIL_PACKAGE)) { - return false; - } - String className = typeName.getClassName(); - return Strings.CI.equalsAny( - className, "Set", "HashSet", "LinkedHashSet", "TreeSet", "SortedSet", "NavigableSet"); - } - - /** - * Helper to check if the type is a map-like collection (Map or any Map implementation). + *

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 is a Map or Map implementation + * @return {@code true} if it's an Optional with exactly 1 type argument */ - public static boolean isMapLike(TypeName typeName) { - if (!Strings.CI.equals(typeName.getPackageName(), JAVA_UTIL_PACKAGE)) { - return false; - } - String className = typeName.getClassName(); - return Strings.CI.equalsAny( - className, - "Map", - "HashMap", - "LinkedHashMap", - "TreeMap", - "SortedMap", - "NavigableMap", - "Hashtable"); + public static boolean isParameterizedOptional(TypeName typeName) { + return typeName instanceof TypeNameGeneric genericType + && isOptional(typeName) + && genericType.getInnerTypeArguments().size() == 1; } } From 4cd8ed7d8b49b8dee31235c6713bc53c0f33481f Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Fri, 26 Dec 2025 00:16:13 +0100 Subject: [PATCH 08/19] Extracting the elementTypes of collections from interface-implementation and not from generic type directly --- .../builders/processor/dtos/TypeNameList.java | 40 +++- .../builders/processor/dtos/TypeNameMap.java | 38 +++- .../builders/processor/dtos/TypeNameSet.java | 26 ++- .../util/BuilderDefinitionCreator.java | 3 +- .../processor/util/JavaLangMapper.java | 192 +++++++++++++++- .../processor/CustomCollectionTypeTest.java | 206 ++++++++++++++++++ 6 files changed, 479 insertions(+), 26 deletions(-) create mode 100644 processor/src/test/java/org/javahelpers/simple/builders/processor/CustomCollectionTypeTest.java 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 index 81b51c5e..24faed61 100644 --- 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 @@ -46,6 +46,7 @@ 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. @@ -58,6 +59,21 @@ 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; + } + /** * Creates a {@code TypeNameList} based on another {@code TypeName} as outer type and a list of * inner type arguments. @@ -69,6 +85,7 @@ private static boolean isListInterface(String packageName, String className) { public TypeNameList(TypeName outerType, List innerTypeArguments) { super(outerType, innerTypeArguments); this.isConcreteImplementation = !isListInterface(getPackageName(), getClassName()); + this.elementType = innerTypeArguments.isEmpty() ? null : innerTypeArguments.get(0); } /** @@ -81,6 +98,7 @@ public TypeNameList(TypeName outerType, List innerTypeArguments) { public TypeNameList(String packageName, String className, List innerTypeArguments) { super(packageName, className, innerTypeArguments); this.isConcreteImplementation = !isListInterface(packageName, className); + this.elementType = innerTypeArguments.isEmpty() ? null : innerTypeArguments.get(0); } /** @@ -94,6 +112,7 @@ public TypeNameList(String packageName, String className, List innerTy public TypeNameList(TypeName outerType, TypeName... innerTypeArguments) { super(outerType, innerTypeArguments); this.isConcreteImplementation = !isListInterface(getPackageName(), getClassName()); + this.elementType = innerTypeArguments.length == 0 ? null : innerTypeArguments[0]; } /** @@ -107,6 +126,7 @@ public TypeNameList(TypeName outerType, TypeName... innerTypeArguments) { public TypeNameList(String packageName, String className, TypeName... innerTypeArguments) { super(packageName, className, innerTypeArguments); this.isConcreteImplementation = !isListInterface(packageName, className); + this.elementType = innerTypeArguments.length == 0 ? null : innerTypeArguments[0]; } /** @@ -122,13 +142,16 @@ public boolean isConcreteImplementation() { /** * Checks if this List type is properly parameterized (not a raw type). * - *

A parameterized List has exactly 1 type argument (the element type). A raw List has 0 type - * arguments. + *

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 exactly 1 type argument + * @return {@code true} if this List has an element type */ public boolean isParameterized() { - return getInnerTypeArguments().size() == 1; + return elementType != null; } /** @@ -136,14 +159,17 @@ public boolean isParameterized() { * *

For a parameterized List like {@code List}, this returns the String type. * - * @return the element type (the single type argument) + *

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 (!isParameterized()) { + if (elementType == null) { throw new IllegalStateException( "Cannot get element type from raw List type: " + getClassName()); } - return getInnerTypeArguments().get(0); + 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 index 56f2e404..32cb2ce5 100644 --- 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 @@ -46,6 +46,8 @@ 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. @@ -58,6 +60,24 @@ 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; + } + /** * Creates a {@code TypeNameMap} based on another {@code TypeName} as outer type and a list of * inner type arguments. @@ -69,6 +89,8 @@ private static boolean isMapInterface(String packageName, String className) { public TypeNameMap(TypeName outerType, List innerTypeArguments) { super(outerType, innerTypeArguments); this.isConcreteImplementation = !isMapInterface(getPackageName(), getClassName()); + this.keyType = innerTypeArguments.size() >= 1 ? innerTypeArguments.get(0) : null; + this.valueType = innerTypeArguments.size() >= 2 ? innerTypeArguments.get(1) : null; } /** @@ -81,6 +103,8 @@ public TypeNameMap(TypeName outerType, List innerTypeArguments) { public TypeNameMap(String packageName, String className, List innerTypeArguments) { super(packageName, className, innerTypeArguments); this.isConcreteImplementation = !isMapInterface(packageName, className); + this.keyType = innerTypeArguments.size() >= 1 ? innerTypeArguments.get(0) : null; + this.valueType = innerTypeArguments.size() >= 2 ? innerTypeArguments.get(1) : null; } /** @@ -94,6 +118,8 @@ public TypeNameMap(String packageName, String className, List innerTyp public TypeNameMap(TypeName outerType, TypeName... innerTypeArguments) { super(outerType, innerTypeArguments); this.isConcreteImplementation = !isMapInterface(getPackageName(), getClassName()); + this.keyType = innerTypeArguments.length >= 1 ? innerTypeArguments[0] : null; + this.valueType = innerTypeArguments.length >= 2 ? innerTypeArguments[1] : null; } /** @@ -107,6 +133,8 @@ public TypeNameMap(TypeName outerType, TypeName... innerTypeArguments) { public TypeNameMap(String packageName, String className, TypeName... innerTypeArguments) { super(packageName, className, innerTypeArguments); this.isConcreteImplementation = !isMapInterface(packageName, className); + this.keyType = innerTypeArguments.length >= 1 ? innerTypeArguments[0] : null; + this.valueType = innerTypeArguments.length >= 2 ? innerTypeArguments[1] : null; } /** @@ -128,7 +156,7 @@ public boolean isConcreteImplementation() { * @return {@code true} if this Map has exactly 2 type arguments */ public boolean isParameterized() { - return getInnerTypeArguments().size() == 2; + return keyType != null && valueType != null; } /** @@ -140,10 +168,10 @@ public boolean isParameterized() { * @throws IllegalStateException if this is a raw Map with no type arguments */ public TypeName getKeyType() { - if (!isParameterized()) { + if (keyType == null) { throw new IllegalStateException("Cannot get key type from raw Map type: " + getClassName()); } - return getInnerTypeArguments().get(0); + return keyType; } /** @@ -155,9 +183,9 @@ public TypeName getKeyType() { * @throws IllegalStateException if this is a raw Map with no type arguments */ public TypeName getValueType() { - if (!isParameterized()) { + if (valueType == null) { throw new IllegalStateException("Cannot get value type from raw Map type: " + getClassName()); } - return getInnerTypeArguments().get(1); + 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 index 1596c466..ae0e6afe 100644 --- 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 @@ -46,6 +46,7 @@ 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. @@ -58,6 +59,21 @@ 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; + } + /** * Creates a {@code TypeNameSet} based on another {@code TypeName} as outer type and a list of * inner type arguments. @@ -69,6 +85,7 @@ private static boolean isSetInterface(String packageName, String className) { public TypeNameSet(TypeName outerType, List innerTypeArguments) { super(outerType, innerTypeArguments); this.isConcreteImplementation = !isSetInterface(getPackageName(), getClassName()); + this.elementType = innerTypeArguments.isEmpty() ? null : innerTypeArguments.get(0); } /** @@ -81,6 +98,7 @@ public TypeNameSet(TypeName outerType, List innerTypeArguments) { public TypeNameSet(String packageName, String className, List innerTypeArguments) { super(packageName, className, innerTypeArguments); this.isConcreteImplementation = !isSetInterface(packageName, className); + this.elementType = innerTypeArguments.isEmpty() ? null : innerTypeArguments.get(0); } /** @@ -94,6 +112,7 @@ public TypeNameSet(String packageName, String className, List innerTyp public TypeNameSet(TypeName outerType, TypeName... innerTypeArguments) { super(outerType, innerTypeArguments); this.isConcreteImplementation = !isSetInterface(getPackageName(), getClassName()); + this.elementType = innerTypeArguments.length == 0 ? null : innerTypeArguments[0]; } /** @@ -107,6 +126,7 @@ public TypeNameSet(TypeName outerType, TypeName... innerTypeArguments) { public TypeNameSet(String packageName, String className, TypeName... innerTypeArguments) { super(packageName, className, innerTypeArguments); this.isConcreteImplementation = !isSetInterface(packageName, className); + this.elementType = innerTypeArguments.length == 0 ? null : innerTypeArguments[0]; } /** @@ -128,7 +148,7 @@ public boolean isConcreteImplementation() { * @return {@code true} if this Set has exactly 1 type argument */ public boolean isParameterized() { - return getInnerTypeArguments().size() == 1; + return elementType != null; } /** @@ -140,10 +160,10 @@ public boolean isParameterized() { * @throws IllegalStateException if this is a raw Set with no type arguments */ public TypeName getElementType() { - if (!isParameterized()) { + if (elementType == null) { throw new IllegalStateException( "Cannot get element type from raw Set type: " + getClassName()); } - return getInnerTypeArguments().get(0); + 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 41187145..7f456404 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 @@ -946,7 +946,8 @@ private static MethodDto createFieldSetterForCollectionType( * @return the wrapped expression for concrete collections, or base expression otherwise */ private static String wrapConcreteCollectionType(TypeName fieldType, String baseExpression) { - // Only wrap concrete collection implementations, Interfaces should not be wrapped + // 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()) { 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 8718858f..0d65719c 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 @@ -147,8 +147,12 @@ private static List extractTypeForList( * 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 + * @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 @@ -163,27 +167,70 @@ private static TypeName wrapInCollectionTypeIfApplicable( if (listElement != null) { TypeMirror listType = context.erasure(listElement.asType()); if (context.isAssignable(context.erasure(typeMirror), listType)) { - return argTypes.isEmpty() - ? new TypeNameList(rawType, argTypes) - : new TypeNameList(rawType, argTypes); + // Check if this is the List interface itself + TypeElement typeElement = (TypeElement) context.asElement(typeMirror); + boolean isInterface = typeElement.equals(listElement); + + // Check if type has a constructor accepting Collection (like ArrayList(Collection)) + boolean hasConstructor = !isInterface && hasCollectionConstructor(typeElement, context); + + if (isInterface || hasConstructor) { + // Extract the actual List element type from the interface + List interfaceTypeArgs = + extractInterfaceTypeArguments(typeMirror, listElement, context); + TypeName elementType = interfaceTypeArgs.isEmpty() ? null : interfaceTypeArgs.get(0); + // Store all class type parameters AND the extracted element type + return new TypeNameList(rawType, argTypes, elementType); + } + // Custom List implementation without Collection constructor - treat as generic type + return argTypes.isEmpty() ? rawType : new TypeNameGeneric(rawType, argTypes); } } if (setElement != null) { TypeMirror setType = context.erasure(setElement.asType()); if (context.isAssignable(context.erasure(typeMirror), setType)) { - return argTypes.isEmpty() - ? new TypeNameSet(rawType, argTypes) - : new TypeNameSet(rawType, argTypes); + // Check if this is the Set interface itself + TypeElement typeElement = (TypeElement) context.asElement(typeMirror); + boolean isInterface = typeElement.equals(setElement); + + // Check if type has a constructor accepting Collection (like HashSet(Collection)) + boolean hasConstructor = !isInterface && hasCollectionConstructor(typeElement, context); + + if (isInterface || hasConstructor) { + // Extract the actual Set element type from the interface + List interfaceTypeArgs = + extractInterfaceTypeArguments(typeMirror, setElement, context); + TypeName elementType = interfaceTypeArgs.isEmpty() ? null : interfaceTypeArgs.get(0); + // Store all class type parameters AND the extracted element type + return new TypeNameSet(rawType, argTypes, elementType); + } + // Custom Set implementation without Collection constructor - treat as generic type + return argTypes.isEmpty() ? rawType : new TypeNameGeneric(rawType, argTypes); } } if (mapElement != null) { TypeMirror mapType = context.erasure(mapElement.asType()); if (context.isAssignable(context.erasure(typeMirror), mapType)) { - return argTypes.isEmpty() - ? new TypeNameMap(rawType, argTypes) - : new TypeNameMap(rawType, argTypes); + // Check if this is the Map interface itself + TypeElement typeElement = (TypeElement) context.asElement(typeMirror); + boolean isInterface = typeElement.equals(mapElement); + + // Check if type has a constructor accepting Map (like HashMap(Map)) + boolean hasConstructor = !isInterface && hasMapConstructor(typeElement, context); + + if (isInterface || hasConstructor) { + // Extract the actual Map key and value types from the interface + List interfaceTypeArgs = + extractInterfaceTypeArguments(typeMirror, mapElement, context); + TypeName keyType = interfaceTypeArgs.isEmpty() ? null : interfaceTypeArgs.get(0); + TypeName valueType = interfaceTypeArgs.size() < 2 ? null : interfaceTypeArgs.get(1); + // Store all class type parameters AND the extracted key/value types + return new TypeNameMap(rawType, argTypes, keyType, valueType); + } + // Custom Map implementation without Map constructor - treat as generic type + return argTypes.isEmpty() ? rawType : new TypeNameGeneric(rawType, argTypes); } } @@ -191,6 +238,131 @@ private static TypeName wrapInCollectionTypeIfApplicable( return argTypes.isEmpty() ? rawType : new TypeNameGeneric(rawType, argTypes); } + /** + * Checks if a type has a constructor that accepts a Collection parameter. + * + *

This allows us to detect if we can generate helper methods for List/Set types by checking if + * it has a constructor like {@code ArrayList(Collection)}. + * + * @param typeElement the type to check + * @param context the processing context + * @return true if the type has a constructor accepting Collection + */ + private static boolean hasCollectionConstructor( + TypeElement typeElement, ProcessingContext context) { + TypeElement collectionElement = context.getTypeElement("java.util.Collection"); + if (collectionElement == null) { + return false; + } + + TypeMirror collectionType = context.erasure(collectionElement.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 paramType = constructor.getParameters().get(0).asType(); + TypeMirror erasedParamType = context.erasure(paramType); + // Parameter should be Collection or a supertype (like Iterable) + return context.isSameType(erasedParamType, collectionType) + || context.isAssignable(erasedParamType, collectionType); + }); + } + + /** + * Checks if a type has a constructor that accepts a Map parameter. + * + *

This allows us to detect if we can generate helper methods for Map types by checking if it + * has a constructor like {@code HashMap(Map)}. + * + * @param typeElement the type to check + * @param context the processing context + * @return true if the type has a constructor accepting Map + */ + private static boolean hasMapConstructor(TypeElement typeElement, ProcessingContext context) { + TypeElement mapElement = context.getTypeElement("java.util.Map"); + if (mapElement == null) { + return false; + } + + TypeMirror mapType = context.erasure(mapElement.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 paramType = constructor.getParameters().get(0).asType(); + TypeMirror erasedParamType = context.erasure(paramType); + // Parameter should be Map + return context.isSameType(erasedParamType, mapType) + || context.isAssignable(erasedParamType, mapType); + }); + } + + /** + * 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() { 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..8b13c2f3 --- /dev/null +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/CustomCollectionTypeTest.java @@ -0,0 +1,206 @@ +/* + * 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 com.google.testing.compile.JavaFileObjects; +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 = + JavaFileObjects.forSourceLines( + "test.CustomList", + "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 = + JavaFileObjects.forSourceLines( + "test.CustomSet", + "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 = + JavaFileObjects.forSourceLines( + "test.CustomMap", + "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)"); + } +} From e77607a9f2d4f7e253717bc94fa8a5580e3e90f8 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Fri, 26 Dec 2025 00:18:51 +0100 Subject: [PATCH 09/19] Adding a test for a class, where the number of generics is just 1 for a map (but reusing it multiple times for the HashMap) --- .../processor/CustomCollectionTypeTest.java | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) 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 index 8b13c2f3..23d184b1 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/CustomCollectionTypeTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/CustomCollectionTypeTest.java @@ -203,4 +203,50 @@ public CustomMap getData() { 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 = + JavaFileObjects.forSourceLines( + "test.SymmetricMap", + "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)"); + } } From cda0aff093708580927c8143fb87f65ed623aac2 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Fri, 26 Dec 2025 00:28:34 +0100 Subject: [PATCH 10/19] Extending tests for having tests on raw CollectionTypes too --- .../processor/CustomCollectionTypeTest.java | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) 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 index 23d184b1..1051d666 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/CustomCollectionTypeTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/CustomCollectionTypeTest.java @@ -249,4 +249,101 @@ public SymmetricMap getData() { 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)"); + } } From ed222e47f85c1816a5243152cffb2fbdebf29bc3 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Fri, 26 Dec 2025 00:35:37 +0100 Subject: [PATCH 11/19] Renaming functions for improving readability --- .../processor/util/BuilderDefinitionCreator.java | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) 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 7f456404..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 @@ -332,7 +332,7 @@ private static void addAdditionalHelperMethodsForField( // Only add varargs helper if enabled in configuration if (context.getConfiguration().shouldGenerateVarArgsHelpers()) { MethodDto method = - createFieldSetterForCollectionType( + createFieldSetterByVarArgs( field, new TypeNameArray(listType.getElementType()), builderType, context); field.addMethod(method); } @@ -340,7 +340,7 @@ private static void addAdditionalHelperMethodsForField( // Only add varargs helper if enabled in configuration if (context.getConfiguration().shouldGenerateVarArgsHelpers()) { MethodDto method = - createFieldSetterForCollectionType( + createFieldSetterByVarArgs( field, new TypeNameArray(setType.getElementType()), builderType, context); field.addMethod(method); } @@ -352,8 +352,7 @@ private static void addAdditionalHelperMethodsForField( new TypeNameArray( new TypeNameGeneric( "java.util.Map", "Entry", mapType.getKeyType(), mapType.getValueType())); - MethodDto method = - createFieldSetterForCollectionType(field, mapEntryType, builderType, context); + MethodDto method = createFieldSetterByVarArgs(field, mapEntryType, builderType, context); field.addMethod(method); } } else if (isParameterizedOptional(field.getFieldType())) { @@ -895,7 +894,7 @@ private static MethodDto createFieldSetterWithTransform( * @param context processing context * @return the method DTO for the setter */ - private static MethodDto createFieldSetterForCollectionType( + private static MethodDto createFieldSetterByVarArgs( FieldDto field, TypeName parameterType, TypeName builderType, ProcessingContext context) { String baseExpression; TypeName fieldType = field.getFieldType(); @@ -963,11 +962,10 @@ private static String wrapConcreteCollectionType(TypeName fieldType, String base * Calculates the build expression wrapper for builder consumers. * * @param fieldType the original field type - * @param baseExpression the base expression to wrap (e.g., "builder.build()") * @return the wrapped expression or the base expression if no wrapping needed */ - private static String calculateBuildExpression(TypeName fieldType, String baseExpression) { - return wrapConcreteCollectionType(fieldType, baseExpression); + private static String calculateBuildExpression(TypeName fieldType) { + return wrapConcreteCollectionType(fieldType, "builder.build()"); } /** @@ -1187,7 +1185,7 @@ private static MethodDto createFieldConsumerWithBuilder( setMethodAccessModifier(methodDto, getMethodAccessModifier(context)); // Wrap the builder result with a specific collection constructor if needed - String buildExpression = calculateBuildExpression(field.getFieldType(), "builder.build()"); + String buildExpression = calculateBuildExpression(field.getFieldType()); methodDto.setCode( """ From 191d38fceb227f902d270748e9c53571c52f6fe7 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Fri, 26 Dec 2025 00:43:43 +0100 Subject: [PATCH 12/19] Trying to fix codesmell issues --- .../builders/processor/dtos/TypeNameMap.java | 4 +- .../processor/util/JavaLangMapper.java | 190 ++++++++++++------ 2 files changed, 126 insertions(+), 68 deletions(-) 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 index 32cb2ce5..9d752a2e 100644 --- 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 @@ -89,7 +89,7 @@ public TypeNameMap( public TypeNameMap(TypeName outerType, List innerTypeArguments) { super(outerType, innerTypeArguments); this.isConcreteImplementation = !isMapInterface(getPackageName(), getClassName()); - this.keyType = innerTypeArguments.size() >= 1 ? innerTypeArguments.get(0) : null; + this.keyType = !innerTypeArguments.isEmpty() ? innerTypeArguments.get(0) : null; this.valueType = innerTypeArguments.size() >= 2 ? innerTypeArguments.get(1) : null; } @@ -103,7 +103,7 @@ public TypeNameMap(TypeName outerType, List innerTypeArguments) { public TypeNameMap(String packageName, String className, List innerTypeArguments) { super(packageName, className, innerTypeArguments); this.isConcreteImplementation = !isMapInterface(packageName, className); - this.keyType = innerTypeArguments.size() >= 1 ? innerTypeArguments.get(0) : null; + this.keyType = !innerTypeArguments.isEmpty() ? innerTypeArguments.get(0) : null; this.valueType = innerTypeArguments.size() >= 2 ? innerTypeArguments.get(1) : null; } 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 0d65719c..db818da3 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 @@ -160,81 +160,139 @@ private static List extractTypeForList( */ 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"); - TypeElement mapElement = context.getTypeElement("java.util.Map"); + if (setElement == null) { + return null; + } - if (listElement != null) { - TypeMirror listType = context.erasure(listElement.asType()); - if (context.isAssignable(context.erasure(typeMirror), listType)) { - // Check if this is the List interface itself - TypeElement typeElement = (TypeElement) context.asElement(typeMirror); - boolean isInterface = typeElement.equals(listElement); - - // Check if type has a constructor accepting Collection (like ArrayList(Collection)) - boolean hasConstructor = !isInterface && hasCollectionConstructor(typeElement, context); - - if (isInterface || hasConstructor) { - // Extract the actual List element type from the interface - List interfaceTypeArgs = - extractInterfaceTypeArguments(typeMirror, listElement, context); - TypeName elementType = interfaceTypeArgs.isEmpty() ? null : interfaceTypeArgs.get(0); - // Store all class type parameters AND the extracted element type - return new TypeNameList(rawType, argTypes, elementType); - } - // Custom List implementation without Collection constructor - treat as generic type - return argTypes.isEmpty() ? rawType : new TypeNameGeneric(rawType, argTypes); - } + TypeMirror setType = context.erasure(setElement.asType()); + if (!context.isAssignable(context.erasure(typeMirror), setType)) { + return null; } - if (setElement != null) { - TypeMirror setType = context.erasure(setElement.asType()); - if (context.isAssignable(context.erasure(typeMirror), setType)) { - // Check if this is the Set interface itself - TypeElement typeElement = (TypeElement) context.asElement(typeMirror); - boolean isInterface = typeElement.equals(setElement); - - // Check if type has a constructor accepting Collection (like HashSet(Collection)) - boolean hasConstructor = !isInterface && hasCollectionConstructor(typeElement, context); - - if (isInterface || hasConstructor) { - // Extract the actual Set element type from the interface - List interfaceTypeArgs = - extractInterfaceTypeArguments(typeMirror, setElement, context); - TypeName elementType = interfaceTypeArgs.isEmpty() ? null : interfaceTypeArgs.get(0); - // Store all class type parameters AND the extracted element type - return new TypeNameSet(rawType, argTypes, elementType); - } - // Custom Set implementation without Collection constructor - treat as generic type - return argTypes.isEmpty() ? rawType : new TypeNameGeneric(rawType, argTypes); - } + TypeElement typeElement = (TypeElement) context.asElement(typeMirror); + if (!shouldWrapAsCollectionType(typeElement, setElement, context)) { + return createFallbackTypeName(rawType, argTypes); } - if (mapElement != null) { - TypeMirror mapType = context.erasure(mapElement.asType()); - if (context.isAssignable(context.erasure(typeMirror), mapType)) { - // Check if this is the Map interface itself - TypeElement typeElement = (TypeElement) context.asElement(typeMirror); - boolean isInterface = typeElement.equals(mapElement); - - // Check if type has a constructor accepting Map (like HashMap(Map)) - boolean hasConstructor = !isInterface && hasMapConstructor(typeElement, context); - - if (isInterface || hasConstructor) { - // Extract the actual Map key and value types from the interface - List interfaceTypeArgs = - extractInterfaceTypeArguments(typeMirror, mapElement, context); - TypeName keyType = interfaceTypeArgs.isEmpty() ? null : interfaceTypeArgs.get(0); - TypeName valueType = interfaceTypeArgs.size() < 2 ? null : interfaceTypeArgs.get(1); - // Store all class type parameters AND the extracted key/value types - return new TypeNameMap(rawType, argTypes, keyType, valueType); - } - // Custom Map implementation without Map constructor - treat as generic type - return argTypes.isEmpty() ? rawType : new TypeNameGeneric(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; } - // Not a collection type - return generic or raw type + 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 && hasMapConstructor(typeElement, 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 && hasCollectionConstructor(typeElement, 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); } From 4b8462cb4afa1bdaa1c58725f9ef27b059a16e06 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Fri, 26 Dec 2025 00:47:38 +0100 Subject: [PATCH 13/19] Removing unused function --- .../processor/util/ProcessingContext.java | 15 --------------- 1 file changed, 15 deletions(-) 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 e9f4a036..c937c755 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 @@ -172,21 +172,6 @@ public boolean isAssignable(TypeMirror type1, TypeMirror type2) { return typeUtils.isAssignable(type1, type2); } - /** - * Returns the type of a member when viewed as a member of a given type. - * - *

This is useful for resolving type parameters. For example, if you have {@code - * ArrayList} and want to know what type {@code List.get(int)} returns, this method - * resolves it to {@code String}. - * - * @param containing the containing type - * @param element the member element - * @return the type of the member as viewed from the containing type - */ - public TypeMirror asMemberOf(DeclaredType containing, javax.lang.model.element.Element element) { - return typeUtils.asMemberOf(containing, element); - } - /** * Returns the direct supertypes of a type. * From a446c0f34d5c644a61972726320756df35729997 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Fri, 26 Dec 2025 00:49:24 +0100 Subject: [PATCH 14/19] Fixing codestyle --- .../simple/builders/processor/util/ProcessingContext.java | 1 - 1 file changed, 1 deletion(-) 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 c937c755..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 @@ -28,7 +28,6 @@ import javax.lang.model.element.Element; import javax.lang.model.element.PackageElement; import javax.lang.model.element.TypeElement; -import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Elements; import javax.lang.model.util.Types; From 62e0b895a9c87beb5ba83cda4d3c9c90f6d1f993 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 28 Dec 2025 17:09:59 +0100 Subject: [PATCH 15/19] Fixing codesmell in TypeNameGeneric, because the collection is already not able to be modified --- .../simple/builders/processor/dtos/TypeNameGeneric.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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; } /** From 05f3ed26835404fd1c43a82c675ffdeb4cd060fe Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 28 Dec 2025 17:20:05 +0100 Subject: [PATCH 16/19] Adding a test for unmodifiable Lists --- .../processor/CustomCollectionTypeTest.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) 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 index 1051d666..6bfe0ddc 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/CustomCollectionTypeTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/CustomCollectionTypeTest.java @@ -346,4 +346,45 @@ public java.util.Map getConfig() { // 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)"); + } } From 4b1c410d4c06e1b67318cd0cfbb9693d61b6931e Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 28 Dec 2025 17:27:59 +0100 Subject: [PATCH 17/19] Removing unused constructors --- .../builders/processor/dtos/TypeNameList.java | 55 ----------------- .../builders/processor/dtos/TypeNameMap.java | 59 ------------------- .../builders/processor/dtos/TypeNameSet.java | 55 ----------------- 3 files changed, 169 deletions(-) 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 index 24faed61..0b6a9065 100644 --- 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 @@ -74,61 +74,6 @@ public TypeNameList(TypeName outerType, List innerTypeArguments, TypeN this.elementType = elementType; } - /** - * 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 (should be 0 or 1 for List) - */ - public TypeNameList(TypeName outerType, List innerTypeArguments) { - super(outerType, innerTypeArguments); - this.isConcreteImplementation = !isListInterface(getPackageName(), getClassName()); - this.elementType = innerTypeArguments.isEmpty() ? null : innerTypeArguments.get(0); - } - - /** - * Creates a {@code TypeNameList} for the given package/class and a list of inner type arguments. - * - * @param packageName the package name - * @param className the class name (the concrete List implementation) - * @param innerTypeArguments the list of generic type arguments (should be 0 or 1 for List) - */ - public TypeNameList(String packageName, String className, List innerTypeArguments) { - super(packageName, className, innerTypeArguments); - this.isConcreteImplementation = !isListInterface(packageName, className); - this.elementType = innerTypeArguments.isEmpty() ? null : innerTypeArguments.get(0); - } - - /** - * Varargs convenience constructor with an outer {@code TypeName} and any number of inner type - * arguments. - * - * @param outerType the outer type to use for package and class name (the concrete List - * implementation) - * @param innerTypeArguments variable number of generic type arguments (should be 0 or 1 for List) - */ - public TypeNameList(TypeName outerType, TypeName... innerTypeArguments) { - super(outerType, innerTypeArguments); - this.isConcreteImplementation = !isListInterface(getPackageName(), getClassName()); - this.elementType = innerTypeArguments.length == 0 ? null : innerTypeArguments[0]; - } - - /** - * Varargs convenience constructor with package/class names and any number of inner type - * arguments. - * - * @param packageName the package name - * @param className the class name (the concrete List implementation) - * @param innerTypeArguments variable number of generic type arguments (should be 0 or 1 for List) - */ - public TypeNameList(String packageName, String className, TypeName... innerTypeArguments) { - super(packageName, className, innerTypeArguments); - this.isConcreteImplementation = !isListInterface(packageName, className); - this.elementType = innerTypeArguments.length == 0 ? null : innerTypeArguments[0]; - } - /** * Checks if this is a concrete List implementation (not the interface itself). * 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 index 9d752a2e..c30b0a06 100644 --- 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 @@ -78,65 +78,6 @@ public TypeNameMap( this.valueType = valueType; } - /** - * 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 (should be 0 or 2 for Map) - */ - public TypeNameMap(TypeName outerType, List innerTypeArguments) { - super(outerType, innerTypeArguments); - this.isConcreteImplementation = !isMapInterface(getPackageName(), getClassName()); - this.keyType = !innerTypeArguments.isEmpty() ? innerTypeArguments.get(0) : null; - this.valueType = innerTypeArguments.size() >= 2 ? innerTypeArguments.get(1) : null; - } - - /** - * Creates a {@code TypeNameMap} for the given package/class and a list of inner type arguments. - * - * @param packageName the package name - * @param className the class name (the concrete Map implementation) - * @param innerTypeArguments the list of generic type arguments (should be 0 or 2 for Map) - */ - public TypeNameMap(String packageName, String className, List innerTypeArguments) { - super(packageName, className, innerTypeArguments); - this.isConcreteImplementation = !isMapInterface(packageName, className); - this.keyType = !innerTypeArguments.isEmpty() ? innerTypeArguments.get(0) : null; - this.valueType = innerTypeArguments.size() >= 2 ? innerTypeArguments.get(1) : null; - } - - /** - * Varargs convenience constructor with an outer {@code TypeName} and any number of inner type - * arguments. - * - * @param outerType the outer type to use for package and class name (the concrete Map - * implementation) - * @param innerTypeArguments variable number of generic type arguments (should be 0 or 2 for Map) - */ - public TypeNameMap(TypeName outerType, TypeName... innerTypeArguments) { - super(outerType, innerTypeArguments); - this.isConcreteImplementation = !isMapInterface(getPackageName(), getClassName()); - this.keyType = innerTypeArguments.length >= 1 ? innerTypeArguments[0] : null; - this.valueType = innerTypeArguments.length >= 2 ? innerTypeArguments[1] : null; - } - - /** - * Varargs convenience constructor with package/class names and any number of inner type - * arguments. - * - * @param packageName the package name - * @param className the class name (the concrete Map implementation) - * @param innerTypeArguments variable number of generic type arguments (should be 0 or 2 for Map) - */ - public TypeNameMap(String packageName, String className, TypeName... innerTypeArguments) { - super(packageName, className, innerTypeArguments); - this.isConcreteImplementation = !isMapInterface(packageName, className); - this.keyType = innerTypeArguments.length >= 1 ? innerTypeArguments[0] : null; - this.valueType = innerTypeArguments.length >= 2 ? innerTypeArguments[1] : null; - } - /** * Checks if this is a concrete Map implementation (not the interface itself). * 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 index ae0e6afe..5bb1add5 100644 --- 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 @@ -74,61 +74,6 @@ public TypeNameSet(TypeName outerType, List innerTypeArguments, TypeNa this.elementType = elementType; } - /** - * 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 (should be 0 or 1 for Set) - */ - public TypeNameSet(TypeName outerType, List innerTypeArguments) { - super(outerType, innerTypeArguments); - this.isConcreteImplementation = !isSetInterface(getPackageName(), getClassName()); - this.elementType = innerTypeArguments.isEmpty() ? null : innerTypeArguments.get(0); - } - - /** - * Creates a {@code TypeNameSet} for the given package/class and a list of inner type arguments. - * - * @param packageName the package name - * @param className the class name (the concrete Set implementation) - * @param innerTypeArguments the list of generic type arguments (should be 0 or 1 for Set) - */ - public TypeNameSet(String packageName, String className, List innerTypeArguments) { - super(packageName, className, innerTypeArguments); - this.isConcreteImplementation = !isSetInterface(packageName, className); - this.elementType = innerTypeArguments.isEmpty() ? null : innerTypeArguments.get(0); - } - - /** - * Varargs convenience constructor with an outer {@code TypeName} and any number of inner type - * arguments. - * - * @param outerType the outer type to use for package and class name (the concrete Set - * implementation) - * @param innerTypeArguments variable number of generic type arguments (should be 0 or 1 for Set) - */ - public TypeNameSet(TypeName outerType, TypeName... innerTypeArguments) { - super(outerType, innerTypeArguments); - this.isConcreteImplementation = !isSetInterface(getPackageName(), getClassName()); - this.elementType = innerTypeArguments.length == 0 ? null : innerTypeArguments[0]; - } - - /** - * Varargs convenience constructor with package/class names and any number of inner type - * arguments. - * - * @param packageName the package name - * @param className the class name (the concrete Set implementation) - * @param innerTypeArguments variable number of generic type arguments (should be 0 or 1 for Set) - */ - public TypeNameSet(String packageName, String className, TypeName... innerTypeArguments) { - super(packageName, className, innerTypeArguments); - this.isConcreteImplementation = !isSetInterface(packageName, className); - this.elementType = innerTypeArguments.length == 0 ? null : innerTypeArguments[0]; - } - /** * Checks if this is a concrete Set implementation (not the interface itself). * From 510e68d72b6ee3acc982dbeae8f066464232e20a Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 28 Dec 2025 17:36:45 +0100 Subject: [PATCH 18/19] Extracting functionality into hasConstructorWithParameterOfType to remove duplicated code --- .../processor/util/JavaLangMapper.java | 72 ++++++------------- 1 file changed, 22 insertions(+), 50 deletions(-) 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 db818da3..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 @@ -257,7 +257,8 @@ private static TypeName tryWrapAsMap( TypeElement typeElement = (TypeElement) context.asElement(typeMirror); boolean isInterface = typeElement.equals(mapElement); - boolean hasConstructor = !isInterface && hasMapConstructor(typeElement, context); + boolean hasConstructor = + !isInterface && hasConstructorWithParameterOfType(typeElement, "java.util.Map", context); if (!isInterface && !hasConstructor) { return createFallbackTypeName(rawType, argTypes); @@ -281,7 +282,9 @@ private static TypeName tryWrapAsMap( private static boolean shouldWrapAsCollectionType( TypeElement typeElement, TypeElement interfaceElement, ProcessingContext context) { boolean isInterface = typeElement.equals(interfaceElement); - boolean hasConstructor = !isInterface && hasCollectionConstructor(typeElement, context); + boolean hasConstructor = + !isInterface + && hasConstructorWithParameterOfType(typeElement, "java.util.Collection", context); return isInterface || hasConstructor; } @@ -297,23 +300,26 @@ private static TypeName createFallbackTypeName(TypeName rawType, List } /** - * Checks if a type has a constructor that accepts a Collection parameter. + * 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 List/Set types by checking if - * it has a constructor like {@code ArrayList(Collection)}. + *

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 Collection + * @return true if the type has a constructor accepting the specified parameter type */ - private static boolean hasCollectionConstructor( - TypeElement typeElement, ProcessingContext context) { - TypeElement collectionElement = context.getTypeElement("java.util.Collection"); - if (collectionElement == null) { + private static boolean hasConstructorWithParameterOfType( + TypeElement typeElement, String parameterTypeName, ProcessingContext context) { + TypeElement parameterElement = context.getTypeElement(parameterTypeName); + if (parameterElement == null) { return false; } - TypeMirror collectionType = context.erasure(collectionElement.asType()); + TypeMirror parameterType = context.erasure(parameterElement.asType()); return typeElement.getEnclosedElements().stream() .filter(element -> element.getKind() == javax.lang.model.element.ElementKind.CONSTRUCTOR) @@ -323,45 +329,11 @@ private static boolean hasCollectionConstructor( if (constructor.getParameters().size() != 1) { return false; } - TypeMirror paramType = constructor.getParameters().get(0).asType(); - TypeMirror erasedParamType = context.erasure(paramType); - // Parameter should be Collection or a supertype (like Iterable) - return context.isSameType(erasedParamType, collectionType) - || context.isAssignable(erasedParamType, collectionType); - }); - } - - /** - * Checks if a type has a constructor that accepts a Map parameter. - * - *

This allows us to detect if we can generate helper methods for Map types by checking if it - * has a constructor like {@code HashMap(Map)}. - * - * @param typeElement the type to check - * @param context the processing context - * @return true if the type has a constructor accepting Map - */ - private static boolean hasMapConstructor(TypeElement typeElement, ProcessingContext context) { - TypeElement mapElement = context.getTypeElement("java.util.Map"); - if (mapElement == null) { - return false; - } - - TypeMirror mapType = context.erasure(mapElement.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 paramType = constructor.getParameters().get(0).asType(); - TypeMirror erasedParamType = context.erasure(paramType); - // Parameter should be Map - return context.isSameType(erasedParamType, mapType) - || context.isAssignable(erasedParamType, mapType); + 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); }); } From 8487e76ed06b2735635e746eebb49ae1f0fc5a04 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Sun, 28 Dec 2025 17:40:09 +0100 Subject: [PATCH 19/19] Refactoring CustomCollectionTypeTest to use helperfunctions --- .../processor/CustomCollectionTypeTest.java | 77 ++++++++++--------- 1 file changed, 40 insertions(+), 37 deletions(-) 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 index 6bfe0ddc..2b33fcf0 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/CustomCollectionTypeTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/CustomCollectionTypeTest.java @@ -27,7 +27,6 @@ import static org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils.loadGeneratedSource; 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.javahelpers.simple.builders.processor.testing.ProcessorTestUtils; @@ -51,15 +50,16 @@ 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 = - JavaFileObjects.forSourceLines( - "test.CustomList", - "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; }", - "}"); + 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( @@ -122,15 +122,16 @@ public java.util.ArrayList getNumbers() { void customSetWithMultipleTypeParameters_shouldNotGenerateVarargsHelper() { // Custom Set with 2 type parameters - should be treated as generic type, not TypeNameSet JavaFileObject customSet = - JavaFileObjects.forSourceLines( - "test.CustomSet", - "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; }", - "}"); + 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( @@ -165,15 +166,16 @@ public CustomSet getTags() { void customMapWithMultipleTypeParameters_shouldNotGenerateVarargsHelper() { // Custom Map with 3 type parameters - should be treated as generic type, not TypeNameMap JavaFileObject customMap = - JavaFileObjects.forSourceLines( - "test.CustomMap", - "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; }", - "}"); + 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( @@ -209,15 +211,16 @@ void customMapWithSingleTypeParameter_shouldGenerateVarargsHelperWithSameKeyValu // CustomMap implements Map with a Map constructor // This should be detected as a valid Map type and generate helpers JavaFileObject customMap = - JavaFileObjects.forSourceLines( - "test.SymmetricMap", - "package test;", - "import java.util.HashMap;", - "import java.util.Map;", - "public class SymmetricMap extends HashMap {", - " public SymmetricMap() { }", - " public SymmetricMap(Map m) { super(m); }", - "}"); + 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(