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
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,25 @@
*/
String jacksonModulePackage() default "";

/**
* The package generated builder classes are placed in. <br>
* Useful to collect builders in a central package or to generate a builder for a type whose
* package is not suitable, e.g. a third-party package.
*
* <p>Example: {@code "com.example.generated"}
*
* <p>Only members of the target type that are accessible from the configured package are used
* for builder generation - package-private and protected constructors, setters and getters are
* then not reachable and are treated as if absent.
*
* <p>Default: "" (empty - the package of the annotated type, resp. of the element carrying
* {@code @SimpleBuilderFor}) <br>
* Annotation-only: a global value would collapse all generated builders into a single package.
*
* @return the package generated builders are placed in
*/
String packageName() default "";

/**
* Generate Javadoc comments on the generated builder class and its members. <br>
* When disabled, no class, field, constructor or method Javadoc is emitted, producing smaller
Expand Down
27 changes: 27 additions & 0 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -1045,6 +1045,33 @@ public class OwnerDto {

---

#### `packageName`

**Default**: `""` (empty — the package of the annotated type, resp. of the element carrying
`@SimpleBuilderFor`) | **Annotation only** — a global `-Asimplebuilder.packageName` is
deliberately not supported: it would collapse all generated builders into a single package.

Places generated builder classes into the given package instead of the default package.
Useful to collect builders in a central package, or to keep a third-party target's builder
in your own namespace when using `@SimpleBuilderFor`.

Only members of the target type that are accessible from the configured package are used for
builder generation — package-private and protected constructors, setters and getters are
then not reachable and are treated as if absent. The value must be a valid Java package
name; an invalid value produces a compile-time diagnostic.

**Example**:
```java
@SimpleBuilderFor(
value = ExternalUser.class,
options = @SimpleBuilder.Options(packageName = "com.example.generated"))
public class ExternalBuilders { }

// Generated class: com.example.generated.ExternalUserBuilder
```

---

#### `setterSuffix`

**Default**: `""` (empty) | **Compiler Option**: `-Asimplebuilder.setterSuffix=customPrefix`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,14 +305,15 @@ private Optional<ElementToGenerate> planGenerationOfAnnotatedElement(
return Optional.empty();
}
alreadyPlannedBuilders.add(
builderTypeName(annotatedElement, context.getPackageName(annotatedElement), config));
builderTypeName(
annotatedElement, effectiveBuilderPackage(annotatedElement, config), config));
return Optional.of(new ElementToGenerate(annotatedElement, config, annotatedElement));
}

/**
* Expands a {@code @SimpleBuilderFor} holder into the external types listed in its {@code value}
* attribute and plans a builder for each of them. The generated builder is placed in the holder's
* package.
* attribute and plans a builder for each of them. The generated builder is placed in the
* configured {@code packageName} or the holder's package.
*/
private List<ElementToGenerate> planGenerationOfTypeByHolder(
Element holder,
Expand All @@ -334,7 +335,7 @@ private List<ElementToGenerate> planGenerationOfTypeByHolder(
BuilderConfiguration config = reader.resolveHolderConfiguration(holder);
tracker.endPhase(PHASE_CONFIGURATION_RESOLUTION);

String builderPackage = context.getPackageName(holder);
String builderPackage = effectiveBuilderPackage(holder, config);
List<ElementToGenerate> results = new ArrayList<>();
for (TypeElement target : targets) {
planExternalTarget(target, holder, config, builderPackage, alreadyPlannedBuilders)
Expand Down Expand Up @@ -441,9 +442,11 @@ private void registerGeneratedTypes(List<ElementToGenerate> elementsToGenerate)
}
scopeResolver.registerGeneratedBuilder(
new TypeName(context.getPackageName(targetType), targetType.getSimpleName().toString()),
new TypeName(
context.getPackageName(elementToGenerate.reportingElement()),
targetType.getSimpleName() + elementToGenerate.config().getBuilderSuffix()));
builderTypeName(
targetType,
effectiveBuilderPackage(
elementToGenerate.reportingElement(), elementToGenerate.config()),
elementToGenerate.config()));
}
}

Expand Down Expand Up @@ -545,12 +548,22 @@ private record ElementToGenerate(
Element element, BuilderConfiguration config, Element reportingElement) {}

/**
* The package the builder is generated into: the package of the element the generation is
* reported on - the {@code @SimpleBuilderFor} holder for external types, the annotated type
* itself otherwise.
* The package the builder is generated into: the configured {@code packageName} option when set,
* otherwise the package of the element the generation is reported on - the
* {@code @SimpleBuilderFor} holder for external types, the annotated type itself otherwise.
*/
private String builderPackageOf(ElementToGenerate elementToGenerate) {
return context.getPackageName(elementToGenerate.reportingElement());
return effectiveBuilderPackage(
elementToGenerate.reportingElement(), elementToGenerate.config());
}

/**
* The effective builder package: the configured {@code packageName} option when set, otherwise
* the package of the given element.
*/
private String effectiveBuilderPackage(Element reportingElement, BuilderConfiguration config) {
String configured = config.getPackageName();
return configured == null ? context.getPackageName(reportingElement) : configured;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ public record BuilderConfiguration(
OptionState generateJacksonModule,
OptionState generateJavaDoc,
String jacksonModulePackage,
String packageName,
PackageScopes builderGenerationPackages,
PackageScopes builderUsagePackages,
String builderSuffix,
Expand Down Expand Up @@ -263,6 +264,16 @@ public String getJacksonModulePackage() {
return jacksonModulePackage;
}

/**
* The package generated builders are placed in, or {@code null} when the default applies (the
* package of the annotated type, resp. of the {@code @SimpleBuilderFor} holder).
*
* @return the configured builder package, or {@code null} if unset
*/
public String getPackageName() {
return packageName;
}

/**
* Returns the parsed set of builder generation package scopes.
*
Expand Down Expand Up @@ -373,6 +384,7 @@ public BuilderConfiguration merge(BuilderConfiguration other) {
mergeOptionState(other.generateJacksonModule, this.generateJacksonModule))
.generateJavaDoc(mergeOptionState(other.generateJavaDoc, this.generateJavaDoc))
.jacksonModulePackage(mergeString(other.jacksonModulePackage, this.jacksonModulePackage))
.packageName(mergeString(other.packageName, this.packageName))
.builderGenerationPackages(
overrideScopes(other.builderGenerationPackages, this.builderGenerationPackages))
.builderUsagePackages(overrideScopes(other.builderUsagePackages, this.builderUsagePackages))
Expand Down Expand Up @@ -462,6 +474,7 @@ public String toString() {
.appendValueIfSet("generateJacksonModule", generateJacksonModule)
.appendValueIfSet("generateJavaDoc", generateJavaDoc)
.appendIfNotEmpty("jacksonModulePackage", jacksonModulePackage)
.appendIfNotEmpty("packageName", packageName)
.appendIfNotEmpty("builderGenerationPackages", builderGenerationPackages.toString())
.appendIfNotEmpty("builderUsagePackages", builderUsagePackages.toString())
.appendIfNotEmpty("builderSuffix", builderSuffix)
Expand Down Expand Up @@ -546,6 +559,7 @@ public static class Builder {
private OptionState generateJacksonModule = OptionState.UNSET;
private OptionState generateJavaDoc = OptionState.UNSET;
private String jacksonModulePackage = null;
private String packageName = null;

// === Naming ===
private String builderSuffix = null;
Expand Down Expand Up @@ -648,6 +662,11 @@ public Builder jacksonModulePackage(String value) {
return this;
}

public Builder packageName(String value) {
this.packageName = StringUtils.trimToNull(value);
return this;
}

public Builder builderGenerationPackages(String value) {
this.builderGenerationPackages = PackageScopes.parse(value);
return this;
Expand Down Expand Up @@ -896,6 +915,7 @@ public BuilderConfiguration build() {
generateJacksonModule,
generateJavaDoc,
jacksonModulePackage,
packageName,
builderGenerationPackages,
builderUsagePackages,
builderSuffix,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
Expand Down Expand Up @@ -137,6 +138,7 @@ public BuilderConfiguration resolveConfiguration(Element element) throws Builder

// Validate access modifiers and warn about problematic configurations
validateAccessModifiers(element, result);
validatePackageName(element, result);

logger.debugEndOperation("Resulting configuration resolved: %s", result.toString());
return result;
Expand Down Expand Up @@ -170,6 +172,7 @@ public BuilderConfiguration resolveHolderConfiguration(Element element) throws B

// Validate access modifiers and warn about problematic configurations
validateAccessModifiers(element, result);
validatePackageName(element, result);

logger.debugEndOperation("Resulting configuration resolved: %s", result.toString());
return result;
Expand Down Expand Up @@ -427,4 +430,21 @@ private static void validateAccessModifiers(Element element, BuilderConfiguratio
elementName);
}
}

/**
* Validates the {@code packageName} option and throws an exception for values that are not a
* valid Java package name.
*
* @param element the element being processed
* @param config the resolved configuration
* @throws BuilderException if the configured package name is invalid
*/
private static void validatePackageName(Element element, BuilderConfiguration config)
throws BuilderException {
String packageName = config.getPackageName();
if (packageName != null && !SourceVersion.isName(packageName)) {
throw new BuilderException(
element, "Option packageName '%s' is not a valid Java package name", packageName);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,12 @@ public enum CompilerArgumentsEnum {
/** Option for Jackson Module package name. */
JACKSON_MODULE_PACKAGE("jacksonModulePackage", string(Builder::jacksonModulePackage)),

/**
* Option for the package generated builders are placed in. Annotation-only: a global {@code
* packageName} would collapse all builders into a single package.
*/
PACKAGE_NAME("packageName", string(Builder::packageName), true),

// === Builder Scoping ===
/** Option for builder generation packages. */
BUILDER_GENERATION_PACKAGES(
Expand Down Expand Up @@ -214,6 +220,12 @@ public enum CompilerArgumentsEnum {
*/
private final OptionApplier builderApplier;

/**
* Whether this option may only be set per annotation and is ignored in compiler arguments ({@code
* -Asimplebuilder.*} / {@code -D}).
*/
private final boolean annotationOnly;

/**
* Constructs a CompilerArgumentsEnum constant for an argument that is not a builder configuration
* option.
Expand Down Expand Up @@ -244,8 +256,20 @@ public enum CompilerArgumentsEnum {
* @param builderApplier applies the raw option value to the configuration builder
*/
CompilerArgumentsEnum(String optionName, OptionApplier builderApplier) {
this(optionName, builderApplier, false);
}

/**
* Constructs a CompilerArgumentsEnum constant for a builder configuration option.
*
* @param optionName The option name
* @param builderApplier applies the raw option value to the configuration builder
* @param annotationOnly {@code true} to restrict the option to annotation usage
*/
CompilerArgumentsEnum(String optionName, OptionApplier builderApplier, boolean annotationOnly) {
this.optionName = optionName;
this.builderApplier = builderApplier;
this.annotationOnly = annotationOnly;
}

/**
Expand Down Expand Up @@ -304,6 +328,16 @@ public boolean hasValueApplier() {
return builderApplier != null;
}

/**
* Returns whether this option is only valid as an annotation member and is ignored when set
* through compiler arguments.
*
* @return {@code true} if the option must not be read from compiler arguments
*/
public boolean isAnnotationOnly() {
return annotationOnly;
}

/**
* Applies a raw option value to the given configuration builder.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,8 @@ public boolean readBooleanValue(CompilerArgumentsEnum argument) {
* {@code BuilderConfigurationReader}. The remaining checklist when adding a new option:
*
* <ol>
* <li>{@code CompilerArgumentsEnum} — add the enum constant with its applier.
* <li>{@code CompilerArgumentsEnum} — add the enum constant with its applier (or pass {@code
* annotationOnly = true} if the option must not be set globally).
* <li>{@code BuilderConfiguration} — add the field, builder method, merge logic, and a typed
* accessor (e.g. {@code formattingModeEnum}) if enum conversion is needed. Set the default
* in {@code BuilderConfiguration.DEFAULT}.
Expand All @@ -133,6 +134,7 @@ public boolean readBooleanValue(CompilerArgumentsEnum argument) {
public BuilderConfiguration readBuilderConfiguration(ProcessingLogger logger) {
BuilderConfiguration.Builder builder = BuilderConfiguration.builder();
Stream.of(CompilerArgumentsEnum.values())
.filter(option -> !option.isAnnotationOnly())
.forEach(option -> option.apply(builder, readValue(option), logger));
return builder.build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,10 @@
* can reach it without threading both values through every call.
*
* @param configuration the resolved builder configuration for the current target
* @param builderPackage the package the generated builder is written to. For {@code @SimpleBuilder}
* targets this is the processed type's own package; for {@code @SimpleBuilderFor} targets it is
* the holder's package. The latter cannot be derived from the processed element itself, which
* is why it is carried explicitly here
* @param builderPackage the package the generated builder is written to. This is the configured
* {@code packageName} option when set; otherwise the processed type's own package for
* {@code @SimpleBuilder} targets, resp. the holder's package for {@code @SimpleBuilderFor}
* targets. It cannot always be derived from the processed element itself, which is why it is
* carried explicitly here
*/
public record ProcessingTarget(BuilderConfiguration configuration, String builderPackage) {}
Original file line number Diff line number Diff line change
Expand Up @@ -3696,4 +3696,28 @@ public class GenericSetterDto {
assertThat(compilation)
.hadWarningContaining("Field 'value' has field-specific generics, so it will be ignored");
}

@Test
void packageName_GeneratesBuilderIntoConfiguredPackage() {
JavaFileObject source =
ProcessorTestUtils.forSource(
"""
package test;
import org.javahelpers.simple.builders.core.annotations.SimpleBuilder;
@SimpleBuilder(options = @SimpleBuilder.Options(packageName = "com.example.generated"))
public class Person {
private String name;
public Person() {}
public String getName() { return name; }
public void setName(String name) { this.name = name; }
}
""");

Compilation compilation = ProcessorTestUtils.createCompiler().compile(source);

assertThat(compilation).succeededWithoutWarnings();
String generated = loadGeneratedSource(compilation, "PersonBuilder");
ProcessorAsserts.assertContaining(
generated, "package com.example.generated;", "import test.Person;");
}
}
Loading
Loading