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 @@ -25,6 +25,7 @@
package org.javahelpers.simple.builders.core.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
Expand Down Expand Up @@ -83,6 +84,21 @@
* }
* }</pre>
*
* <p>The annotation is {@link Repeatable}: several declarations may be placed on the same holder
* (class or {@code package-info.java}) when the listed types need different {@link #options()}, for
* example a different {@code packageName} for the generated builders:
*
* <pre>{@code
* @SimpleBuilderFor(
* value = ExternalUser.class,
* options = @SimpleBuilder.Options(packageName = "com.example.users"))
* @SimpleBuilderFor(
* value = ExternalOrder.class,
* options = @SimpleBuilder.Options(packageName = "com.example.orders"))
* public class ExternalBuildersProvider {
* }
* }</pre>
*
* <p>Configuration uses the existing {@link SimpleBuilder.Options} model and may be overridden via
* compiler options:
*
Expand All @@ -103,6 +119,7 @@
*/
@Target({ElementType.TYPE, ElementType.PACKAGE})
@Retention(RetentionPolicy.CLASS)
@Repeatable(SimpleBuilderFors.class)
public @interface SimpleBuilderFor {

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* MIT License
*
* Copyright (c) 2026 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;

/**
* Container annotation holding several {@link SimpleBuilderFor} declarations on the same element.
* It is applied implicitly when {@link SimpleBuilderFor} is repeated and is not meant to be used
* directly.
*
* @see SimpleBuilderFor
*/
@Target({ElementType.TYPE, ElementType.PACKAGE})
@Retention(RetentionPolicy.CLASS)
public @interface SimpleBuilderFors {

/**
* The repeated {@link SimpleBuilderFor} declarations.
*
* @return the contained {@link SimpleBuilderFor} annotations
*/
SimpleBuilderFor[] value();
}
13 changes: 13 additions & 0 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,19 @@ The generated builders are placed in the annotated package - or in the package o

The target type must be constructible through accessible Java APIs from the builder's package: it needs a visible type and an accessible constructor, otherwise generation fails with a compile-time diagnostic. The target type's own annotations are not consulted - the explicit declaration wins, so `@Ignore4BuilderGeneration` on the target does not suppress generation either. `@SimpleBuilderFor` is not `@Inherited`, and the provider class or package itself never gets a builder.

`@SimpleBuilderFor` is `@Repeatable`: several declarations may be placed on the same holder or `package-info.java` when the listed types need different options - for example a different `packageName` per declaration:

```java
@SimpleBuilderFor(
value = ExternalUser.class,
options = @SimpleBuilder.Options(packageName = "com.example.users"))
@SimpleBuilderFor(
value = ExternalOrder.class,
options = @SimpleBuilder.Options(packageName = "com.example.orders"))
public class ExternalBuildersProvider {
}
```

## Compiler Options

Set project-wide defaults via compiler options. These apply to all builders unless overridden by annotations.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
import org.javahelpers.simple.builders.core.annotations.Ignore4BuilderGeneration;
import org.javahelpers.simple.builders.core.annotations.SimpleBuilder.Template;
import org.javahelpers.simple.builders.core.annotations.SimpleBuilderFor;
import org.javahelpers.simple.builders.core.annotations.SimpleBuilderFors;
import org.javahelpers.simple.builders.processor.analysis.BuilderScopeResolver;
import org.javahelpers.simple.builders.processor.analysis.JavaLangAnalyser;
import org.javahelpers.simple.builders.processor.classgen.roaster.RoasterCodeGenerator;
Expand Down Expand Up @@ -207,10 +208,13 @@ private void generateJacksonModules(PerformanceTracker tracker) {

/**
* Collects all elements annotated with {@code @SimpleBuilderFor} in this round. These are holder
* classes declaring external types a builder is generated for.
* classes declaring external types a builder is generated for. Repeated declarations arrive
* wrapped in the {@code @SimpleBuilderFors} container, so both are collected.
*/
private Set<Element> collectExternalTypeHolders(RoundEnvironment roundEnv) {
return new HashSet<>(roundEnv.getElementsAnnotatedWith(SimpleBuilderFor.class));
Set<Element> holders = new HashSet<>(roundEnv.getElementsAnnotatedWith(SimpleBuilderFor.class));
holders.addAll(roundEnv.getElementsAnnotatedWith(SimpleBuilderFors.class));
return holders;
}

/**
Expand Down Expand Up @@ -311,9 +315,11 @@ private Optional<ElementToGenerate> planGenerationOfAnnotatedElement(
}

/**
* 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
* configured {@code packageName} or the holder's package.
* Expands a {@code @SimpleBuilderFor} holder into the external types listed in the {@code value}
* attributes of its annotation instances and plans a builder for each of them. Each instance
* resolves its own configuration, so repeated declarations may place their generated builders
* into different packages via {@code packageName}; without a configured package the holder's
* package is used.
*/
private List<ElementToGenerate> planGenerationOfTypeByHolder(
Element holder,
Expand All @@ -322,24 +328,30 @@ private List<ElementToGenerate> planGenerationOfTypeByHolder(
PerformanceTracker tracker)
throws BuilderException {
Objects.requireNonNull(holder, "holder must not be null");
List<TypeElement> targets = extractExternalTargetTypes(holder);
if (targets.isEmpty()) {
context.warning(
holder,
"simple-builders: @SimpleBuilderFor on '%s' does not list any types - nothing to generate",
holder.getSimpleName());
return List.of();
}

tracker.startPhase();
BuilderConfiguration config = reader.resolveHolderConfiguration(holder);
tracker.endPhase(PHASE_CONFIGURATION_RESOLUTION);
List<AnnotationMirror> annotationInstances =
JavaLangAnalyser.findAnnotationInstances(
holder, SimpleBuilderFor.class, SimpleBuilderFors.class, context);

String builderPackage = effectiveBuilderPackage(holder, config);
List<ElementToGenerate> results = new ArrayList<>();
for (TypeElement target : targets) {
planExternalTarget(target, holder, config, builderPackage, alreadyPlannedBuilders)
.ifPresent(results::add);
for (AnnotationMirror annotationInstance : annotationInstances) {
List<TypeElement> targets = extractExternalTargetTypes(holder, annotationInstance);
if (targets.isEmpty()) {
context.warning(
holder,
"simple-builders: @SimpleBuilderFor on '%s' does not list any types - nothing to generate",
holder.getSimpleName());
continue;
}

tracker.startPhase();
BuilderConfiguration config = reader.resolveHolderConfiguration(holder, annotationInstance);
tracker.endPhase(PHASE_CONFIGURATION_RESOLUTION);

String builderPackage = effectiveBuilderPackage(holder, config);
for (TypeElement target : targets) {
planExternalTarget(target, holder, config, builderPackage, alreadyPlannedBuilders)
.ifPresent(results::add);
}
}
return results;
}
Expand Down Expand Up @@ -380,17 +392,13 @@ private Optional<ElementToGenerate> planExternalTarget(
}

/**
* Reads the {@code value} attribute of the {@code @SimpleBuilderFor} annotation on the holder and
* resolves each entry to the {@link TypeElement} the builder is generated for.
* Reads the {@code value} attribute of one {@code @SimpleBuilderFor} annotation instance on the
* holder and resolves each entry to the {@link TypeElement} the builder is generated for.
*/
private List<TypeElement> extractExternalTargetTypes(Element holder) throws BuilderException {
Optional<AnnotationMirror> mirror =
JavaLangAnalyser.findAnnotation(holder, SimpleBuilderFor.class);
if (mirror.isEmpty()) {
return List.of();
}
private List<TypeElement> extractExternalTargetTypes(
Element holder, AnnotationMirror annotationInstance) throws BuilderException {
Optional<AnnotationValue> valueAttribute =
JavaLangAnalyser.findAnnotationAttribute(mirror.get(), "value", context);
JavaLangAnalyser.findAnnotationAttribute(annotationInstance, "value", context);
List<TypeElement> results = new ArrayList<>();
// For an array-valued attribute javac always delivers a list, even for a single entry.
if (valueAttribute.isEmpty() || !(valueAttribute.get().getValue() instanceof List<?> items)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import static javax.lang.model.type.TypeKind.VOID;

import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
Expand Down Expand Up @@ -232,6 +233,47 @@ public static Optional<AnnotationMirror> findAnnotation(
return Optional.empty();
}

/**
* Finds all instances of a repeatable annotation on an element: direct mirrors plus the entries
* wrapped in its container annotation, in declaration order.
*
* @param element Element to be checked
* @param annotationClass the repeatable annotation to be checked
* @param containerClass the container annotation wrapping repeats of {@code annotationClass}
* @param context the processing context providing element utilities
* @return the list of {@code javax.lang.model.element.AnnotationMirror} instances of
* annotationClass found on the element
*/
public static List<AnnotationMirror> findAnnotationInstances(
Element element,
Class<? extends Annotation> annotationClass,
Class<? extends Annotation> containerClass,
ProcessingContext context) {
List<AnnotationMirror> instances = new ArrayList<>();
if (element == null) {
return instances;
}
for (AnnotationMirror annotationMirror : element.getAnnotationMirrors()) {
String annotationName = annotationMirror.getAnnotationType().toString();
if (annotationName.equals(annotationClass.getCanonicalName())) {
instances.add(annotationMirror);
} else if (annotationName.equals(containerClass.getCanonicalName())) {
Optional<AnnotationValue> containerValue =
findAnnotationAttribute(annotationMirror, "value", context);
if (containerValue.isPresent()
&& containerValue.get().getValue() instanceof List<?> entries) {
for (Object entry : entries) {
if (entry instanceof AnnotationValue entryValue
&& entryValue.getValue() instanceof AnnotationMirror entryMirror) {
instances.add(entryMirror);
}
}
}
}
}
return instances;
}

/**
* Reads the value of one attribute of an annotation mirror, including default values.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,7 @@
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;
import org.javahelpers.simple.builders.core.annotations.SimpleBuilderFor;
import org.javahelpers.simple.builders.core.enums.AccessModifier;
import org.javahelpers.simple.builders.processor.analysis.JavaLangAnalyser;
import org.javahelpers.simple.builders.processor.exceptions.BuilderException;
import org.javahelpers.simple.builders.processor.model.core.BuilderConfiguration;
import org.javahelpers.simple.builders.processor.processing.logging.ProcessingLogger;
Expand Down Expand Up @@ -151,21 +149,22 @@ public BuilderConfiguration resolveConfiguration(Element element) throws Builder
* are considered - the type is typically external and must not be modified, so its annotations
* (if any) do not participate in configuration. The configuration is composed of the built-in
* defaults, the global compiler arguments, and the {@code options()} of the
* {@code @SimpleBuilderFor} annotation found on the given element.
* {@code @SimpleBuilderFor} annotation instance found on the given element.
*
* @param element the {@code @SimpleBuilderFor} holder (used for validation messages)
* @param annotationInstance one {@code @SimpleBuilderFor} annotation instance on the element;
* repeated declarations each resolve their own configuration
* @return the fully resolved configuration with all sources merged
*/
public BuilderConfiguration resolveHolderConfiguration(Element element) throws BuilderException {
public BuilderConfiguration resolveHolderConfiguration(
Element element, AnnotationMirror annotationInstance) throws BuilderException {
Objects.requireNonNull(element, "element must not be null");
Objects.requireNonNull(annotationInstance, "annotationInstance must not be null");
String elementName = element.getSimpleName().toString();
logger.debugStartOperation(
"Resolving configuration for @SimpleBuilderFor target: %s", elementName);

BuilderConfiguration optionsConfig =
JavaLangAnalyser.findAnnotation(element, SimpleBuilderFor.class)
.map(this::extractOptionsFromAnnotationMirror)
.orElse(null);
BuilderConfiguration optionsConfig = extractOptionsFromAnnotationMirror(annotationInstance);

BuilderConfiguration result =
BuilderConfiguration.DEFAULT.merge(globalConfiguration).merge(optionsConfig);
Expand Down
Loading
Loading