Skip to content

Commit 8d58dae

Browse files
authored
builder interceptor support (#5591)
* Support for interceptors * fix build * checkstyles * javadoc * handle case where common Builder methods interfere with target bean methods * more checkstyles * Fix for issue #5608 * Fix for issue #5609 * address review comments * resolved the rest of the review comments
1 parent aa55867 commit 8d58dae

File tree

30 files changed

+1118
-193
lines changed

30 files changed

+1118
-193
lines changed

builder/README.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ The <b>Helidon Builder</b> provides compile-time code generation for fluent buil
1313
Supported annotation types (see [builder](./builder/src/main/java/io/helidon/builder) for further details):
1414
* Builder - similar to Lombok's SuperBuilder.
1515
* Singular - similar to Lombok's Singular.
16+
* NonNull - accomplished alternatively via Helidon's <i>ConfiguredOption#required</i>.
17+
* Default - accomplished alternatively via Helidon's <i>ConfiguredOption#value</i>.
18+
19+
Explicitly unsupported (i.e., these are just a few of the types that do not have a counterpart from Helidon's Builder):
20+
* NoArgsConstructor - must instead use one of the <i>toBuilder()</i> methods
21+
* AllArgsConstructor - must instead use one of the <i>toBuilder()</i> methods
1622

1723
Any and all types are supported by the Builder, with special handling for List, Map, Set, and Optional types. The target interface,
1824
however, should only contain getter like methods (i.e., has a non-void return and takes no arguments). All static and default methods
@@ -40,21 +46,19 @@ The result of this will create (under ./target/generated-sources/annotations):
4046
* Support for toBuilder().
4147
* Support for streams (see javadoc for [Builder](./builder/src/main/java/io/helidon/builder/Builder.java)).
4248
* Support for attribute visitors (see [test-builder](./tests/builder/src/main/java/io/helidon/builder/test/testsubjects/package-info.java)).
43-
* Support for attribute validation (see ConfiguredOption#required() and [builder](./tests/builder/src/main/java/io/helidon/builder/test/testsubjects/package-info.java)).
44-
45-
The implementation of the processor also allows for a provider-based extensibility mechanism.
49+
* Support for attribute validation (see ConfiguredOption#required() and [test-builder](./tests/builder/src/main/java/io/helidon/builder/test/testsubjects/package-info.java)).
50+
* Support for builder interception (i.e., including decoration or mutation). (see [test-builder](./tests/builder/src/main/java/io/helidon/builder/test/testsubjects/package-info.java)).
4651

4752
## Modules
4853
* [builder](./builder) - provides the compile-time annotations, as well as optional runtime supporting types.
4954
* [processor-spi](./processor-spi) - defines the Builder Processor SPI runtime definitions used by builder tooling. This module is only needed at compile time.
5055
* [processor-tools](./processor-tools) - provides the concrete creators & code generators. This module is only needed at compile time.
5156
* [processor](./processor) - the annotation processor which delegates to the processor-tools module for the main processing logic. This module is only needed at compile time.
52-
* [tests/builder](./tests/builder) - internal tests that can also serve as examples on usage.
57+
* [tests/builder](./tests/builder) - tests that can also serve as examples for usage.
5358

5459
## Customizations
5560
To implement your own custom <i>Builder</i>:
56-
* Write an implementation of <i>BuilderCreator</i> having a higher-than-default <i>Weighted</i> value as compared to <i>DefaultBuilderCreator</i>.
57-
* Include your module with this creator in your annotation processing path.
61+
* See [pico/builder-config](../pico/builder-config) for an example.
5862

5963
## Usage
6064
See [tests/builder](./tests/builder) for usage examples.

builder/builder/src/main/java/io/helidon/builder/AttributeVisitor.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@
2222
/**
2323
* A functional interface that can be used to visit all attributes of this type.
2424
* <p>
25-
* This type is used when {@link Builder#requireLibraryDependencies()} is used.
25+
* This type is used when {@link Builder#requireLibraryDependencies()} is used. When it is turned off, however, an equivalent
26+
* type will be code-generated into each generated bean.
2627
*
2728
* @param <T> type of the user defined context this attribute visitor supports
2829
*/
2930
@FunctionalInterface
31+
// important note: this class is also code generated - please keep this in synch with generated code
3032
public interface AttributeVisitor<T> {
3133

3234
/**

builder/builder/src/main/java/io/helidon/builder/Builder.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,52 @@
180180
*/
181181
boolean allowNulls() default DEFAULT_ALLOW_NULLS;
182182

183+
/**
184+
* The interceptor implementation type. See {@link BuilderInterceptor} for further details. Any interceptor applied will be called
185+
* prior to validation. The interceptor implementation can be any lambda-like implementation for the {@link BuilderInterceptor}
186+
* functional interface. This means that the implementation should declare a public method that matches the following:
187+
* <pre>{@code
188+
* Builder intercept(Builder builder);
189+
* }
190+
* </pre>
191+
* Note that the method name must be named <i>intercept</i>.
192+
*
193+
* @return the interceptor implementation class
194+
*/
195+
Class<?> interceptor() default Void.class;
196+
197+
/**
198+
* The (static) interceptor method to call on the {@link #interceptor()} implementation type in order to create the interceptor.
199+
* If left undefined then the {@code new} operator will be called on the type. If provided then the method must be public
200+
* and take no arguments. Example (see the create() method):
201+
* <pre>{@code
202+
* public class CustomBuilderInterceptor { // implements BuilderInterceptor
203+
* public CustomBuilderInterceptor() {
204+
* }
205+
*
206+
* public static CustomBuilderInterceptor create() {
207+
* ...
208+
* }
209+
*
210+
* public Builder intercept(Builder builder) {
211+
* ...
212+
* }
213+
* }
214+
* }
215+
* </pre>
216+
* <p>
217+
* This attribute is ignored if the {@link #interceptor()} class type is left undefined.
218+
* Note that the method must return an instance of the Builder, and there must be a public method that matches the following:
219+
* <pre>{@code
220+
* public Builder intercept(Builder builder);
221+
* }
222+
* </pre>
223+
* Note that the method name must be named <i>intercept</i>.
224+
*
225+
* @return the interceptor create method
226+
*/
227+
String interceptorCreateMethod() default "";
228+
183229
/**
184230
* The list implementation type to apply, defaulting to {@link #DEFAULT_LIST_TYPE}.
185231
*
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright (c) 2022 Oracle and/or its affiliates.
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+
* http://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+
17+
package io.helidon.builder;
18+
19+
/**
20+
* Provides a contract by which the {@link Builder}-annotated builder type can be intercepted (i.e., including decoration or
21+
* mutation).
22+
* <p>
23+
* This type is used when {@link Builder#requireLibraryDependencies} is used. When it is turned off, however, an equivalent
24+
* type will be code-generated into each generated bean.
25+
* Note also that in this situation your interceptor implementation does not need to implement this interface contract,
26+
* but instead must adhere to the following:
27+
* <ul>
28+
* <li>The implementation class type must provide a no-arg accessible constructor available to the generated class, unless
29+
* the {@link io.helidon.builder.Builder#interceptorCreateMethod()} is used.
30+
* <li>The implementation class type must provide a method-compatible (lambda) signature to the {@link #intercept} method.
31+
* <li>Any exceptions that might be thrown from the {@link #intercept} method must be an unchecked exception type.
32+
* </ul>
33+
*
34+
* @param <T> the type of the bean builder to intercept
35+
*
36+
* @see io.helidon.builder.Builder#interceptor()
37+
*/
38+
@FunctionalInterface
39+
public interface BuilderInterceptor<T> {
40+
41+
/**
42+
* Provides the ability to intercept (i.e., including decoration or mutation) the target.
43+
*
44+
* @param target the target being intercepted
45+
* @return the mutated or replaced target (must not be null)
46+
*/
47+
T intercept(T target);
48+
49+
}

builder/builder/src/main/java/io/helidon/builder/RequiredAttributeVisitor.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public class RequiredAttributeVisitor implements AttributeVisitor<Object> {
4343
/**
4444
* Default constructor.
4545
*/
46-
// important note: this needs to remain public since it will be new'ed from code-generated builder processing ...
46+
// important note: this class is also code generated - please keep this in synch with generated code
4747
public RequiredAttributeVisitor() {
4848
this(Builder.DEFAULT_ALLOW_NULLS);
4949
}
@@ -53,13 +53,13 @@ public RequiredAttributeVisitor() {
5353
*
5454
* @param allowNullsByDefault true if nulls should be allowed
5555
*/
56-
// important note: this needs to remain public since it will be new'ed from code-generated builder processing ...
56+
// important note: this class is also code generated - please keep this in synch with generated code
5757
public RequiredAttributeVisitor(boolean allowNullsByDefault) {
5858
this.allowNullsByDefault = allowNullsByDefault;
5959
}
6060

6161
@Override
62-
// important note: this needs to remain public since it will be new'ed from code-generated builder processing ...
62+
// important note: this class is also code generated - please keep this in synch with generated code
6363
public void visit(String attrName,
6464
Supplier<Object> valueSupplier,
6565
Map<String, Object> meta,
@@ -90,7 +90,7 @@ public void visit(String attrName,
9090
*
9191
* @throws java.lang.IllegalStateException when any attributes are in violation with the validation policy
9292
*/
93-
// important note: this needs to remain public since it will be new'ed from code-generated builder processing ...
93+
// important note: this class is also code generated - please keep this in synch with generated code
9494
public void validate() {
9595
if (!errors.isEmpty()) {
9696
throw new IllegalStateException(String.join(", ", errors));

builder/processor-spi/src/main/java/io/helidon/builder/processor/spi/DefaultTypeInfo.java

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@
1818

1919
import java.util.ArrayList;
2020
import java.util.Collection;
21-
import java.util.Collections;
22-
import java.util.LinkedList;
2321
import java.util.List;
2422
import java.util.Objects;
2523
import java.util.Optional;
@@ -36,6 +34,7 @@ public class DefaultTypeInfo implements TypeInfo {
3634
private final String typeKind;
3735
private final List<AnnotationAndValue> annotations;
3836
private final List<TypedElementName> elementInfo;
37+
private final List<TypedElementName> otherElementInfo;
3938
private final TypeInfo superTypeInfo;
4039

4140
/**
@@ -47,8 +46,9 @@ public class DefaultTypeInfo implements TypeInfo {
4746
protected DefaultTypeInfo(Builder b) {
4847
this.typeName = b.typeName;
4948
this.typeKind = b.typeKind;
50-
this.annotations = Collections.unmodifiableList(new LinkedList<>(b.annotations));
51-
this.elementInfo = Collections.unmodifiableList(new LinkedList<>(b.elementInfo));
49+
this.annotations = List.copyOf(b.annotations);
50+
this.elementInfo = List.copyOf(b.elementInfo);
51+
this.otherElementInfo = List.copyOf(b.otherElementInfo);
5252
this.superTypeInfo = b.superTypeInfo;
5353
}
5454

@@ -81,6 +81,11 @@ public List<TypedElementName> elementInfo() {
8181
return elementInfo;
8282
}
8383

84+
@Override
85+
public List<TypedElementName> otherElementInfo() {
86+
return otherElementInfo;
87+
}
88+
8489
@Override
8590
public Optional<TypeInfo> superTypeInfo() {
8691
return Optional.ofNullable(superTypeInfo);
@@ -99,6 +104,7 @@ public String toString() {
99104
protected String toStringInner() {
100105
return "typeName=" + typeName()
101106
+ ", elementInfo=" + elementInfo()
107+
+ ", annotations=" + annotations()
102108
+ ", superTypeInfo=" + superTypeInfo();
103109
}
104110

@@ -108,7 +114,7 @@ protected String toStringInner() {
108114
public static class Builder implements io.helidon.common.Builder<Builder, DefaultTypeInfo> {
109115
private final List<AnnotationAndValue> annotations = new ArrayList<>();
110116
private final List<TypedElementName> elementInfo = new ArrayList<>();
111-
117+
private final List<TypedElementName> otherElementInfo = new ArrayList<>();
112118
private TypeName typeName;
113119
private String typeKind;
114120

@@ -198,7 +204,32 @@ public Builder elementInfo(Collection<TypedElementName> val) {
198204
*/
199205
public Builder addElementInfo(TypedElementName val) {
200206
Objects.requireNonNull(val);
201-
elementInfo.add(Objects.requireNonNull(val));
207+
elementInfo.add(val);
208+
return this;
209+
}
210+
211+
/**
212+
* Sets the otherElementInfo to val.
213+
*
214+
* @param val the value
215+
* @return this fluent builder
216+
*/
217+
public Builder otherElementInfo(Collection<TypedElementName> val) {
218+
Objects.requireNonNull(val);
219+
this.otherElementInfo.clear();
220+
this.otherElementInfo.addAll(val);
221+
return this;
222+
}
223+
224+
/**
225+
* Adds a single otherElementInfo val.
226+
*
227+
* @param val the value
228+
* @return this fluent builder
229+
*/
230+
public Builder addOtherElementInfo(TypedElementName val) {
231+
Objects.requireNonNull(val);
232+
otherElementInfo.add(Objects.requireNonNull(val));
202233
return this;
203234
}
204235

builder/processor-spi/src/main/java/io/helidon/builder/processor/spi/TypeInfo.java

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
import io.helidon.pico.types.TypedElementName;
2525

2626
/**
27-
* Represents the model object for an interface type (e.g., one that was annotated with {@link io.helidon.builder.Builder}).
27+
* Represents the model object for an interface or an abstract type (i.e., one that was annotated with
28+
* {@link io.helidon.builder.Builder}).
2829
*/
2930
public interface TypeInfo {
3031

@@ -50,12 +51,19 @@ public interface TypeInfo {
5051
List<AnnotationAndValue> annotations();
5152

5253
/**
53-
* The elements that make up the type.
54+
* The elements that make up the type that are relevant for processing.
5455
*
55-
* @return the elements that make up the type
56+
* @return the elements that make up the type that are relevant for processing
5657
*/
5758
List<TypedElementName> elementInfo();
5859

60+
/**
61+
* The elements that make up this type that are considered "other", or being skipped because they are irrelevant to processing.
62+
*
63+
* @return the elements that still make up the type, but are otherwise deemed irrelevant for processing
64+
*/
65+
List<TypedElementName> otherElementInfo();
66+
5967
/**
6068
* The parent/super class for this type info.
6169
*

builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/BeanUtils.java

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,21 +100,36 @@ public static boolean validateAndParseMethodName(String methodName,
100100
return invalidMethod(methodName, throwIfInvalid, "invalid method name (must start with get or is)");
101101
}
102102

103-
return validMethod(methodName.substring(3), attributeNameRef, throwIfInvalid);
103+
return validMethod(methodName, attributeNameRef, throwIfInvalid);
104+
}
105+
106+
/**
107+
* Returns true if the word provided is considered to be a reserved word and should otherwise be avoided from generation.
108+
*
109+
* @param word the word
110+
* @return true if it appears to be a reserved word
111+
*/
112+
public static boolean isReservedWord(String word) {
113+
word = word.toLowerCase();
114+
return word.equals("class") || word.equals("interface") || word.equals("package") || word.equals("static")
115+
|| word.equals("final") || word.equals("public") || word.equals("protected") || word.equals("private")
116+
|| word.equals("abstract");
104117
}
105118

106119
private static boolean validMethod(String name,
107120
AtomicReference<Optional<List<String>>> attributeNameRef,
108121
boolean throwIfInvalid) {
109122
assert (name.trim().equals(name));
110-
char c = name.charAt(0);
123+
String attrName = name.substring(3);
124+
char c = attrName.charAt(0);
111125

112-
if (!validMethodCase(name, c, throwIfInvalid)) {
126+
if (!validMethodCase(attrName, c, throwIfInvalid)) {
113127
return false;
114128
}
115129

116130
c = Character.toLowerCase(c);
117-
attributeNameRef.set(Optional.of(Collections.singletonList("" + c + name.substring(1))));
131+
String altName = "" + c + attrName.substring(1);
132+
attributeNameRef.set(Optional.of(Collections.singletonList(isReservedWord(altName) ? name : altName)));
118133

119134
return true;
120135
}
@@ -130,7 +145,8 @@ private static boolean validBooleanIsMethod(String name,
130145
}
131146

132147
c = Character.toLowerCase(c);
133-
attributeNameRef.set(Optional.of(List.of("" + c + name.substring(3), name)));
148+
String altName = "" + c + name.substring(3);
149+
attributeNameRef.set(Optional.of(isReservedWord(altName) ? List.of(name) : List.of(altName, name)));
134150

135151
return true;
136152
}

0 commit comments

Comments
 (0)