Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Simple Builders is a Java [annotation processor](https://docs.oracle.com/en/java
- **Fluent API**: Clean, chainable API for object construction
- **Collections Support**: Built-in support for collections and maps
- **Nested Builders**: Automatic generation of nested object builders
- **Annotation Preservation**: Validation annotations are automatically copied to builder methods

## Requirements

Expand Down Expand Up @@ -120,6 +121,41 @@ Person person = PersonBuilder.create()
.build();
```

#### Validation Annotations

Simple Builders preserves validation annotations on your builder methods:

```java
import jakarta.validation.constraints.*;

@SimpleBuilder
public class User {
private String email;
private int age;

public String getEmail() { return email; }
public void setEmail(@Email @NotNull String email) {
this.email = email;
}

public int getAge() { return age; }
public void setAge(@Min(18) int age) {
this.age = age;
}
}
```

The generated builder preserves these annotations:

```java
User user = UserBuilder.create()
.email("user@example.com") // @Email and @NotNull are on the parameter
.age(25) // @Min(18) is on the parameter
.build();
```

This ensures validation frameworks work seamlessly with builder-generated objects.

### Collections and Nested Objects

Simple Builders provides special handling for collections and nested objects:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* 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.LinkedHashMap;
import java.util.Map;

/**
* DTO representing an annotation to be copied from the target class field to the builder class
* field. Contains the annotation type and its members in a plain format suitable for code
* generation.
*/
public class AnnotationDto {
/** The annotation type (fully qualified name). */
private TypeName annotationType;

/**
* Annotation members (parameters) as key-value pairs. The values are stored as code strings ready
* for generation (e.g., "\"value\"", "123", "{1, 2, 3}"). LinkedHashMap preserves declaration
* order.
*/
private final Map<String, String> members = new LinkedHashMap<>();

/**
* Gets the annotation type.
*
* @return the type of the annotation
*/
public TypeName getAnnotationType() {
return annotationType;
}

/**
* Sets the annotation type.
*
* @param annotationType the type of the annotation
*/
public void setAnnotationType(TypeName annotationType) {
this.annotationType = annotationType;
}

/**
* Gets the annotation members (parameters) as key-value pairs.
*
* @return map of member names to their code string values
*/
public Map<String, String> getMembers() {
return members;
}

/**
* Adds an annotation member (parameter).
*
* @param name the member name
* @param value the member value as a code string
*/
public void addMember(String name, String value) {
this.members.put(name, value);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@

package org.javahelpers.simple.builders.processor.dtos;

import java.util.ArrayList;
import java.util.List;

/** MethodParameterDto contains all information for generating parameters in method headers. */
public class MethodParameterDto {
/** Name of parameter. */
Expand All @@ -32,6 +35,9 @@ public class MethodParameterDto {
/** Type of parameter. */
private TypeName parameterType;

/** Annotations to be applied to this parameter. */
private final List<AnnotationDto> annotations = new ArrayList<>();

/**
* Getting name of parameter
*
Expand Down Expand Up @@ -67,4 +73,22 @@ public TypeName getParameterType() {
public void setParameterTypeName(TypeName parameterType) {
this.parameterType = parameterType;
}

/**
* Returns the list of annotations to be applied to this parameter.
*
* @return the list of annotations
*/
public List<AnnotationDto> getAnnotations() {
return annotations;
}

/**
* Adds an annotation to be applied to this parameter.
*
* @param annotation the annotation to add
*/
public void addAnnotation(AnnotationDto annotation) {
this.annotations.add(annotation);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -234,11 +234,12 @@ private static boolean isMethodRelevantForBuilder(
}

private static void addAdditionalHelperMethodsForField(
FieldDto result, String fieldName, TypeName fieldType) {
FieldDto result, String fieldName, TypeName fieldType, List<AnnotationDto> annotations) {
// Check for String type (not array) and add format method
if (isString(fieldType) && !(fieldType instanceof TypeNameArray)) {
result.addMethod(
createStringFormatMethodWithTransform(fieldName, "String.format(format, args)"));
createStringFormatMethodWithTransform(
fieldName, "String.format(format, args)", annotations));
}

// Only process generic types (List, Set, Map, Optional, etc.)
Expand All @@ -265,16 +266,16 @@ private static void addAdditionalHelperMethodsForField(
result.addMethod(
createFieldSetterWithTransform(fieldName, "Map.ofEntries(%s)", mapEntryType));
} else if (isOptional(fieldType) && innerTypesCnt == 1) {
// Add setter that accepts the inner type T and wraps it in Optional.of()
// Add setter that accepts the inner type T and wraps it in Optional.ofNullable()
result.addMethod(
createFieldSetterWithTransform(fieldName, "Optional.of(%s)", innerTypes.get(0)));
createFieldSetterWithTransform(fieldName, "Optional.ofNullable(%s)", innerTypes.get(0)));

// If Optional<String>, add format method
TypeName innerType = innerTypes.get(0);
if (isString(innerType)) {
result.addMethod(
createStringFormatMethodWithTransform(
fieldName, "Optional.of(String.format(format, args))"));
fieldName, "Optional.of(String.format(format, args))", List.of()));
}
}
}
Expand Down Expand Up @@ -547,22 +548,49 @@ private static Optional<FieldDto> createFieldDto(
JavaLangAnalyser.findGetterForField(dtoType, fieldName, fieldTypeMirror, context)
.ifPresent(getter -> field.setGetterName(getter.getSimpleName().toString()));

// Add basic setter method
field.addMethod(createFieldSetterWithTransform(fieldName, null, fieldType));
// Extract annotations from the field parameter
List<AnnotationDto> annotations = FieldAnnotationExtractor.extractAnnotations(param, context);

// Add basic setter method with annotations
field.addMethod(createFieldSetterWithTransform(fieldName, null, fieldType, annotations));

// Add consumer/supplier/helper methods
addConsumerMethodsForField(field, fieldName, fieldType, param, fieldTypeElement, context);
addSupplierMethodsForField(field, fieldName, fieldType, fieldTypeElement);
addAdditionalHelperMethodsForField(field, fieldName, fieldType);
addAdditionalHelperMethodsForField(field, fieldName, fieldType, annotations);

return Optional.of(field);
}

/**
* Creates a field setter method with optional transform, without annotations.
*
* @param fieldName the name of the field
* @param transform optional transform expression (e.g., "Optional.of(%s)")
* @param fieldType the type of the field
* @return the method DTO for the setter
*/
private static MethodDto createFieldSetterWithTransform(
String fieldName, String transform, TypeName fieldType) {
return createFieldSetterWithTransform(fieldName, transform, fieldType, List.of());
}

/**
* Creates a field setter method with optional transform and annotations.
*
* @param fieldName the name of the field
* @param transform optional transform expression (e.g., "Optional.of(%s)")
* @param fieldType the type of the field
* @param annotations annotations to apply to the parameter
* @return the method DTO for the setter
*/
private static MethodDto createFieldSetterWithTransform(
String fieldName, String transform, TypeName fieldType, List<AnnotationDto> annotations) {
MethodParameterDto parameter = new MethodParameterDto();
parameter.setParameterName(fieldName);
parameter.setParameterTypeName(fieldType);
// Add annotations to the parameter
annotations.forEach(parameter::addAnnotation);
MethodDto methodDto = new MethodDto();
methodDto.setMethodName(fieldName);
methodDto.addParameter(parameter);
Expand Down Expand Up @@ -724,12 +752,14 @@ private static MethodDto createFieldSupplier(String fieldName, TypeName fieldTyp
}

private static MethodDto createStringFormatMethodWithTransform(
String fieldName, String transform) {
String fieldName, String transform, List<AnnotationDto> annotations) {
TypeName stringType = new TypeName("java.lang", "String");

MethodParameterDto formatParam = new MethodParameterDto();
formatParam.setParameterName("format");
formatParam.setParameterTypeName(stringType);
// Apply annotations to the format parameter (it's a String value)
annotations.forEach(formatParam::addAnnotation);

MethodParameterDto argsParam = new MethodParameterDto();
argsParam.setParameterName("args");
Expand Down
Loading
Loading