Skip to content

Commit ee7d527

Browse files
authored
Support using builder/wither from other processors (#284)
This allows avoiding having multiple rounds of processing. Also added a hook if the user needs to add logic to build method.
1 parent b3920ef commit ee7d527

File tree

11 files changed

+675
-99
lines changed

11 files changed

+675
-99
lines changed

sourcegen-bytecode-writer/src/main/java/io/micronaut/sourcegen/bytecode/EnumGenUtils.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
import javax.lang.model.element.Modifier;
3333
import java.util.ArrayList;
3434
import java.util.List;
35-
import java.util.Map;
3635
import java.util.Optional;
3736

3837
/**

sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/BuilderAnnotationVisitor.java

Lines changed: 100 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848

4949
import javax.lang.model.element.Modifier;
5050
import java.util.ArrayList;
51+
import java.util.Arrays;
5152
import java.util.Collection;
5253
import java.util.Collections;
5354
import java.util.HashSet;
@@ -100,34 +101,23 @@ public void visitClass(ClassElement element, VisitorContext context) {
100101
return;
101102
}
102103
try {
103-
String simpleName = element.getSimpleName() + "Builder";
104-
String builderClassName = element.getPackageName() + "." + simpleName;
105-
106-
ClassTypeDef builderType = ClassTypeDef.of(builderClassName);
107-
108-
ClassDef.ClassDefBuilder builder = ClassDef.builder(builderClassName)
109-
.addModifiers(Modifier.PUBLIC, Modifier.FINAL);
110-
addAnnotations(builder, element.getAnnotation(Builder.class));
111-
112104
List<PropertyElement> properties = element.getBeanProperties();
113-
for (PropertyElement beanProperty : properties) {
114-
createModifyPropertyMethod(builder, beanProperty, ExpressionDef::returning);
115-
}
116-
117-
builder.addMethod(MethodDef.constructor().build());
118-
if (!properties.isEmpty()) {
119-
builder.addMethod(createAllPropertiesConstructor(properties));
120-
}
121-
122-
builder.addMethod(createBuilderMethod(builderType));
123-
builder.addMethod(createBuildMethod(element));
105+
@NonNull ParameterElement[] constructorElement = element.getPrimaryConstructor()
106+
.filter(c -> !c.isPrivate())
107+
.or(element::getDefaultConstructor)
108+
.map(MethodElement::getParameters).orElse(ParameterElement.ZERO_PARAMETER_ELEMENTS);
109+
List<ParameterElement> constructorParameters = Arrays.asList(constructorElement);
110+
AnnotationValue<Builder> builderAnnotationValue = element.getAnnotation(Builder.class);
111+
ClassTypeDef elementType = ClassTypeDef.of(element);
112+
113+
ClassDefBuilder builder = createBuilder(element.getPackageName(), elementType, builderAnnotationValue, properties, constructorParameters);
114+
ClassDef builderDef = builder.build();
124115

125116
SourceGenerator sourceGenerator = SourceGenerators.findByLanguage(context.getLanguage()).orElse(null);
126117
if (sourceGenerator == null) {
127118
return;
128119
}
129120

130-
ClassDef builderDef = builder.build();
131121
processed.add(element.getName());
132122
sourceGenerator.write(builderDef, context, element);
133123
} catch (ProcessingException e) {
@@ -145,6 +135,72 @@ public void visitClass(ClassElement element, VisitorContext context) {
145135
}
146136
}
147137

138+
/**
139+
* Create a builder for the given arguments.
140+
* @param packageName The package name
141+
* @param elementType The element type
142+
* @param builderAnnotationValue The builder annotation value.
143+
* @param properties The properties
144+
* @param constructorParameters The constructor parameters
145+
* @return A class definition builder for the builder
146+
*/
147+
static @NonNull ClassDefBuilder createBuilder(
148+
String packageName,
149+
@NonNull ClassTypeDef elementType,
150+
@Nullable AnnotationValue<Builder> builderAnnotationValue,
151+
@NonNull List<PropertyElement> properties,
152+
@NonNull List<ParameterElement> constructorParameters) {
153+
Function<BuilderGenerator.BuildContext, StatementDef> returnSelf = (context) -> context.aThis().returning();
154+
return createBuilder(packageName, elementType, builderAnnotationValue, properties, constructorParameters, returnSelf);
155+
}
156+
157+
/**
158+
* Create a builder for the given arguments.
159+
*
160+
* @param packageName The package name
161+
* @param elementType The element type
162+
* @param builderAnnotationValue The builder annotation value.
163+
* @param properties The properties
164+
* @param constructorParameters The constructor parameters
165+
* @param buildReturnStatement The return statement to use for building.
166+
* @return A class definition builder for the builder
167+
*/
168+
static ClassDefBuilder createBuilder(
169+
String packageName,
170+
ClassTypeDef elementType,
171+
AnnotationValue<Builder> builderAnnotationValue,
172+
List<PropertyElement> properties,
173+
List<ParameterElement> constructorParameters,
174+
Function<BuilderGenerator.BuildContext, StatementDef> buildReturnStatement) {
175+
String simpleName = elementType.getSimpleName() + "Builder";
176+
String builderClassName = packageName + "." + simpleName;
177+
ClassTypeDef builderType = ClassTypeDef.of(builderClassName);
178+
179+
ClassDefBuilder builder = ClassDef.builder(builderClassName)
180+
.addModifiers(Modifier.PUBLIC, Modifier.FINAL);
181+
if (builderAnnotationValue != null) {
182+
addAnnotations(builder, builderAnnotationValue);
183+
}
184+
185+
for (PropertyElement beanProperty : properties) {
186+
createModifyPropertyMethod(builder, beanProperty, buildReturnStatement);
187+
}
188+
189+
builder.addMethod(MethodDef.constructor().build());
190+
if (!properties.isEmpty()) {
191+
builder.addMethod(createAllPropertiesConstructor(properties));
192+
}
193+
194+
builder.addMethod(createBuilderMethod(builderType));
195+
196+
builder.addMethod(createBuildMethod(
197+
elementType,
198+
properties,
199+
constructorParameters)
200+
);
201+
return builder;
202+
}
203+
148204
static void addAnnotations(ClassDefBuilder builder, AnnotationValue<?> annotation) {
149205
Optional<AnnotationClassValue[]> annotatedWith = annotation.getConvertibleValues()
150206
.get(BUILDER_ANNOTATED_WITH_MEMBER, AnnotationClassValue[].class);
@@ -212,7 +268,7 @@ private static StatementDef mapToArrayListStatement(VariableDef.Field propertyFi
212268
);
213269
}
214270

215-
private MethodDef createBuilderMethod(ClassTypeDef builderType) {
271+
private static MethodDef createBuilderMethod(ClassTypeDef builderType) {
216272
return MethodDef.builder("builder")
217273
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
218274
.returns(builderType)
@@ -222,7 +278,7 @@ private MethodDef createBuilderMethod(ClassTypeDef builderType) {
222278

223279
static void createModifyPropertyMethod(ClassDef.ClassDefBuilder classDefBuilder,
224280
PropertyElement beanProperty,
225-
Function<VariableDef.This, StatementDef> returningExpressionProvider) {
281+
Function<BuilderGenerator.BuildContext, StatementDef> returningExpressionProvider) {
226282
if (beanProperty.hasAnnotation(Singular.class)) {
227283
createSingularPropertyMethods(classDefBuilder, beanProperty, returningExpressionProvider);
228284
} else {
@@ -232,7 +288,7 @@ static void createModifyPropertyMethod(ClassDef.ClassDefBuilder classDefBuilder,
232288

233289
private static void createDefaultModifyPropertyMethod(ClassDef.ClassDefBuilder classDefBuilder,
234290
PropertyElement beanProperty,
235-
Function<VariableDef.This, StatementDef> returningExpressionProvider) {
291+
Function<BuilderGenerator.BuildContext, StatementDef> returningExpressionProvider) {
236292
TypeDef propertyTypeDef = TypeDef.of(beanProperty.getType());
237293
FieldDef field = createField(beanProperty, propertyTypeDef);
238294
classDefBuilder.addField(field);
@@ -242,7 +298,7 @@ private static void createDefaultModifyPropertyMethod(ClassDef.ClassDefBuilder c
242298
.addParameter(propertyName, propertyTypeDef)
243299
.build((self, parameterDefs) -> StatementDef.multi(
244300
self.field(field).assign(parameterDefs.get(0)),
245-
returningExpressionProvider.apply(self)
301+
returningExpressionProvider.apply(new BuilderGenerator.BuildContext(self, field))
246302
)));
247303
}
248304

@@ -266,7 +322,7 @@ private static FieldDef createField(PropertyElement beanProperty, TypeDef type)
266322

267323
private static void createSingularPropertyMethods(ClassDef.ClassDefBuilder classBuilder,
268324
PropertyElement beanProperty,
269-
Function<VariableDef.This, StatementDef> returningExpressionProvider) {
325+
Function<BuilderGenerator.BuildContext, StatementDef> returningExpressionProvider) {
270326
String propertyName = beanProperty.getSimpleName();
271327
String singularName = beanProperty.stringValue(Singular.class).orElse(null);
272328
if (singularName == null) {
@@ -293,7 +349,7 @@ private static void createSingularPropertyMethods(ClassDef.ClassDefBuilder class
293349
self.field(field).assign(ClassTypeDef.of(ArrayList.class).instantiate())
294350
),
295351
self.field(field).invoke("addAll", TypeDef.primitive(boolean.class), parameterDefs.get(0)),
296-
returningExpressionProvider.apply(self)
352+
returningExpressionProvider.apply(new BuilderGenerator.BuildContext(self, field))
297353
)));
298354
classBuilder.addMethod(MethodDef.builder(singularName)
299355
.addModifiers(Modifier.PUBLIC)
@@ -303,15 +359,15 @@ private static void createSingularPropertyMethods(ClassDef.ClassDefBuilder class
303359
self.field(field).assign(ClassTypeDef.of(ArrayList.class).instantiate())
304360
),
305361
self.field(field).invoke("add", TypeDef.of(boolean.class), parameterDefs.get(0)),
306-
returningExpressionProvider.apply(self)
362+
returningExpressionProvider.apply(new BuilderGenerator.BuildContext(self, field))
307363
)));
308364
classBuilder.addMethod(MethodDef.builder("clear" + StringUtils.capitalize(propertyName))
309365
.addModifiers(Modifier.PUBLIC)
310366
.build((self, parameterDefs) -> StatementDef.multi(
311367
self.field(field).isNonNull().doIf(
312368
self.field(field).invoke("clear", TypeDef.VOID)
313369
),
314-
returningExpressionProvider.apply(self)
370+
returningExpressionProvider.apply(new BuilderGenerator.BuildContext(self, field))
315371
)));
316372
} else if (beanProperty.getType().isAssignable(Map.class)) {
317373
TypeDef keyType = beanProperty.getType().getFirstTypeArgument().<TypeDef>map(ClassTypeDef::of).orElse(TypeDef.OBJECT);
@@ -341,7 +397,7 @@ private static void createSingularPropertyMethods(ClassDef.ClassDefBuilder class
341397
TypeDef.primitive(boolean.class),
342398
parameterDefs.get(0).invoke("entrySet", ClassTypeDef.of(Set.class))
343399
),
344-
returningExpressionProvider.apply(self)
400+
returningExpressionProvider.apply(new BuilderGenerator.BuildContext(self, field))
345401
)));
346402
classBuilder.addMethod(MethodDef.builder(singularName)
347403
.addModifiers(Modifier.PUBLIC)
@@ -361,47 +417,39 @@ private static void createSingularPropertyMethods(ClassDef.ClassDefBuilder class
361417
parameterDefs.get(1)
362418
)
363419
),
364-
returningExpressionProvider.apply(self)
420+
returningExpressionProvider.apply(new BuilderGenerator.BuildContext(self, field))
365421
)));
366422
classBuilder.addMethod(MethodDef.builder("clear" + StringUtils.capitalize(propertyName))
367423
.addModifiers(Modifier.PUBLIC)
368424
.build((self, parameterDefs) -> StatementDef.multi(
369425
self.field(field).isNonNull().doIf(
370426
self.field(field).invoke("clear", TypeDef.VOID)
371427
),
372-
returningExpressionProvider.apply(self)
428+
returningExpressionProvider.apply(new BuilderGenerator.BuildContext(self, field))
373429
)));
374430
} else {
375431
throw new IllegalStateException("Unsupported singular collection type [" + beanProperty.getType().getName() + "] for property: " + beanProperty.getName());
376432
}
377433
}
378434

379-
static MethodDef createBuildMethod(ClassElement producedType) {
435+
static MethodDef createBuildMethod(ClassTypeDef buildType, List<PropertyElement> properties, List<ParameterElement> constructorParameters) {
380436
return MethodDef.builder("build")
381437
.addModifiers(Modifier.PUBLIC)
382438
.build((self, parameterDefs) -> {
383-
MethodElement constructorElement = producedType.getPrimaryConstructor()
384-
.filter(c -> !c.isPrivate())
385-
.or(producedType::getDefaultConstructor)
386-
.orElse(null);
387-
388-
List<PropertyElement> beanProperties = new ArrayList<>(producedType.getBeanProperties());
439+
List<PropertyElement> beanProperties = new ArrayList<>(properties);
389440
List<ExpressionDef> values = new ArrayList<>();
390-
if (constructorElement != null) {
391-
for (ParameterElement parameter : constructorElement.getParameters()) {
392-
PropertyElement propertyElement = beanProperties.stream().filter(p -> p.getName().equals(parameter.getName())).findFirst().orElse(null);
393-
if (propertyElement != null) {
394-
beanProperties.remove(propertyElement);
395-
}
396-
// We need to convert it for the correct type in Kotlin
397-
TypeDef fieldType = TypeDef.of(parameter.getType()).makeNullable();
398-
VariableDef.Field field = self.field(parameter.getName(), fieldType);
399-
values.add(
400-
valueExpression(propertyElement, field).cast(TypeDef.of(parameter.getType()))
401-
);
441+
for (ParameterElement parameter : constructorParameters) {
442+
PropertyElement propertyElement = beanProperties.stream().filter(p -> p.getName().equals(parameter.getName())).findFirst().orElse(null);
443+
if (propertyElement != null) {
444+
beanProperties.remove(propertyElement);
402445
}
446+
// We need to convert it for the correct type in Kotlin
447+
TypeDef fieldType = TypeDef.of(parameter.getType()).makeNullable();
448+
VariableDef.Field field = self.field(parameter.getName(), fieldType);
449+
values.add(
450+
valueExpression(propertyElement, field).cast(TypeDef.of(parameter.getType()))
451+
);
403452
}
404-
ClassTypeDef buildType = ClassTypeDef.of(producedType);
405453
if (beanProperties.isEmpty()) {
406454
return buildType.instantiate(values).returning();
407455
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* Copyright 2017-2025 original authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.micronaut.sourcegen.generator.visitors;
17+
18+
import io.micronaut.core.annotation.AnnotationValue;
19+
import io.micronaut.core.annotation.Experimental;
20+
import io.micronaut.core.annotation.NonNull;
21+
import io.micronaut.core.annotation.Nullable;
22+
import io.micronaut.inject.ast.ParameterElement;
23+
import io.micronaut.inject.ast.PropertyElement;
24+
import io.micronaut.sourcegen.annotations.Builder;
25+
import io.micronaut.sourcegen.model.ClassDef;
26+
import io.micronaut.sourcegen.model.ClassTypeDef;
27+
import io.micronaut.sourcegen.model.FieldDef;
28+
import io.micronaut.sourcegen.model.StatementDef;
29+
import io.micronaut.sourcegen.model.VariableDef;
30+
31+
import java.util.List;
32+
import java.util.function.Function;
33+
34+
/**
35+
* Exposes methods that can be used to create builders from other processors.
36+
*/
37+
@Experimental
38+
public class BuilderGenerator {
39+
/**
40+
* Create a builder for the given arguments.
41+
* @param packageName The package name
42+
* @param elementType The element type
43+
* @param builderAnnotationValue The builder annotation value.
44+
* @param properties The properties
45+
* @param constructorParameters The constructor parameters
46+
* @return A class definition builder for the builder
47+
*/
48+
public static @NonNull ClassDef.ClassDefBuilder createBuilder(
49+
String packageName,
50+
@NonNull ClassTypeDef elementType,
51+
@Nullable AnnotationValue<Builder> builderAnnotationValue,
52+
@NonNull List<PropertyElement> properties,
53+
@NonNull List<ParameterElement> constructorParameters) {
54+
Function<BuildContext, StatementDef> returnSelf = (context) -> context.aThis().returning();
55+
return createBuilder(packageName, elementType, builderAnnotationValue, properties, constructorParameters, returnSelf);
56+
}
57+
58+
/**
59+
* Create a builder for the given arguments.
60+
*
61+
* @param packageName The package name
62+
* @param elementType The element type
63+
* @param builderAnnotationValue The builder annotation value.
64+
* @param properties The properties
65+
* @param constructorParameters The constructor parameters
66+
* @param buildReturnStatement The return statement to use for building.
67+
* @return A class definition builder for the builder
68+
*/
69+
public static ClassDef.ClassDefBuilder createBuilder(
70+
String packageName,
71+
ClassTypeDef elementType,
72+
AnnotationValue<Builder> builderAnnotationValue,
73+
List<PropertyElement> properties,
74+
List<ParameterElement> constructorParameters,
75+
Function<BuildContext, StatementDef> buildReturnStatement) {
76+
return BuilderAnnotationVisitor.createBuilder(
77+
packageName,
78+
elementType,
79+
builderAnnotationValue,
80+
properties,
81+
constructorParameters,
82+
buildReturnStatement
83+
);
84+
}
85+
86+
/**
87+
* Invocation context for when a builder method is called.
88+
*
89+
* @param aThis A this
90+
* @param field The field being assigned
91+
*/
92+
public record BuildContext(VariableDef.This aThis, FieldDef field) {
93+
}
94+
}

0 commit comments

Comments
 (0)