Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
fff0ff1
Make @SimpleBuilder @Inherited to match documentation (#244)
AndreasIgel Aug 15, 2026
13a8fc8
Rename to BuilderAnnotationInheritanceTest and cover Template inherit…
AndreasIgel Aug 15, 2026
105310a
Refactoring code to move assertNoBuilderGenerated to common asserts
AndreasIgel Aug 15, 2026
2c83b78
fixing codeformat
AndreasIgel Aug 15, 2026
aca9ef6
Document @Inherited behavior of @SimpleBuilder.Template
AndreasIgel Aug 15, 2026
bd3bf93
Document options-inheritance limitation for inherited subclass builders
AndreasIgel Aug 15, 2026
e8e0352
Fix issue reference: #245 -> #248
AndreasIgel Aug 15, 2026
9776343
Clarify @SimpleBuilder vs @SimpleBuilder.Template usage in docs
AndreasIgel Aug 15, 2026
4b7723a
Link issue #248 from @SimpleBuilder.Template inheritance Javadoc
AndreasIgel Aug 15, 2026
218ea79
Remove redundant troubleshooting item about @SimpleBuilder.Template t…
AndreasIgel Aug 15, 2026
bd6020d
Merge remote-tracking branch 'upstream/main'
devin-ai-integration[bot] Aug 16, 2026
62d1269
Merge remote-tracking branch 'upstream/main'
devin-ai-integration[bot] Aug 16, 2026
ec99605
Merge upstream main
devin-ai-integration[bot] Aug 22, 2026
640bc82
Merge branch 'java-helpers:main' into main
AndreasIgel Sep 2, 2026
1830728
feat: support external Eclipse formatter profile via -Asimplebuilder.…
devin-ai-integration[bot] Sep 5, 2026
22bf38e
refactor: simplify formatter profile fallback loading
devin-ai-integration[bot] Sep 5, 2026
2e1b3a8
refactor: address review on formatter profile loading (#278)
devin-ai-integration[bot] Sep 5, 2026
72d4b52
Merge upstream/main: resolve test conflicts
AndreasIgel Sep 12, 2026
69c1fe8
Merge remote-tracking branch 'upstream/main' into devin/1788610730-fo…
AndreasIgel Sep 19, 2026
2c17ea7
refactor: address formatter profile review
AndreasIgel Sep 19, 2026
07f948f
test: centralize temporary formatter profiles
AndreasIgel Sep 19, 2026
3f363e2
Fixing sonarIssue
AndreasIgel Sep 19, 2026
4771586
Merge remote-tracking branch 'upstream/main' into devin/1788610730-fo…
AndreasIgel Sep 20, 2026
b015bf3
refactor: address formatter review feedback
AndreasIgel Sep 20, 2026
0923b8a
refactor: simplify formatter fallback wiring
AndreasIgel Sep 20, 2026
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
19 changes: 19 additions & 0 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -1135,6 +1135,24 @@ Per-annotation override:
public class PersonDto { ... }
```

#### `formatterProfile`

**Default**: `eclipse-java-format.xml` (bundled) | **Compiler Option**: `-Asimplebuilder.formatterProfile=path/to/profile.xml`

> **Note**: This is a **processor-level option** only. It cannot be set per-annotation via
> `@SimpleBuilder.Options`.

Specifies an external Eclipse formatter profile for JDT formatting. This option only affects [`JDT` formatting](#formattingmode); it is ignored for `LIGHTWEIGHT` and `NONE`. A file system path is tried first, then a classpath resource. If neither can be loaded, a warning is logged and the bundled `eclipse-java-format.xml` profile is used.

**Example**:
```bash
# Maven
mvn compile -Dsimplebuilder.formatterProfile=config/eclipse-formatter.xml

# Or via compiler arg
-Asimplebuilder.formatterProfile=config/eclipse-formatter.xml
```
Comment thread
AndreasIgel marked this conversation as resolved.

Comment thread
AndreasIgel marked this conversation as resolved.
## Examples

### Minimal Builder
Expand Down Expand Up @@ -1533,6 +1551,7 @@ methodAccess = AccessModifier.PRIVATE

# Performance Optimization
-Asimplebuilder.formattingMode=JDT|LIGHTWEIGHT|NONE
-Asimplebuilder.formatterProfile=path/to/profile.xml
```

### Complete Options Example
Expand Down
1 change: 1 addition & 0 deletions docs/DEBUG_LOGGING.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ When debug logging is enabled, you'll see detailed output with visual separators
[INFO] [DEBUG] │ │ └─ Fields added: 2 fields
[INFO] [DEBUG] │ ├─ Adding Methods for 4 candidates
[INFO] [DEBUG] │ │ └─ 4 Methods added
[INFO] [DEBUG] │ ├─ Using JDT source formatting with Eclipse formatter profile 'eclipse-java-format.xml'.
[INFO] [DEBUG] │ ├─ Writing builder class to file: org.example.PersonDtoBuilder
[INFO] [DEBUG] │ └─ Successfully generated builder: PersonDtoBuilder
[INFO] [DEBUG] Processing element: OrderDto
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,7 @@ public synchronized void init(ProcessingEnvironment processingEnv) {
logger.debug("Loaded global configuration from compiler arguments: %s", globalConfig);

this.context = new ProcessingContext(logger, globalConfig, processingEnv);
this.codeGenerator =
new RoasterCodeGenerator(processingEnv, logger, context.getPerformanceTracker());
this.codeGenerator = new RoasterCodeGenerator(context, processingEnv);
this.jacksonModuleGenerator = new JacksonModuleGenerator(processingEnv, logger, globalConfig);

// Initialize GeneratorRegistry once during processor initialization
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.processing.ProcessingEnvironment;
Expand All @@ -59,8 +60,7 @@
import org.javahelpers.simple.builders.processor.model.type.NestedTypeDto;
import org.javahelpers.simple.builders.processor.model.type.TypeName;
import org.javahelpers.simple.builders.processor.model.type.TypeNameArray;
import org.javahelpers.simple.builders.processor.processing.logging.PerformanceTracker;
import org.javahelpers.simple.builders.processor.processing.logging.ProcessingLogger;
import org.javahelpers.simple.builders.processor.processing.ProcessingContext;
import org.javahelpers.simple.builders.processor.util.ImportCollector;
import org.jboss.forge.roaster.Roaster;
import org.jboss.forge.roaster.model.source.AnnotationSource;
Expand All @@ -74,31 +74,26 @@

/** Roaster-based code generator for builder source files. */
public class RoasterCodeGenerator {
/** Processing environment for accessing filer and element utilities. */
private final ProcessingEnvironment processingEnv;

/** Logger for debug output during code generation. */
private final ProcessingLogger logger;
/** Processing context providing logger, performance tracking, type lookup and profile. */
private final ProcessingContext context;

/** Performance tracker for sub-phase timing (Source Construction, File Writing). */
private final PerformanceTracker performanceTracker;
/** Processing environment for creating generated source files via its filer. */
private final ProcessingEnvironment processingEnv;

/** Cached formatters per formatting mode (at most 3 instances, created lazily). */
private final EnumMap<FormattingMode, RoasterSourceFormatter> formatterCache =
new EnumMap<>(FormattingMode.class);

/**
* Constructor for RoasterCodeGenerator.
* Creates a code generator using the given processing context and environment.
*
* @param processingEnv Processing environment for accessing filer and element utilities
* @param logger Logger for debug output
* @param tracker Performance tracker for sub-phase timing
* @param context processing context providing logger, performance tracking, type lookup and
* formatter profile
* @param processingEnv processing environment providing the filer for generated source files
*/
public RoasterCodeGenerator(
ProcessingEnvironment processingEnv, ProcessingLogger logger, PerformanceTracker tracker) {
this.processingEnv = processingEnv;
this.logger = logger;
this.performanceTracker = tracker;
public RoasterCodeGenerator(ProcessingContext context, ProcessingEnvironment processingEnv) {
this.context = Objects.requireNonNull(context, "context must not be null");
this.processingEnv = Objects.requireNonNull(processingEnv, "processingEnv must not be null");
}

/**
Expand All @@ -111,7 +106,7 @@ public RoasterCodeGenerator(
* @return a cached or new formatter instance
*/
private RoasterSourceFormatter getFormatter(FormattingMode mode) {
return formatterCache.computeIfAbsent(mode, m -> new RoasterSourceFormatter(logger, m));
return formatterCache.computeIfAbsent(mode, context::createSourceFormatter);
}

/**
Expand All @@ -121,66 +116,66 @@ private RoasterSourceFormatter getFormatter(FormattingMode mode) {
* @throws BuilderException if there is an error in source code generation
*/
public void generateClass(GenerationTargetClassDto classDef) throws BuilderException {
logger.debugStartOperation(
context.debugStartOperation(
"Code generation for class: %s", classDef.getTypeName().getClassName());

String sourceCode;
try {
performanceTracker.startPhase();
context.startPerformancePhase();
JavaClassSource source = buildClassSource(classDef);
performanceTracker.startPhase();
context.startPerformancePhase();
String unformatted = source.toUnformattedString();
performanceTracker.endPhase(PHASE_STRING_GENERATION);
performanceTracker.startPhase();
context.endPerformancePhase(PHASE_STRING_GENERATION);
context.startPerformancePhase();
sourceCode = formatSource(unformatted, classDef.getFormattingMode());
// Roaster renders some java.lang annotations (e.g. @SuppressWarnings, @Deprecated with
// members) with their FQN (@java.lang.SuppressWarnings) even though java.lang types don't
// need qualification. Fix this by replacing @java.lang.Xxx with @Xxx for known annotations.
sourceCode = sourceCode.replace("@java.lang.SuppressWarnings", "@SuppressWarnings");
sourceCode = sourceCode.replace("@java.lang.Deprecated", "@Deprecated");
performanceTracker.endPhase(PHASE_FORMATTING);
performanceTracker.endPhase(PHASE_SOURCE_CONSTRUCTION);
context.endPerformancePhase(PHASE_FORMATTING);
context.endPerformancePhase(PHASE_SOURCE_CONSTRUCTION);
} catch (RuntimeException ex) {
// Rendering failures (e.g. RoasterMapperException) are RuntimeExceptions. Convert them into
// a BuilderException so callers can isolate the failure to this single class and keep
// generating the remaining builders instead of aborting the whole processing round.
throw new BuilderException(null, ex);
}
performanceTracker.startPhase();
context.startPerformancePhase();
writeClassToFile(sourceCode, classDef);
performanceTracker.endPhase(PHASE_FILE_WRITING);
context.endPerformancePhase(PHASE_FILE_WRITING);

logger.debugEndOperation(
context.debugEndOperation(
"Successfully generated class: %s", classDef.getTypeName().getClassName());
}

private JavaClassSource buildClassSource(GenerationTargetClassDto classDef) {
performanceTracker.startPhase();
context.startPerformancePhase();
JavaClassSource source = createJavaClassSource(classDef);
addClassMetadata(source, classDef);
appendFields(source, classDef);
appendConstructors(source, classDef);
appendMethods(source, classDef);
appendNestedTypes(source, classDef);
applyClassAnnotations(source, classDef);
performanceTracker.endPhase(PHASE_ELEMENT_BUILDING);
context.endPerformancePhase(PHASE_ELEMENT_BUILDING);
return source;
}

private void applyClassAnnotations(JavaClassSource source, GenerationTargetClassDto classDef) {
performanceTracker.startPhase();
context.startPerformancePhase();
if (CollectionUtils.isNotEmpty(classDef.getClassAnnotations())) {
// Adding class annotations
applyAnnotations(source, classDef.getClassAnnotations());
logger.debug("Class-level annotations added");
context.debug("Class-level annotations added");
}
performanceTracker.endPhase(PHASE_CLASS_ANNOTATIONS);
context.endPerformancePhase(PHASE_CLASS_ANNOTATIONS);
}

private JavaClassSource createJavaClassSource(GenerationTargetClassDto classDef) {
performanceTracker.startPhase();
context.startPerformancePhase();
if (CollectionUtils.isNotEmpty(classDef.getGenerics())) {
logger.debug("Class has %d generic type parameter(s)", classDef.getGenerics().size());
context.debug("Class has %d generic type parameter(s)", classDef.getGenerics().size());
}

JavaClassSource source = Roaster.create(JavaClassSource.class);
Expand All @@ -203,13 +198,13 @@ private JavaClassSource createJavaClassSource(GenerationTargetClassDto classDef)
}
}

logger.debug("JavaClassSource created");
performanceTracker.endPhase(PHASE_CLASS_CREATION);
context.debug("JavaClassSource created");
context.endPerformancePhase(PHASE_CLASS_CREATION);
return source;
}

private void addClassMetadata(JavaClassSource source, GenerationTargetClassDto classDef) {
performanceTracker.startPhase();
context.startPerformancePhase();
applyJavadoc(source, classDef.getClassJavadoc());
applyVisibility(source, classDef.getClassAccessModifier());
applySuperType(source, classDef.getSuperType());
Expand All @@ -219,20 +214,20 @@ private void addClassMetadata(JavaClassSource source, GenerationTargetClassDto c
source.addInterface(RoasterMapper.mapInterfaceToTypeName(interfaceName));
}

logger.debug("Class metadata added");
performanceTracker.endPhase(PHASE_CLASS_METADATA);
context.debug("Class metadata added");
context.endPerformancePhase(PHASE_CLASS_METADATA);
}

private void appendFields(JavaClassSource source, GenerationTargetClassDto classDef) {
performanceTracker.startPhase();
logger.debugStartOperation("Generating %d fields", classDef.getClassFields().size());
context.startPerformancePhase();
context.debugStartOperation("Generating %d fields", classDef.getClassFields().size());

for (ClassFieldDto fieldDto : classDef.getClassFields()) {
appendField(source, fieldDto);
}

logger.debugEndOperation("Fields added: %d fields", source.getFields().size());
performanceTracker.endPhase(PHASE_FIELDS);
context.debugEndOperation("Fields added: %d fields", source.getFields().size());
context.endPerformancePhase(PHASE_FIELDS);
}

private void appendField(JavaClassSource source, ClassFieldDto fieldDto) {
Expand All @@ -245,15 +240,15 @@ private void appendField(JavaClassSource source, ClassFieldDto fieldDto) {
}

private void appendConstructors(JavaClassSource source, GenerationTargetClassDto classDef) {
performanceTracker.startPhase();
logger.debugStartOperation("Generating %d constructors", classDef.getConstructors().size());
context.startPerformancePhase();
context.debugStartOperation("Generating %d constructors", classDef.getConstructors().size());

for (ConstructorDto constructor : classDef.getConstructors()) {
appendConstructor(source, constructor);
}

logger.debugEndOperation("Constructors added: %d", classDef.getConstructors().size());
performanceTracker.endPhase(PHASE_CONSTRUCTORS);
context.debugEndOperation("Constructors added: %d", classDef.getConstructors().size());
context.endPerformancePhase(PHASE_CONSTRUCTORS);
}

private void appendConstructor(JavaClassSource source, ConstructorDto constructor) {
Expand All @@ -269,19 +264,19 @@ private void appendConstructor(JavaClassSource source, ConstructorDto constructo
}

private void appendMethods(JavaClassSource source, GenerationTargetClassDto classDef) {
performanceTracker.startPhase();
logger.debugStartOperation("Generating %d method candidates", classDef.getMethods().size());
context.startPerformancePhase();
context.debugStartOperation("Generating %d method candidates", classDef.getMethods().size());

// Resolve method conflicts by signature and priority
List<MethodDto> resolvedMethods = resolveMethodConflicts(classDef.getMethods());
logger.debug("Resolved to %d methods after conflict resolution", resolvedMethods.size());
context.debug("Resolved to %d methods after conflict resolution", resolvedMethods.size());

for (MethodDto methodDto : resolvedMethods) {
appendMethod(source, methodDto, false, false);
}

logger.debugEndOperation("Methods added: %d", resolvedMethods.size());
performanceTracker.endPhase(PHASE_METHODS);
context.debugEndOperation("Methods added: %d", resolvedMethods.size());
context.endPerformancePhase(PHASE_METHODS);
}

/**
Expand Down Expand Up @@ -312,7 +307,7 @@ private List<MethodDto> resolveMethodConflicts(List<MethodDto> methods) {
if (existing == null) {
signatureToMethod.put(signature, method);
} else {
logger.warning(
context.warning(
" Unexpected duplicate method signature: '%s' — keeping first occurrence (safety net)",
signature);
}
Expand All @@ -333,16 +328,16 @@ private void appendMethod(
}

private void appendNestedTypes(JavaClassSource source, GenerationTargetClassDto classDef) {
performanceTracker.startPhase();
context.startPerformancePhase();
if (CollectionUtils.isNotEmpty(classDef.getNestedTypes())) {
logger.debugStartOperation("Generating %d nested type(s)", classDef.getNestedTypes().size());
context.debugStartOperation("Generating %d nested type(s)", classDef.getNestedTypes().size());
for (NestedTypeDto nestedType : classDef.getNestedTypes()) {
appendNestedType(source, nestedType);
logger.debug("Generated nested type: %s", nestedType.getTypeName());
context.debug("Generated nested type: %s", nestedType.getTypeName());
}
logger.debugEndOperation("Nested types added");
context.debugEndOperation("Nested types added");
}
performanceTracker.endPhase(PHASE_NESTED_TYPES);
context.endPerformancePhase(PHASE_NESTED_TYPES);
}

private void appendNestedType(JavaClassSource source, NestedTypeDto nestedType) {
Expand Down Expand Up @@ -575,7 +570,7 @@ private String formatSource(String rawSource, FormattingMode mode) {

private void writeClassToFile(String sourceCode, GenerationTargetClassDto classDef)
throws BuilderException {
logger.debug(
context.debug(
"Writing class to file: %s.%s",
classDef.getTypeName().getPackageName(), classDef.getTypeName().getClassName());

Expand Down Expand Up @@ -619,10 +614,10 @@ private void writeClassToFile(String sourceCode, GenerationTargetClassDto classD
*/
private boolean builderClassAlreadyExists(String qualifiedName) {
try {
TypeElement existingType = processingEnv.getElementUtils().getTypeElement(qualifiedName);
TypeElement existingType = context.getTypeElement(qualifiedName);
return existingType != null;
} catch (Exception e) {
logger.debug(
context.debug(
"Error checking if builder class '%s' already exists: %s",
qualifiedName, StringUtils.isNotBlank(e.getMessage()) ? e.getMessage() : "No message");
return false;
Expand Down
Loading
Loading