diff --git a/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilderConstructor.java b/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilderConstructor.java new file mode 100644 index 00000000..ad05a2b4 --- /dev/null +++ b/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilderConstructor.java @@ -0,0 +1,38 @@ +/* + * 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.core.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to be placed on a constructor, if a specific constructor should be used for building + * the resulting DTO. + */ +@Target({ElementType.CONSTRUCTOR}) +@Retention(RetentionPolicy.CLASS) +public @interface SimpleBuilderConstructor {} diff --git a/core/src/main/java/org/javahelpers/simple/builders/core/builders/ArrayListBuilder.java b/core/src/main/java/org/javahelpers/simple/builders/core/builders/ArrayListBuilder.java index 6e394cc9..6b5d13d6 100644 --- a/core/src/main/java/org/javahelpers/simple/builders/core/builders/ArrayListBuilder.java +++ b/core/src/main/java/org/javahelpers/simple/builders/core/builders/ArrayListBuilder.java @@ -66,6 +66,6 @@ public ArrayListBuilder addAll(List elements) { * @return new ArrayList holding all elements which have been added before */ public List build() { - return new ArrayList(mList); + return new ArrayList<>(mList); } } diff --git a/core/src/main/java/org/javahelpers/simple/builders/core/util/TrackedValue.java b/core/src/main/java/org/javahelpers/simple/builders/core/util/TrackedValue.java new file mode 100644 index 00000000..8d4f6854 --- /dev/null +++ b/core/src/main/java/org/javahelpers/simple/builders/core/util/TrackedValue.java @@ -0,0 +1,93 @@ +/* + * 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.core.util; + +import java.util.function.Consumer; + +/** + * Represents a value together with a flag indicating whether it has been explicitly changed. + * + *

This utility is used by generated builders to differentiate between: + * + *

    + *
  • an unset value (no change made), + *
  • an initial value (derived from an existing instance), and + *
  • a changed value (explicitly set via a builder method). + *
+ * + * The {@code isChanged} flag controls whether setter invocations should be performed during {@code + * build()}; only changed values are applied. + * + * @param the value type + * @param value the underlying value (may be {@code null}) + * @param isChanged whether the value was explicitly changed by the builder API + */ +public record TrackedValue(T value, boolean isChanged) { + + /** + * Executes the provided {@link Consumer} only if this value has been explicitly changed. + * + * @param consumer action to perform with the current {@link #value()} when {@link #isChanged()} + * is {@code true} + */ + public void ifChanged(Consumer consumer) { + if (isChanged) { + consumer.accept(value); + } + } + + /** + * Creates a tracked value representing an unset state (no change made). + * + * @param the value type + * @return an instance with {@code value == null} and {@code isChanged == false} + */ + public static TrackedValue unsetValue() { + return new TrackedValue<>(null, false); + } + + /** + * Creates a tracked value representing an initial value copied from an existing instance. The + * value is present but is not considered a change initiated by the builder. + * + * @param the value type + * @param value the initial value (may be {@code null}) + * @return an instance with the given value and {@code isChanged == false} + */ + public static TrackedValue initialValue(T value) { + return new TrackedValue<>(value, false); + } + + /** + * Creates a tracked value representing an explicit change performed via the builder API. + * + * @param the value type + * @param value the changed value (may be {@code null}) + * @return an instance with the given value and {@code isChanged == true} + */ + public static TrackedValue changedValue(T value) { + return new TrackedValue<>(value, true); + } +} diff --git a/processor/pom.xml b/processor/pom.xml index a2ce5ff8..3c706e0c 100644 --- a/processor/pom.xml +++ b/processor/pom.xml @@ -56,6 +56,7 @@ 1.1.1 0.7.0 3.19.0 + 4.4 5.13.4 0.23.0 @@ -91,6 +92,12 @@ commons-lang3 ${commons-lang.version} + + + org.apache.commons + commons-collections4 + ${commons-collections.version} + com.google.auto.service diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java index 669bcbff..941acebd 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java @@ -41,7 +41,6 @@ import javax.lang.model.util.Elements; import javax.lang.model.util.Types; import javax.tools.Diagnostic; -import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; import org.javahelpers.simple.builders.processor.dtos.BuilderDefinitionDto; import org.javahelpers.simple.builders.processor.exceptions.BuilderException; import org.javahelpers.simple.builders.processor.util.JavaCodeGenerator; @@ -88,7 +87,18 @@ public boolean process(Set annotations, RoundEnvironment // Fail fast: we already emitted an error in init(); do not attempt any processing. return true; } - for (Element annotatedElement : roundEnv.getElementsAnnotatedWith(SimpleBuilder.class)) { + // Resolve annotation as TypeElement to support environments where the Class overload + // of getElementsAnnotatedWith is unavailable. + TypeElement simpleBuilderAnnotation = + elementUtils.getTypeElement( + org.javahelpers.simple.builders.core.annotations.SimpleBuilder.class + .getCanonicalName()); + if (simpleBuilderAnnotation == null) { + // TODO: Logging + // Annotation type not on classpath; nothing to do this round. + return true; + } + for (Element annotatedElement : roundEnv.getElementsAnnotatedWith(simpleBuilderAnnotation)) { try { process(annotatedElement); } catch (BuilderException ex) { diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderDefinitionDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderDefinitionDto.java index 5fe8c644..f8d54a84 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderDefinitionDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderDefinitionDto.java @@ -35,20 +35,21 @@ public class BuilderDefinitionDto { /** Type of builder. Containing package and name of DTO. */ private TypeName builderTypeName; - /** Generic parameters declared on the target DTO (e.g., ). */ - private final List generics = new LinkedList<>(); - /** - * List of all methods of target DTO which are supported by builder and not targeting a specific - * DTO field. + * Fields in constructor. Containing all fields which need to be given for calling the + * constructor. Ordering in LinkedList is matching orderung of parameters in constructor call. + * Instances of fields are added in LinkedList of all fields too. */ - private final List methodsForBuilder = new LinkedList<>(); + private final List fieldsInConstructor = new LinkedList<>(); + + /** Generic parameters declared on the target DTO (e.g., ). */ + private final List generics = new LinkedList<>(); /** * List of all fields of target DTO which are supported by builder. A field could be supported by * multiple functions in builder. */ - private final List setterFieldsForBuilder = new LinkedList<>(); + private final List fields = new LinkedList<>(); /** * Getting type of builder. @@ -88,32 +89,18 @@ public void setBuildingTargetTypeName(TypeName buildingTargetTypeName) { this.buildingTargetTypeName = buildingTargetTypeName; } - /** - * Adding a method-definition to builder defintion. - * - * @param member {@code org.javahelpers.simple.builders.internal.dtos.MethodDto} to be added - */ - public void addMethod(MethodDto member) { - methodsForBuilder.add(member); - } - - /** - * Getting all definied methods in builder definition. - * - * @return list of methods with type {@code - * org.javahelpers.simple.builders.internal.dtos.MethodDto} - */ - public List getMethodsForBuilder() { - return methodsForBuilder; - } - /** * Adding a field-definition to builder defintion. * * @param field {@code org.javahelpers.simple.builders.internal.dtos.FieldDto} to be added */ public void addField(FieldDto field) { - setterFieldsForBuilder.add(field); + fields.add(field); + } + + /** Adds a field definition that will be provided via the DTO constructor. */ + public void addFieldInConstructor(FieldDto field) { + fieldsInConstructor.add(field); } /** @@ -122,7 +109,23 @@ public void addField(FieldDto field) { * @return list of fields with type {@code org.javahelpers.simple.builders.internal.dtos.FieldDto} */ public List getSetterFieldsForBuilder() { - return setterFieldsForBuilder; + return fields; + } + + /** Returns the ordered list of fields to be passed to the DTO constructor. */ + public List getConstructorFieldsForBuilder() { + return fieldsInConstructor; + } + + /** + * Returns all fields known to the builder in a single list. Constructor parameters appear first + * (in constructor order), followed by setter-derived fields. + */ + public List getAllFieldsForBuilder() { + LinkedList all = new LinkedList<>(); + all.addAll(fieldsInConstructor); + all.addAll(fields); + return all; } /** Adds a generic parameter definition. */ diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/FieldDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/FieldDto.java index 77a5359b..7a76b82d 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/FieldDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/FieldDto.java @@ -26,6 +26,8 @@ import java.util.ArrayList; import java.util.List; +import java.util.Optional; +import org.apache.commons.lang3.StringUtils; /** * Definition of a field. Containing all information to generate methods in builder to set or modify @@ -38,7 +40,7 @@ public class FieldDto { /** Type of field. Containing generic, name of package and class */ private TypeName fieldType; - /** List of all methods in builder, which change the field. */ + /** List of all methods in builder, which provide helpers to change the field. */ private final List fieldSetterMethodsList = new ArrayList<>(); /** Optional Javadoc of the field extracted from setter or constructor. */ @@ -47,6 +49,12 @@ public class FieldDto { /** Method-level generics declared on the setter, to be reused in builder methods. */ private final List fieldGenerics = new ArrayList<>(); + /** + * The name of the getter method to call, because in case of boolean it does not need to start + * with get (e.g., getName or isActive). + */ + private String getterName; + /** * Getting name of field. * @@ -65,6 +73,15 @@ public void setFieldName(String fieldName) { this.fieldName = fieldName; } + /** + * Getting name of setter method. + * + * @return name of setter for field + */ + public String getSetterName() { + return "set" + StringUtils.capitalize(fieldName); + } + /** * Getting type of field. * @@ -132,4 +149,14 @@ public void setFieldGenerics(List generics) { this.fieldGenerics.addAll(generics); } } + + /** Returns the getter method name to use (without parentheses). */ + public Optional getGetterName() { + return Optional.ofNullable(getterName); + } + + /** Sets the getter method name to use (without parentheses). */ + public void setGetterName(String getterName) { + this.getterName = getterName; + } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/MethodDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/MethodDto.java index a9a45482..165e9f0b 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/MethodDto.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/MethodDto.java @@ -27,7 +27,6 @@ import static org.javahelpers.simple.builders.processor.dtos.MethodTypes.PROXY; import java.util.LinkedList; -import java.util.List; import java.util.Optional; import javax.lang.model.element.Modifier; import org.apache.commons.lang3.StringUtils; @@ -52,9 +51,6 @@ public class MethodDto { /** Definition of inner implementation for method. */ private final MethodCodeDto methodCodeDto = new MethodCodeDto(); - /** Method-level type parameters (e.g., ). */ - private final List methodGenerics = new LinkedList<>(); - /** * Getting type of method for source code generation. Defaults to {@code * org.javahelpers.simple.builders.internal.dtos.MethodTypes.PROXY}. @@ -163,19 +159,6 @@ public LinkedList getParameters() { return parameters; } - /** Replaces method-level generic parameter list with the provided entries. */ - public void setMethodGenerics(List generics) { - this.methodGenerics.clear(); - if (generics != null) { - this.methodGenerics.addAll(generics); - } - } - - /** Returns declared method-level generic parameters. */ - public List getMethodGenerics() { - return methodGenerics; - } - /** * Getting the access modifier for method. Optional for usage in stream-notation. * diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/TypeName.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/TypeName.java index a62c567a..77e83715 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/TypeName.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/TypeName.java @@ -79,4 +79,14 @@ public String getClassName() { public Optional getInnerType() { return Optional.empty(); } + + /** + * Creates a new instance of {@code TypeName} from an existing {@code Class}. + * + * @param clazz existing class to retrieve packe and class-name from + * @return + */ + public static TypeName of(Class clazz) { + return new TypeName(clazz.getPackage().getName(), clazz.getSimpleName()); + } } 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 226991de..301ca5ce 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 @@ -24,17 +24,17 @@ package org.javahelpers.simple.builders.processor.util; +import static java.util.stream.Collectors.toSet; import static javax.lang.model.type.TypeKind.ARRAY; import static org.javahelpers.simple.builders.processor.util.AnnotationValidator.validateAnnotatedElement; import static org.javahelpers.simple.builders.processor.util.JavaLangAnalyser.*; import static org.javahelpers.simple.builders.processor.util.JavaLangMapper.map2MethodParameter; import static org.javahelpers.simple.builders.processor.util.JavaLangMapper.map2TypeName; -import static org.javahelpers.simple.builders.processor.util.JavaLangMapper.mapRelevantModifier; import static org.javahelpers.simple.builders.processor.util.TypeNameAnalyser.*; import java.util.List; -import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.function.Consumer; import java.util.function.Supplier; import javax.lang.model.element.*; @@ -42,12 +42,14 @@ import javax.lang.model.util.ElementFilter; import javax.lang.model.util.Elements; import javax.lang.model.util.Types; +import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.javahelpers.simple.builders.core.annotations.IgnoreInBuilder; import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; import org.javahelpers.simple.builders.core.builders.ArrayListBuilder; import org.javahelpers.simple.builders.core.builders.HashMapBuilder; import org.javahelpers.simple.builders.core.builders.HashSetBuilder; +import org.javahelpers.simple.builders.core.util.TrackedValue; import org.javahelpers.simple.builders.processor.dtos.*; import org.javahelpers.simple.builders.processor.exceptions.BuilderException; @@ -79,16 +81,38 @@ public static BuilderDefinitionDto extractFromElement( JavaLangMapper.map2GenericParameterDtos(annotatedType, elementUtils, typeUtils) .forEach(result::addGeneric); + // Extract constructors and their parameters (treat as constructor fields) + Optional constructorOpt = + findConstructorForBuilder(annotatedType, elementUtils); + if (constructorOpt.isPresent()) { + ExecutableElement ctor = constructorOpt.get(); + for (VariableElement param : ctor.getParameters()) { + Optional fieldFromCtor = + createFieldFromConstructor(annotatedType, param, elementUtils, typeUtils); + fieldFromCtor.ifPresent(result::addFieldInConstructor); + } + } + + // Todo: moving into helper function List allMembers = elementUtils.getAllMembers(annotatedType); List methods = ElementFilter.methodsIn(allMembers); + // Build a set of constructor field names to avoid duplicates from setters + Set ctorFieldNames = + result.getConstructorFieldsForBuilder().stream() + .map(FieldDto::getFieldName) + .collect(toSet()); + for (ExecutableElement mth : methods) { // nur public if (isMethodRelevantForBuilder(mth)) { - if (isSetterForField(mth)) { - createFieldDto(mth, elementUtils, typeUtils).ifPresent(result::addField); - } else { - createMethodDto(mth, elementUtils, typeUtils).ifPresent(result::addMethod); + // Avoid adding setter-derived fields that duplicate constructor params + Optional maybeField = createFieldFromSetter(mth, elementUtils, typeUtils); + if (maybeField.isPresent()) { + FieldDto field = maybeField.get(); + if (!ctorFieldNames.contains(field.getFieldName())) { + result.addField(field); + } } } } @@ -97,7 +121,8 @@ public static BuilderDefinitionDto extractFromElement( } private static boolean isMethodRelevantForBuilder(ExecutableElement mth) { - return isNoMethodOfObjectClass(mth) + return isSetterForField(mth) + && isNoMethodOfObjectClass(mth) && hasNoThrowablesDeclared(mth) && hasNoReturnValue(mth) && hasNotAnnotation(IgnoreInBuilder.class, mth) @@ -105,36 +130,6 @@ && isNotPrivate(mth) && isNotStatic(mth); } - private static Optional createMethodDto( - ExecutableElement mth, Elements elementUtils, Types typeUtils) { - String methodName = mth.getSimpleName().toString(); - List parameters = mth.getParameters(); - - MethodDto result = new MethodDto(); - result.setMethodName(methodName); - result.setModifier(mapRelevantModifier(mth.getModifiers())); - parameters.stream() - .map(v -> map2MethodParameter(v, elementUtils, typeUtils)) - .forEach(result::addParameter); - if (result.getParameters().stream().anyMatch(Objects::isNull)) { - // TODO: Logging - return Optional.empty(); - } - - List paramList = - result.getParameters().stream().map(MethodParameterDto::getParameterName).toList(); - String paramListJoin = String.join(",", paramList); - result.setCode( - """ - instance.$dtoMethod:N($dtoMethodParams:N); - return this; - """); - result.addArgument("dtoMethod", methodName); - result.addArgument("dtoMethodParams", paramListJoin); - - return Optional.of(result); - } - private static void addAdditionalHelperMethodsForField( FieldDto result, String fieldName, TypeName fieldType) { List innerTypes; @@ -150,25 +145,18 @@ private static void addAdditionalHelperMethodsForField( if (isList(fieldType) && innerTypesCnt == 1) { result.addMethod( createFieldSetterWithTransform( - fieldName, - "List.of(%s)", - new TypeNameArray(innerTypes.get(0), false), - result.getFieldGenerics())); + fieldName, "List.of(%s)", new TypeNameArray(innerTypes.get(0), false))); } else if (isSet(fieldType) && innerTypesCnt == 1) { result.addMethod( createFieldSetterWithTransform( - fieldName, - "Set.of(%s)", - new TypeNameArray(innerTypes.get(0), true), - result.getFieldGenerics())); + fieldName, "Set.of(%s)", new TypeNameArray(innerTypes.get(0), true))); } else if (isMap(fieldType) && innerTypesCnt == 2) { TypeName mapEntryType = new TypeNameArray( new TypeNameGeneric("java.util", "Map.Entry", innerTypes.get(0), innerTypes.get(1)), false); result.addMethod( - createFieldSetterWithTransform( - fieldName, "Map.ofEntries(%s)", mapEntryType, result.getFieldGenerics())); + createFieldSetterWithTransform(fieldName, "Map.ofEntries(%s)", mapEntryType)); } } @@ -236,10 +224,10 @@ private static void addSupplierMethodsForField( if (isFunctionalInterface(fieldTypeElement)) { return; } - result.addMethod(createFieldSupplier(fieldName, fieldType, result.getFieldGenerics())); + result.addMethod(createFieldSupplier(fieldName, fieldType)); } - private static Optional createFieldDto( + private static Optional createFieldFromSetter( ExecutableElement mth, Elements elementUtils, Types typeUtils) { String methodName = mth.getSimpleName().toString(); String fieldName = StringUtils.uncapitalize(StringUtils.removeStart(methodName, "set")); @@ -273,17 +261,26 @@ private static Optional createFieldDto( return Optional.empty(); } TypeName fieldType = fieldParameterDto.getParameterType(); + result.setFieldType(fieldType); - // Extract generics declared on the setter itself (field-specific), e.g., result.setGetterName(getterExecutable.getSimpleName().toString())); + + // Finding generics declared on the setter itself (field-specific), e.g., - List fieldGenerics = - mth.getTypeParameters().stream() - .map(tp -> JavaLangMapper.map2GenericParameterDto(tp, elementUtils, typeUtils)) - .toList(); - result.setFieldGenerics(fieldGenerics); + // If there are field-specific generics, no field in builder could be generated for it, so it + // needs to be ignored + if (CollectionUtils.isNotEmpty(mth.getTypeParameters())) { + // TODO: Logging + return Optional.empty(); + } // simple setter - result.addMethod(createFieldSetterWithTransform(fieldName, null, fieldType, fieldGenerics)); + result.addMethod(createFieldSetterWithTransform(fieldName, null, fieldType)); // add consumer/supplier generation via helpers addConsumerMethodsForField( @@ -294,11 +291,33 @@ private static Optional createFieldDto( return Optional.of(result); } + /** + * Creates a FieldDto from a constructor parameter, including a simple builder setter to supply + * the constructor argument. + */ + private static Optional createFieldFromConstructor( + TypeElement dtoType, VariableElement param, Elements elementUtils, Types typeUtils) { + MethodParameterDto paramDto = map2MethodParameter(param, elementUtils, typeUtils); + if (paramDto == null) { + return Optional.empty(); + } + String fieldName = param.getSimpleName().toString(); + TypeName fieldType = paramDto.getParameterType(); + FieldDto ctorField = new FieldDto(); + ctorField.setFieldName(fieldName); + ctorField.setFieldType(fieldType); + // Set javadoc (default to field name if no javadoc found) + ctorField.setJavaDoc(fieldName); + // Find matching getter on the DTO type + JavaLangAnalyser.findGetterForField(dtoType, fieldName, param.asType(), elementUtils, typeUtils) + .ifPresent(getter -> ctorField.setGetterName(getter.getSimpleName().toString())); + // Provide a simple setter in builder to set the constructor argument + ctorField.addMethod(createFieldSetterWithTransform(fieldName, null, fieldType)); + return Optional.of(ctorField); + } + private static MethodDto createFieldSetterWithTransform( - String fieldName, - String transform, - TypeName fieldType, - List fieldGenerics) { + String fieldName, String transform, TypeName fieldType) { MethodParameterDto parameter = new MethodParameterDto(); parameter.setParameterName(fieldName); parameter.setParameterTypeName(fieldType); @@ -307,7 +326,6 @@ private static MethodDto createFieldSetterWithTransform( methodDto.addParameter(parameter); methodDto.setModifier(Modifier.PUBLIC); methodDto.setMethodType(MethodTypes.PROXY); - methodDto.setMethodGenerics(fieldGenerics); String params; if (StringUtils.isBlank(transform)) { params = parameter.getParameterName(); @@ -316,11 +334,12 @@ private static MethodDto createFieldSetterWithTransform( } methodDto.setCode( """ - instance.$dtoMethod:N($dtoMethodParams:N); + this.$fieldName:N = $builderFieldWrapper:T.changedValue($dtoMethodParams:N); return this; """); - methodDto.addArgument("dtoMethod", methodDto.createFieldSetterMethodName()); + methodDto.addArgument("fieldName", fieldName); methodDto.addArgument("dtoMethodParams", params); + methodDto.addArgument("builderFieldWrapper", TypeName.of(TrackedValue.class)); return methodDto; } @@ -338,12 +357,13 @@ private static MethodDto createFieldConsumer(String fieldName, TypeName fieldTyp """ $helperType:T consumer = new $helperType:T(); $dtoMethodParam:N.accept(consumer); - instance.$dtoMethod:N(consumer); + this.$fieldName:N = $builderFieldWrapper:T.changedValue(consumer); return this; """); - methodDto.addArgument("dtoMethod", methodDto.createFieldSetterMethodName()); + methodDto.addArgument("fieldName", fieldName); methodDto.addArgument("dtoMethodParam", parameter.getParameterName()); methodDto.addArgument("helperType", fieldType); + methodDto.addArgument("builderFieldWrapper", TypeName.of(TrackedValue.class)); return methodDto; } @@ -367,17 +387,17 @@ private static MethodDto createFieldConsumerWithBuilder(String fieldName, TypeNa """ $helperType:T builder = new $helperType:T(); $dtoMethodParam:N.accept(builder); - instance.$dtoMethod:N(builder.build()); + this.$fieldName:N = $builderFieldWrapper:T.changedValue(builder.build()); return this; """); - methodDto.addArgument("dtoMethod", methodDto.createFieldSetterMethodName()); + methodDto.addArgument("fieldName", fieldName); methodDto.addArgument("dtoMethodParam", parameter.getParameterName()); methodDto.addArgument("helperType", builderType); + methodDto.addArgument("builderFieldWrapper", TypeName.of(TrackedValue.class)); return methodDto; } - private static MethodDto createFieldSupplier( - String fieldName, TypeName fieldType, List fieldGenerics) { + private static MethodDto createFieldSupplier(String fieldName, TypeName fieldType) { TypeNameGeneric supplierType = new TypeNameGeneric(map2TypeName(Supplier.class), fieldType); MethodParameterDto parameter = new MethodParameterDto(); parameter.setParameterName(fieldName + "Supplier"); @@ -387,14 +407,14 @@ private static MethodDto createFieldSupplier( methodDto.addParameter(parameter); methodDto.setModifier(Modifier.PUBLIC); methodDto.setMethodType(MethodTypes.SUPPLIER); - methodDto.setMethodGenerics(fieldGenerics); methodDto.setCode( """ - instance.$dtoMethod:N($dtoMethodParam:N.get()); + this.$fieldName:N = $builderFieldWrapper:T.changedValue($dtoMethodParam:N.get()); return this; """); - methodDto.addArgument("dtoMethod", methodDto.createFieldSetterMethodName()); + methodDto.addArgument("fieldName", fieldName); methodDto.addArgument("dtoMethodParam", parameter.getParameterName()); + methodDto.addArgument("builderFieldWrapper", TypeName.of(TrackedValue.class)); return methodDto; } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java index 8e14c365..59620ea4 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java @@ -26,7 +26,6 @@ import static javax.lang.model.element.Modifier.PUBLIC; import static javax.lang.model.element.Modifier.STATIC; -import static org.javahelpers.simple.builders.processor.dtos.MethodTypes.*; import static org.javahelpers.simple.builders.processor.util.JavapoetMapper.*; import com.palantir.javapoet.AnnotationSpec; @@ -44,6 +43,7 @@ import javax.lang.model.element.Modifier; import org.javahelpers.simple.builders.core.annotations.BuilderImplementation; import org.javahelpers.simple.builders.core.interfaces.IBuilderBase; +import org.javahelpers.simple.builders.core.util.TrackedValue; import org.javahelpers.simple.builders.processor.dtos.*; import org.javahelpers.simple.builders.processor.exceptions.BuilderException; @@ -89,34 +89,45 @@ public void generateBuilder(BuilderDefinitionDto builderDef) throws BuilderExcep TypeSpec.classBuilder(builderBaseClass) .addTypeVariables(map2TypeVariables(builderDef.getGenerics())) .addJavadoc(createJavadocForClass(dtoBaseClass)) - .addSuperinterface(createInterfaceBuilderBase(dtoTypeName)) - .addField(createFieldDtoInstance(dtoTypeName)); + .addSuperinterface(createInterfaceBuilderBase(dtoTypeName)); - // TODO: Constructors sollten in der BuilderDefinitionDto definiert werden, nur für Klassen mit - // leerem Constructor ist auch ein Builder mit leerem Constructor möglich - classBuilder.addMethod(createConstructorWithInstance(dtoBaseClass, dtoTypeName)); - classBuilder.addMethod(createEmptyConstructor(dtoBaseClass, builderDef.getGenerics())); - - // Generate Methods in Builder without being setter or getter - for (MethodDto methodDto : builderDef.getMethodsForBuilder()) { - classBuilder.addMethod(createMethod(methodDto, builderTypeName)); + // Adding Constructors for builder + classBuilder.addMethod( + createConstructorWithInstance( + dtoBaseClass, dtoTypeName, builderDef.getAllFieldsForBuilder())); + classBuilder.addMethod(createEmptyConstructor(dtoBaseClass)); + + // Generate backing fields for each DTO field (constructor and setter fields) + for (FieldDto fieldDto : builderDef.getConstructorFieldsForBuilder()) { + FieldSpec fieldSpec = createFieldMember(fieldDto); + classBuilder.addField(fieldSpec); + } + for (FieldDto fieldDto : builderDef.getSetterFieldsForBuilder()) { + FieldSpec fieldSpec = createFieldMember(fieldDto); + classBuilder.addField(fieldSpec); } - // Generate Fields and Fieldspecific funtions in Builder + // Generate field-specific functions in Builder (constructor fields first, then setter fields) + for (FieldDto fieldDto : builderDef.getConstructorFieldsForBuilder()) { + List methodSpecs = createFieldMethods(fieldDto, builderTypeName); + classBuilder.addMethods(methodSpecs); + } for (FieldDto fieldDto : builderDef.getSetterFieldsForBuilder()) { List methodSpecs = createFieldMethods(fieldDto, builderTypeName); classBuilder.addMethods(methodSpecs); } // Adding builder-specific methods - classBuilder.addMethod(createMethodBuild(dtoTypeName)); classBuilder.addMethod( - createMethodStaticCreate( - builderBaseClass, - builderTypeName, + createMethodBuild( dtoBaseClass, dtoTypeName, + builderDef.getConstructorFieldsForBuilder(), + builderDef.getSetterFieldsForBuilder(), builderDef.getGenerics())); + classBuilder.addMethod( + createMethodStaticCreate( + builderBaseClass, builderTypeName, dtoBaseClass, builderDef.getGenerics())); // Adding annotations classBuilder.addAnnotation(createAnnotationGenerated()); @@ -127,7 +138,13 @@ public void generateBuilder(BuilderDefinitionDto builderDef) throws BuilderExcep private void writeClassToFile(String packageName, TypeSpec typeSpec) throws BuilderException { try { - JavaFile.builder(packageName, typeSpec).skipJavaLangImports(true).build().writeTo(filer); + JavaFile.builder(packageName, typeSpec) + .skipJavaLangImports(true) + .addStaticImport(TrackedValue.class, "initialValue") + .addStaticImport(TrackedValue.class, "changedValue") + .addStaticImport(TrackedValue.class, "unsetValue") + .build() + .writeTo(filer); } catch (IOException ex) { throw new BuilderException(null, ex); } @@ -156,14 +173,7 @@ private AnnotationSpec createAnnotationBuilderImplementation(ClassName dtoClass) .build(); } - private FieldSpec createFieldDtoInstance(com.palantir.javapoet.TypeName dtoTypeName) { - return FieldSpec.builder(dtoTypeName, "instance", Modifier.PRIVATE, Modifier.FINAL) - .addJavadoc("Inner instance of builder.") - .build(); - } - - private MethodSpec createEmptyConstructor( - ClassName dtoClass, List generics) { + private MethodSpec createEmptyConstructor(ClassName dtoClass) { MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder() .addModifiers(Modifier.PUBLIC) @@ -173,49 +183,93 @@ private MethodSpec createEmptyConstructor( """, dtoClass.packageName(), dtoClass); - if (generics.isEmpty()) { - constructorBuilder.addStatement("this.instance = new $1T()", dtoClass); - } else { - // Use diamond operator only if the DTO is generic; otherwise, use raw constructor - constructorBuilder.addStatement("this.instance = new $1T<>()", dtoClass); - } return constructorBuilder.build(); } private MethodSpec createConstructorWithInstance( - ClassName dtoBaseClass, com.palantir.javapoet.TypeName dtoType) { - return MethodSpec.constructorBuilder() - .addModifiers(Modifier.PUBLIC) - .addParameter(dtoType, "instance") - .addStatement("this.instance = instance") - .addJavadoc( - """ + ClassName dtoBaseClass, com.palantir.javapoet.TypeName dtoType, List fields) { + MethodSpec.Builder cb = + MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) + .addParameter(dtoType, "instance") + .addJavadoc( + """ Initialisation of builder for {@code $1N.$2T} by a instance. @param instance object instance for initialisiation """, - dtoBaseClass.packageName(), - dtoBaseClass) - .build(); + dtoBaseClass.packageName(), + dtoBaseClass); + + for (FieldDto f : fields) { + f.getGetterName() + .ifPresent( + getter -> + cb.addStatement( + "this.$N = $T.initialValue(instance.$N())", + f.getFieldName(), + ClassName.get(TrackedValue.class), + getter)); + } + return cb.build(); } - private MethodSpec createMethodBuild(com.palantir.javapoet.TypeName returnType) { - return MethodSpec.methodBuilder("build") - .addModifiers(PUBLIC) - .returns(returnType) - .addAnnotation(Override.class) - .addCode( - """ - return instance; - """) + private FieldSpec createFieldMember(FieldDto fieldDto) { + com.palantir.javapoet.TypeName fieldType = map2ParameterType(fieldDto.getFieldType()); + if (fieldType.isPrimitive()) { + fieldType = fieldType.box(); + } + // Wrap all fields in TrackedValue + ClassName builderFieldWrapper = ClassName.get(TrackedValue.class); + ParameterizedTypeName wrappedFieldType = + ParameterizedTypeName.get(builderFieldWrapper, fieldType); + return FieldSpec.builder(wrappedFieldType, fieldDto.getFieldName(), Modifier.PRIVATE) + .addJavadoc( + "Tracked value for $L: $L.\n", + fieldDto.getFieldName(), + fieldDto.getJavaDoc()) + .initializer("$T.unsetValue()", builderFieldWrapper) .build(); } + private MethodSpec createMethodBuild( + ClassName dtoBaseClass, + com.palantir.javapoet.TypeName returnType, + List constructorFields, + List setterFields, + List generics) { + MethodSpec.Builder mb = + MethodSpec.methodBuilder("build") + .addModifiers(PUBLIC) + .returns(returnType) + .addAnnotation(Override.class); + + // Build constructor argument list: use backing fields' values in declared order + String ctorArgs = + constructorFields.stream() + .map(FieldDto::getFieldName) + .map(n -> String.format("this.%s.value()", n)) + .reduce((a, b) -> a + ", " + b) + .orElse(""); + + if (generics.isEmpty()) { + mb.addStatement("$1T result = new $1T($2L)", dtoBaseClass, ctorArgs); + } else { + mb.addStatement("$1T result = new $2T<>($3L)", returnType, dtoBaseClass, ctorArgs); + } + + // Apply setter-based fields only when changed + for (FieldDto f : setterFields) { + mb.addStatement("this.$N.ifChanged(result::$N)", f.getFieldName(), f.getSetterName()); + } + mb.addStatement("return result"); + return mb.build(); + } + private MethodSpec createMethodStaticCreate( com.palantir.javapoet.ClassName builderBaseClass, com.palantir.javapoet.TypeName builderType, com.palantir.javapoet.ClassName dtoBaseClass, - com.palantir.javapoet.TypeName dtoType, List generics) { MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("create") @@ -229,27 +283,12 @@ private MethodSpec createMethodStaticCreate( dtoBaseClass.packageName(), dtoBaseClass); if (generics.isEmpty()) { - methodBuilder - .returns(builderBaseClass) - .addCode( - """ - $1T instance = new $1T(); - return new $2T(instance); - """, - dtoBaseClass, - builderBaseClass); + methodBuilder.returns(builderBaseClass).addCode("return new $1T();\n", builderBaseClass); } else { methodBuilder .returns(builderType) - .addCode( - """ - $1T instance = new $2T<>(); - return new $3T<>(instance); - """, - dtoType, - dtoBaseClass, - builderBaseClass) - .addTypeVariables(map2TypeVariables(generics)); + .addTypeVariables(map2TypeVariables(generics)) + .addCode("return new $1T<>();\n", builderBaseClass); } return methodBuilder.build(); } @@ -261,11 +300,6 @@ private List createFieldMethods( .toList(); } - private MethodSpec createMethod(MethodDto methodDto, com.palantir.javapoet.TypeName returnType) { - // TODO: Remove when switched to fields instead of instance in builder - return createMethod(methodDto, returnType, ""); - } - private MethodSpec createMethod( MethodDto methodDto, com.palantir.javapoet.TypeName returnType, @@ -273,29 +307,24 @@ private MethodSpec createMethod( MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(methodDto.getMethodName()).returns(returnType); methodDto.getModifier().ifPresent(methodBuilder::addModifiers); - // add method-level generics if present (e.g., ) - if (!methodDto.getMethodGenerics().isEmpty()) { - methodBuilder.addTypeVariables(map2TypeVariables(methodDto.getMethodGenerics())); - } int maxIndexParameters = methodDto.getParameters().size() - 1; // Adding javadoc for method switch (methodDto.getMethodType()) { case PROXY -> methodBuilder.addJavadoc( - "Calling $1N on dto-instance with parameters.\n", - methodDto.getMethodName()); + "Sets the value for $1N.\n", methodDto.getMethodName()); case CONSUMER -> methodBuilder.addJavadoc( - "Calling $1N on dto-instance with value after executing consumer.\n", + "Sets the value for $1N by executing the provided consumer.\n", methodDto.createFieldSetterMethodName()); case CONSUMER_BY_BUILDER -> methodBuilder.addJavadoc( - "Calling $1N on dto-instance with builder result value.\n", + "Sets the value for $1N using a builder consumer that produces the value.\n", methodDto.createFieldSetterMethodName()); case SUPPLIER -> methodBuilder.addJavadoc( - "Calling $1N on instance with value of supplier.\n", + "Sets the value for $1N by invoking the provided supplier.\n", methodDto.createFieldSetterMethodName()); } @@ -311,20 +340,20 @@ private MethodSpec createMethod( switch (methodDto.getMethodType()) { case PROXY -> methodBuilder.addJavadoc( - "\n@param $1N $2N", paramDto.getParameterName(), optionalFieldParamJavaDoc); + "\n@param $1N $2L", paramDto.getParameterName(), optionalFieldParamJavaDoc); case CONSUMER -> methodBuilder.addJavadoc( - "\n@param $1N consumer providing an instance of $2N", + "\n@param $1N consumer providing an instance of $2L", paramDto.getParameterName(), optionalFieldParamJavaDoc); case CONSUMER_BY_BUILDER -> methodBuilder.addJavadoc( - "\n@param $1N consumer providing an instance of a builder for $2N", + "\n@param $1N consumer providing an instance of a builder for $2L", paramDto.getParameterName(), optionalFieldParamJavaDoc); case SUPPLIER -> methodBuilder.addJavadoc( - "\n@param $1N supplier for $2N", + "\n@param $1N supplier for $2L", paramDto.getParameterName(), optionalFieldParamJavaDoc); } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaLangAnalyser.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaLangAnalyser.java index da3aa81c..1395bca4 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaLangAnalyser.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaLangAnalyser.java @@ -36,10 +36,13 @@ import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeMirror; import javax.lang.model.util.ElementFilter; import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Strings; +import org.javahelpers.simple.builders.core.annotations.SimpleBuilderConstructor; /** Helperclass for extrating specific information from existing classes. */ public final class JavaLangAnalyser { @@ -117,7 +120,11 @@ public static boolean isNotStatic(ExecutableElement mth) { * @return {@code true}, if the signature matches setters */ public static boolean isSetterForField(ExecutableElement mth) { - return StringUtils.startsWith(mth.getSimpleName(), "set") && mth.getParameters().size() == 1; + String name = mth.getSimpleName().toString(); + return StringUtils.length(name) > 3 + && Strings.CS.startsWith(name, "set") + && StringUtils.isAllUpperCase(StringUtils.substring(name, 3, 4)) + && mth.getParameters().size() == 1; } /** @@ -240,4 +247,78 @@ public static String extractParamJavaDoc(String javaDoc, VariableElement paramet String result = sb.toString().trim(); return result.isEmpty() ? null : result; } + + /** + * Finds the getter method for a given field on the specified DTO type. + * + *

Prefers boolean-style "isX" over "getX" when both are present. The getter must have no + * parameters and its return type must match the provided field type mirror. + * + * @param dtoType the enclosing DTO type element + * @param fieldName the field name (uncapitalized) + * @param fieldTypeMirror the expected return type of the getter + * @param elementUtils elements utility + * @param typeUtils types utility + * @return Optional containing the getter ExecutableElement if found + */ + public static Optional findGetterForField( + TypeElement dtoType, + String fieldName, + TypeMirror fieldTypeMirror, + Elements elementUtils, + Types typeUtils) { + if (dtoType == null || fieldName == null || fieldTypeMirror == null) { + return Optional.empty(); + } + String cap = StringUtils.capitalize(fieldName); + String getterCandidate = "get" + cap; + String booleanGetterCandidate = "is" + cap; + List classMethods = + ElementFilter.methodsIn(elementUtils.getAllMembers(dtoType)); + // Prefer boolean-style getter if present + for (ExecutableElement candidate : classMethods) { + String name = candidate.getSimpleName().toString(); + if ((name.equals(booleanGetterCandidate) || name.equals(getterCandidate)) + && candidate.getParameters().isEmpty() + && typeUtils.isSameType(candidate.getReturnType(), fieldTypeMirror)) { + return Optional.of(candidate); + } + } + return Optional.empty(); + } + + /** + * Determines which constructor to use for builder initialization. Prioritizes constructors + * annotated with {@link SimpleBuilderConstructor}. If none is annotated, selects the constructor + * with the highest number of parameters. Returns empty if no constructor has parameters (i.e., + * only default constructor or none found). + * + * @param annotatedType the type element to search for constructors + * @param elementUtils elements utility + * @return Optional containing the selected constructor, or empty if none suitable + */ + public static Optional findConstructorForBuilder( + TypeElement annotatedType, Elements elementUtils) { + List ctors = + ElementFilter.constructorsIn(elementUtils.getAllMembers(annotatedType)); + + // First, check if any constructor is annotated with @SimpleBuilderConstructor + for (ExecutableElement ctor : ctors) { + if (ctor.getAnnotation(SimpleBuilderConstructor.class) != null) { + return Optional.of(ctor); + } + } + + // Fall back to heuristic: select constructor with the most parameters + ExecutableElement selected = null; + int maxParams = -1; + for (ExecutableElement ctor : ctors) { + int p = ctor.getParameters().size(); + if (p > maxParams) { + maxParams = p; + selected = ctor; + } + } + return (selected != null && maxParams > 0) ? Optional.of(selected) : Optional.empty(); + } } 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 fd271988..9d1a9ae6 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 @@ -66,6 +66,11 @@ public static TypeName[] map2TypeArgumentsArray( */ public static TypeName map2ParameterType( org.javahelpers.simple.builders.processor.dtos.TypeName parameterType) { + // Handle generic type variables (e.g., T, K, V) + if (parameterType + instanceof org.javahelpers.simple.builders.processor.dtos.TypeNameVariable typeVariable) { + return TypeVariableName.get(typeVariable.getClassName()); + } ClassName classNameParameter = ClassName.get(parameterType.getPackageName(), parameterType.getClassName()); if (parameterType instanceof TypeNameArray parameterTypeArray) { 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 e5ca93f2..c1fe3630 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 @@ -77,8 +77,53 @@ public abstract class AbstractAnnotated { } } @Test - @Disabled("TODO: Constructor-Parameters are not supported yet") - void shouldIgnoreConstructorParametersAndOnlyUseSetters() { + void shouldGenerateBuilderForNonEmptyConstructorWithFinalFields() { + // Given + String packageName = "test"; + String className = "NonEmptyCtorFinals"; + String builderClassName = className + "Builder"; + + JavaFileObject sourceFile = + ProcessorTestUtils.simpleBuilderClass( + packageName, + className, + """ + private final String name; + private final int age; + + public NonEmptyCtorFinals(String name, int age) { + this.name = name; + this.age = age; + } + + public String getName() { return name; } + public int getAge() { return age; } + """); + + // When + Compilation compilation = compile(sourceFile); + + // Then + String generatedCode = loadGeneratedSource(compilation, builderClassName); + assertGenerationSucceeded(compilation, builderClassName, generatedCode); + + // Expect helper methods for constructor params and proper build invocation + ProcessorAsserts.assertContaining( + generatedCode, + "public NonEmptyCtorFinalsBuilder()", + "public NonEmptyCtorFinalsBuilder(NonEmptyCtorFinals instance)", + "this.name = initialValue(instance.getName());", + "this.age = initialValue(instance.getAge());", + "public NonEmptyCtorFinalsBuilder name(String name)", + "public NonEmptyCtorFinalsBuilder age(int age)", + "NonEmptyCtorFinals result = new NonEmptyCtorFinals(this.name.value(), this.age.value());"); + + // Ensure no setter calls are emitted for final fields + ProcessorAsserts.assertNotContaining(generatedCode, "result.setName", "result.setAge"); + } + + @Test + void shouldHandleFieldsOfConstructorAndSetters() { // Given String packageName = "test"; String className = "CtorAndSetter"; @@ -89,10 +134,9 @@ void shouldIgnoreConstructorParametersAndOnlyUseSetters() { packageName, className, """ - private int a; // has no setter, only in constructor + private final int a; // has no setter, only in constructor private String name; // will be set via setter - public CtorAndSetter() {} public CtorAndSetter(int a) { this.a = a; } public String getName() { return name; } @@ -109,10 +153,120 @@ public CtorAndSetter() {} // Setter-based API should appear (name) and Constructor params (a) ProcessorAsserts.assertContaining( generatedCode, - "public CtorAndSetterBuilder a(int a)", - "instance.setA(a);", - "public CtorAndSetterBuilder name(String name)", - "instance.setName(name);"); + "public CtorAndSetterBuilder()", // empty constructor for builder + "public CtorAndSetterBuilder name(String name)", // helpermethod for setter + "public CtorAndSetterBuilder a(int a)", // helpermethod for constructor param + "CtorAndSetter result = new CtorAndSetter(this.a.value());", + "this.name.ifChanged(result::setName);"); + } + + @Test + void shouldPreferConstructorOverSetterWhenBothExist() { + // Given + String packageName = "test"; + String className = "CtorAndSetterSameField"; + String builderClassName = className + "Builder"; + + JavaFileObject sourceFile = + ProcessorTestUtils.simpleBuilderClass( + packageName, + className, + """ + private int a; // could be set via constructor and setter + private String name; + + public CtorAndSetterSameField(int a) { this.a = a; } + + public int getA() { return a; } + public void setA(int a) { this.a = a; } + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + """); + + // When + Compilation compilation = compile(sourceFile); + + // Then + String generatedCode = loadGeneratedSource(compilation, builderClassName); + assertGenerationSucceeded(compilation, builderClassName, generatedCode); + // Expect helper for constructor param and no duplicate setter application for 'a' + ProcessorAsserts.assertContaining( + generatedCode, + "public CtorAndSetterSameFieldBuilder()", + // Expectations on constructor of builder with instance + "public CtorAndSetterSameFieldBuilder(CtorAndSetterSameField instance)", + "this.name = initialValue(instance.getName());", + "this.a = initialValue(instance.getA());", + // Expectations on helper functions + "public CtorAndSetterSameFieldBuilder a(int a)", + "public CtorAndSetterSameFieldBuilder name(String name)", + // Expectations on build function + "CtorAndSetterSameField result = new CtorAndSetterSameField(this.a.value());", + "this.name.ifChanged(result::setName);"); + // expect no setter application for 'a' on build-function + ProcessorAsserts.assertNotContaining(generatedCode, "result.setA(this.a);"); + } + + @Test + void shouldUseAnnotatedConstructorWhenMultipleConstructorsExist() { + // Given + String builderClassName = "AnnotatedCtorChoiceBuilder"; + + JavaFileObject sourceFile = + ProcessorTestUtils.forSource( + """ + package test; + + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilderConstructor; + + @SimpleBuilder + public class AnnotatedCtorChoice { + private int a; + private String name; + private boolean flag; + + @SimpleBuilderConstructor + public AnnotatedCtorChoice(int a, String name) { + this.a = a; + this.name = name; + } + + // Alternative constructor that must NOT be used by the builder + public AnnotatedCtorChoice(int a, String name, boolean flag) { + this.a = a; + this.name = name; + this.flag = flag; + } + + public int getA() { return a; } + public String getName() { return name; } + public boolean isFlag() { return flag; } + + public void setName(String name) { this.name = name; } + } + """); + + // When + Compilation compilation = compile(sourceFile); + + // Then + String generatedCode = loadGeneratedSource(compilation, builderClassName); + assertGenerationSucceeded(compilation, builderClassName, generatedCode); + + // Builder must use the annotated (a, name) constructor + ProcessorAsserts.assertContaining( + generatedCode, + "public AnnotatedCtorChoiceBuilder a(int a)", + "public AnnotatedCtorChoiceBuilder name(String name)", + "AnnotatedCtorChoice result = new AnnotatedCtorChoice(this.a.value(), this.name.value());"); + + // Ensure the alternative constructor (with flag) is not used nor exposed via helpers + ProcessorAsserts.assertNotContaining( + generatedCode, + "public AnnotatedCtorChoiceBuilder flag", + "new AnnotatedCtorChoice(this.a.value(), this.flag.value())"); } @Test @@ -145,9 +299,9 @@ public record PersonRecord(String name, int age) { ProcessorAsserts.assertContaining( generatedCode, "public PersonRecordBuilder name(String name)", - "instance.setName(name);", + "this.name = name;", "public PersonRecordBuilder age(int age)", - "instance.setAge(age);"); + "this.age = age;"); } @Test @@ -189,18 +343,21 @@ void shouldGenerateCollectionSettersAndProviders() { "public WithCollectionsBuilder names(List names)", "public WithCollectionsBuilder names(Supplier> namesSupplier)", "public WithCollectionsBuilder names(String... names)", - "instance.setNames(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)", - "instance.setTags(Set.of(tags));", + "this.tags = changedValue(Set.of(tags));", + "private TrackedValue> tags = unsetValue();", "public WithCollectionsBuilder map(Map map)", "public WithCollectionsBuilder map(Supplier> mapSupplier)", - "instance.setMap(map);"); + "this.map = changedValue(map);", + "private TrackedValue> map = unsetValue();"); } @Test - void shouldDeclareInstanceFieldAsPrivateFinalWithJavadoc() { + void shouldDeclarePerFieldBackingField() { // Given String packageName = "test"; String className = "FieldDoc"; @@ -221,8 +378,10 @@ void shouldDeclareInstanceFieldAsPrivateFinalWithJavadoc() { String generatedCode = loadGeneratedSource(compilation, builderClassName); assertGenerationSucceeded(compilation, builderClassName, generatedCode); + // Expect a private tracked backing field for primitive int setter ProcessorAsserts.assertContaining( - generatedCode, "Inner instance of builder.", "private final FieldDoc instance;"); + generatedCode, "private TrackedValue x = unsetValue();"); + ProcessorAsserts.assertNotContaining(generatedCode, " FieldDoc instance;"); } @Test @@ -253,11 +412,12 @@ void shouldGenerateCreateMethodWithModifiersAndJavadoc() { generatedCode, "public static CreateDocBuilder create()", "Creating a new builder for {@code test.CreateDoc}", - "@return builder for {@code test.CreateDoc}"); + "@return builder for {@code test.CreateDoc}", + "return new CreateDocBuilder();"); } @Test - void shouldGenerateBuildOverrideAndReturnInstance() { + void shouldGenerateBuildOverrideAndReturnAnInstanceWithCalledSetters() { // Given String packageName = "test"; String className = "BuildDoc"; @@ -281,7 +441,15 @@ void shouldGenerateBuildOverrideAndReturnInstance() { assertGenerationSucceeded(compilation, builderClassName, generatedCode); ProcessorAsserts.assertContaining( - generatedCode, "@Override", "public BuildDoc build()", "return instance;"); + generatedCode, + """ + @Override + public BuildDoc build() { + BuildDoc result = new BuildDoc(); + this.x.ifChanged(result::setX); + return result; + } + """); } @Test @@ -306,7 +474,11 @@ void shouldReturnThisFromFluentMethods() { String generatedCode = loadGeneratedSource(compilation, builderClassName); assertGenerationSucceeded(compilation, builderClassName, generatedCode); - ProcessorAsserts.assertContaining(generatedCode, "instance.setName(name);", "return this;"); + ProcessorAsserts.assertContaining( + generatedCode, + "public FluentReturnBuilder name(String name)", + "this.name = changedValue(name);", + "return this;"); } @Test @@ -326,7 +498,9 @@ void shouldContainReturnJavadocForConsumerSupplierAndConsumerByBuilder() { private HelperPlain helperPlain; public String getName() { return name; } public void setName(String name) { this.name = name; } + public HelperAnno getHelper() { return helper; } public void setHelper(HelperAnno helper) { this.helper = helper; } + public HelperPlain getHelperPlain() { return helperPlain; } public void setHelperPlain(HelperPlain helperPlain) { this.helperPlain = helperPlain; } """); @@ -364,33 +538,6 @@ public class HelperPlain { public HelperPlain() {} } "@return current instance of builder"); } - @Test - void shouldGenerateProxyWithMixedParamsAndArrayVarargs() { - // Given - String packageName = "test"; - String className = "MixedProxy"; - String builderClassName = className + "Builder"; - - JavaFileObject sourceFile = - ProcessorTestUtils.simpleBuilderClass( - packageName, - className, - """ - public void doMixed(int a, String b, int[] nums) { /* no-op */ } - """); - - // When - Compilation compilation = compile(sourceFile); - - // Then - String generatedCode = loadGeneratedSource(compilation, builderClassName); - assertGenerationSucceeded(compilation, builderClassName, generatedCode); - - // Expect last parameter mapped to varargs in builder signature and proper call - ProcessorAsserts.assertContaining( - generatedCode, "doMixed(int a, String b, int... nums)", "instance.doMixed(a,b,nums);"); - } - @Test void shouldIncludeNestedPackageInClassJavadoc() { // Given @@ -476,8 +623,9 @@ void shouldGenerateBothConstructorsWithJavadocs() { "Empty constructor of builder for", "Initialisation of builder for", "@param instance object instance for initialisiation", - "this.instance = instance;", - "this.instance = new CtorDoc();"); + "public CtorDocBuilder()", + "public CtorDocBuilder(CtorDoc instance)", + "private TrackedValue x = unsetValue();"); } @Test @@ -495,7 +643,14 @@ void shouldGenerateMethodJavadocsForAllMethodTypes() { private String name; private HelperPlain helperPlain; public String getName() { return name; } + /** + * @param name the name of a specific person + */ public void setName(String name) { this.name = name; } + public HelperPlain getHelperPlain() { return helperPlain; } + /** + * @param helperPlain complex helper object + */ public void setHelperPlain(HelperPlain helperPlain) { this.helperPlain = helperPlain; } public void doSomething(int a, String b) { /* no-op */ } """); @@ -516,23 +671,27 @@ public HelperPlain() {} String generatedCode = loadGeneratedSource(compilation, builderClassName); assertGenerationSucceeded(compilation, builderClassName, generatedCode); - // PROXY method javadoc has @param for each parameter and @return - ProcessorAsserts.assertContaining( - generatedCode, - "Calling doSomething on dto-instance with parameters.", - "@param a", - "@param b", - "@return current instance of builder"); + // Note: Proxy methods are no longer supported; do not assert proxy method javadoc here. // Setter method javadoc and code (default values, when nothing is set) - ProcessorAsserts.assertContaining(generatedCode, "@param name name"); + ProcessorAsserts.assertContaining(generatedCode, "@param name the name of a specific person"); // SUPPLIER method javadoc and code - ProcessorAsserts.assertContaining(generatedCode, "@param nameSupplier supplier for name"); + ProcessorAsserts.assertContaining( + generatedCode, "@param nameSupplier supplier for the name of a specific person"); // CONSUMER method javadoc and code ProcessorAsserts.assertContaining( - generatedCode, "@param helperPlainConsumer consumer providing an instance of helperPlain"); + generatedCode, + "@param helperPlainConsumer consumer providing an instance of complex helper object"); + + // Backing fields present with javadoc + ProcessorAsserts.assertContaining( + generatedCode, + "* Tracked value for name: the name of a specific person.", + "private TrackedValue name = unsetValue();", + "* Tracked value for helperPlain: complex helper object.", + "private TrackedValue helperPlain = unsetValue();"); } @Test @@ -566,7 +725,8 @@ void shouldPropagateFieldJavadocToBuilderParameter() { ProcessorAsserts.assertContaining( generatedCode, "@param name the person name", - "public FieldWithDocBuilder name(String name)"); + "public FieldWithDocBuilder name(String name)", + "private TrackedValue name = unsetValue();"); } @Test @@ -608,10 +768,11 @@ public HelperAnno() {} // Expect consumer-by-builder method using HelperAnnoBuilder and builder.build() ProcessorAsserts.assertContaining( generatedCode, + "private TrackedValue helper = unsetValue();", "helperBuilderConsumer", "HelperAnnoBuilder builder = new HelperAnnoBuilder();", "helperBuilderConsumer.accept(builder);", - "instance.setHelper(builder.build());"); + "this.helper = changedValue(builder.build());"); } @Test @@ -653,7 +814,7 @@ public HelperPlain() {} generatedCode, "HelperPlain consumer = new HelperPlain();", "helperPlainConsumer.accept(consumer);", - "instance.setHelperPlain(consumer);"); + "private TrackedValue helperPlain = unsetValue();"); } @Test @@ -683,7 +844,10 @@ void shouldGenerateSupplierMethodForField() { // Expect supplier-based setter usage ProcessorAsserts.assertContaining( - generatedCode, "nameSupplier", "instance.setName(nameSupplier.get());"); + generatedCode, + "nameSupplier", + "private TrackedValue name = unsetValue();", + "this.name = changedValue(nameSupplier.get());"); } protected Compilation compile(JavaFileObject... sourceFiles) { @@ -759,10 +923,14 @@ public void setAge(int age) { ProcessorAsserts.assertContaining( generatedCode, "public PersonBuilder name(String name)", - "public PersonBuilder age(int age)"); + "public PersonBuilder age(int age)", + "private TrackedValue name = unsetValue();", + "private TrackedValue age = unsetValue();"); } @Test + // TODO: Replace Optional with other generic wrapper type, because for optional there is a + // feature-request void shouldHandleGenericWrapperTypes() { JavaFileObject optionalWrapperClass = @@ -814,6 +982,12 @@ public class OptionalWrapper { "name(String...)", "count(Integer...)", "date(LocalDate...)"); + // Backing fields present + ProcessorAsserts.assertContaining( + optionalBuilder, + "private TrackedValue> name = unsetValue();", + "private TrackedValue> count = unsetValue();", + "private TrackedValue> date = unsetValue();"); } @Test @@ -854,7 +1028,9 @@ public Helper() {} String generatedCode = loadGeneratedSource(compilation, builderClassName); assertGenerationSucceeded(compilation, builderClassName, generatedCode); ProcessorAsserts.assertContaining( - generatedCode, "public UsesOtherPackageHelperBuilder helper(Helper helper)"); + generatedCode, + "public UsesOtherPackageHelperBuilder helper(Helper helper)", + "private TrackedValue helper = unsetValue();"); } @Test @@ -885,13 +1061,13 @@ void shouldHandleSetOfStrings() { ProcessorAsserts.assertContaining( generatedCode, "public HasSetStringBuilder tags(Set tags)", - "instance.setTags(tags);", + "this.tags = changedValue(tags);", "public HasSetStringBuilder tags(Consumer> tagsBuilderConsumer)", - "instance.setTags(builder.build());", + "this.tags = changedValue(builder.build());", "public HasSetStringBuilder tags(String... tags)", - "instance.setTags(Set.of(tags));", + "this.tags = changedValue(Set.of(tags));", "public HasSetStringBuilder tags(Supplier> tagsSupplier)", - "instance.setTags(tagsSupplier.get());"); + "this.tags = changedValue(tagsSupplier.get());"); } @Test @@ -930,13 +1106,13 @@ public Helper() {} ProcessorAsserts.assertContaining( generatedCode, "public HasSetCustomBuilder helpers(Set helpers)", - "instance.setHelpers(helpers);", + "this.helpers = changedValue(helpers);", "public HasSetCustomBuilder helpers(Consumer> helpersBuilderConsumer)", - "instance.setHelpers(builder.build());", + "this.helpers = changedValue(builder.build());", "public HasSetCustomBuilder helpers(Helper... helpers)", - "instance.setHelpers(Set.of(helpers));", + "this.helpers = changedValue(Set.of(helpers));", "public HasSetCustomBuilder helpers(Supplier> helpersSupplier)", - "instance.setHelpers(helpersSupplier.get());"); + "this.helpers = changedValue(helpersSupplier.get());"); } @Test @@ -987,13 +1163,13 @@ void shouldFilterOutNonRelevantMethods() { packageName, className, """ - // valid proxy candidate - public void ok() {} - - // valid setter + // valid setter (having a getter) public void setOk(int ok) {} + public int getOk() { return 0; } // should be filtered + public void nonSetterMethods() {} + public void settingChanged(String nonSetterValue) {} private void setHidden(int hidden) {} public static void setUtil(int util) {} public void setRisky(int risk) throws Exception {} @@ -1007,13 +1183,13 @@ public void setRisky(int risk) throws Exception {} // Then String generatedCode = loadGeneratedSource(compilation, builderClassName); assertGenerationSucceeded(compilation, builderClassName, generatedCode); - // Expect only ok() proxy to be present + // Expect only setter-derived methods to be present (no proxy methods) ProcessorAsserts.assertingResult( generatedCode, - contains("public HasVariousMethodsBuilder ok()"), - contains("instance.ok();"), contains("public HasVariousMethodsBuilder ok(int ok)"), - contains("instance.setOk(ok);"), + contains("this.ok = changedValue(ok);"), + notContains("nonSetterMethods"), + notContains("settingChanged"), notContains("hidden"), notContains("util(int util)"), notContains("risky(int risk)"), @@ -1155,12 +1331,10 @@ void shouldGenerateAnnotationsAndCreateBuildMethods() { assertGenerationSucceeded(compilation, builderClassName, generatedCode); ProcessorAsserts.assertContaining( generatedCode, - "@Generated(", - "@BuilderImplementation(", - "public static AnnoTargetBuilder create()", - "return new AnnoTargetBuilder(instance);", - "public AnnoTarget build()", - "return instance;"); + "@Generated(\"Generated by org.javahelpers.simple.builders.processor.BuilderProcessor\")", + "@BuilderImplementation( forClass = AnnoTarget.class )", + "public static AnnoTargetBuilder create() { return new AnnoTargetBuilder(); }", + "@Override public AnnoTarget build()"); } @Test @@ -1193,10 +1367,8 @@ void shouldGenerateVarargsConvenienceForCollections() { assertGenerationSucceeded(compilation, builderClassName, generatedCode); ProcessorAsserts.assertContaining( generatedCode, - "public HasCollectionsConvenienceBuilder names(String... names)", - "instance.setNames(List.of(names));", - "public HasCollectionsConvenienceBuilder tags(String... tags)", - "instance.setTags(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 @@ -1225,8 +1397,7 @@ void shouldHandleJavaTimeField() { assertGenerationSucceeded(compilation, builderClassName, generatedCode); ProcessorAsserts.assertContaining( generatedCode, - "public HasLocalDateBuilder date(LocalDate date)", - "instance.setDate(date);"); + "public HasLocalDateBuilder date(LocalDate date) { this.date = changedValue(date);"); } @Test @@ -1378,10 +1549,11 @@ void shouldHandleEmptyClass() { String generatedCode = loadGeneratedSource(compilation, builderClassName); assertGenerationSucceeded(compilation, builderClassName, generatedCode); // There should be no builder generated for an empty class - ProcessorAsserts.assertingResult( + ProcessorAsserts.assertContaining( generatedCode, - contains("public EmptyClass build() { return instance; }"), - notContains("return this;")); + "public EmptyClass build()", + "EmptyClass result = new EmptyClass();", + "return result;"); } @Test @@ -1467,7 +1639,7 @@ void shouldBoxPrimitiveTypeArguments() { public void setLongValue(long longValue) { this.longValue = longValue; } public double getDoubleValue() { return doubleValue; } public void setDoubleValue(double doubleValue) { this.doubleValue = doubleValue; } - public boolean getBooleanValue() { return booleanValue; } + public boolean isBooleanValue() { return booleanValue; } public void setBooleanValue(boolean booleanValue) { this.booleanValue = booleanValue; } """); @@ -1905,46 +2077,55 @@ public class GenericDto { } @Test - void shouldGenerateBuilderRetainingFieldspecificTypeParameter() { - // Given - String builderClassName = "GenericFieldDtoBuilder"; - - JavaFileObject genericDtoSource = + void shouldIgnoreSettersWithMethodLevelTypeParameters() { + // Given: a DTO with setters that have their own type parameters + String builderClassName = "DtoWithGenericSettersBuilder"; + JavaFileObject source = ProcessorTestUtils.forSource( """ package test; import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + import java.io.Serializable; @SimpleBuilder - public class GenericFieldDto { - private java.io.Serializable value; - public java.io.Serializable getValue() { return value; } - public void setValue(T value) { this.value = value; } + public class DtoWithGenericSetters { + private String name; + private Object fieldWithOwnGeneric; + private T fieldWithClassGeneric; + private T fieldWithExtendedClassGeneric; + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public T getFieldWithClassGeneric() { return fieldWithClassGeneric; } + public void setFieldWithClassGeneric(T fieldWithClassGeneric) { this.fieldWithClassGeneric = fieldWithClassGeneric; } + public T getFieldWithExtendedClassGeneric() { return fieldWithExtendedClassGeneric; } + public void setFieldWithExtendedClassGeneric(F fieldWithExtendedClassGeneric) { + this.fieldWithExtendedClassGeneric = fieldWithExtendedClassGeneric; + } + public Object getFieldWithOwnGeneric() { return fieldWithOwnGeneric; } + // This setter has its own type parameter - should be ignored + public void setFieldWithOwnGeneric(F fieldWithOwnGeneric) { + this.fieldWithOwnGeneric = fieldWithOwnGeneric; + } } """); // When - Compilation compilation = compile(genericDtoSource); + Compilation compilation = compile(source); // Then String generatedCode = loadGeneratedSource(compilation, builderClassName); - assertGenerationSucceeded(compilation, builderClassName, generatedCode); + assertThat(compilation).succeededWithoutWarnings(); ProcessorAsserts.assertingResult( generatedCode, - // builder preserves type parameter T - contains("class GenericFieldDtoBuilder"), - // new import for Serializable - contains("import java.io.Serializable;"), - // setter keeps T - contains("public GenericFieldDtoBuilder value(T value)"), - // supplier-based setter retains T - contains( - "public GenericFieldDtoBuilder value(Supplier valueSupplier)"), - // no direct consumer possible for unknown T (no empty ctor info) - notContains("GenericFieldDtoBuilder value(Consumer"), - // build returns GenericFieldDto - contains("public GenericFieldDto build()"), - // create() exposes no generic, because the Builder is not generic - contains("public static GenericFieldDtoBuilder create()")); + // name field should be present + contains("public DtoWithGenericSettersBuilder name"), + // fieldWithClassGeneric setter should be present + contains("public DtoWithGenericSettersBuilder fieldWithClassGeneric"), + // fieldWithOwnGeneric setter should be ignored because it has method-level type parameter + // + notContains("public DtoWithGenericSettersBuilder fieldWithOwnGeneric"), + // fieldWithExtendedClassGeneric setter should be ignored because it has method-level type + // parameter , even if it extends the type parameter of the class + notContains("public DtoWithGenericSettersBuilder fieldWithExtendedClassGeneric")); } @Test diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/JavaLangAnalyserTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/JavaLangAnalyserTest.java index 76a073f3..b9893872 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/JavaLangAnalyserTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/JavaLangAnalyserTest.java @@ -203,4 +203,19 @@ void extractParamJavaDoc_shouldAddSpaceBetweenLines() { "first line second line third line", JavaLangAnalyser.extractParamJavaDoc(javaDoc, parameter)); } + + @Test + void findGetterForField_shouldReturnEmpty_whenDtoTypeIsNull() { + assertTrue(JavaLangAnalyser.findGetterForField(null, "fieldName", null, null, null).isEmpty()); + } + + @Test + void findGetterForField_shouldReturnEmpty_whenFieldNameIsNull() { + assertTrue(JavaLangAnalyser.findGetterForField(null, null, null, null, null).isEmpty()); + } + + @Test + void findGetterForField_shouldReturnEmpty_whenFieldTypeMirrorIsNull() { + assertTrue(JavaLangAnalyser.findGetterForField(null, "fieldName", null, null, null).isEmpty()); + } }