Skip to content

Commit

Permalink
builder interceptor support (#5591)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
trentjeff committed Dec 16, 2022
1 parent aa55867 commit 8d58dae
Show file tree
Hide file tree
Showing 30 changed files with 1,118 additions and 193 deletions.
16 changes: 10 additions & 6 deletions builder/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ The <b>Helidon Builder</b> provides compile-time code generation for fluent buil
Supported annotation types (see [builder](./builder/src/main/java/io/helidon/builder) for further details):
* Builder - similar to Lombok's SuperBuilder.
* Singular - similar to Lombok's Singular.
* NonNull - accomplished alternatively via Helidon's <i>ConfiguredOption#required</i>.
* Default - accomplished alternatively via Helidon's <i>ConfiguredOption#value</i>.

Explicitly unsupported (i.e., these are just a few of the types that do not have a counterpart from Helidon's Builder):
* NoArgsConstructor - must instead use one of the <i>toBuilder()</i> methods
* AllArgsConstructor - must instead use one of the <i>toBuilder()</i> methods

Any and all types are supported by the Builder, with special handling for List, Map, Set, and Optional types. The target interface,
however, should only contain getter like methods (i.e., has a non-void return and takes no arguments). All static and default methods
Expand Down Expand Up @@ -40,21 +46,19 @@ The result of this will create (under ./target/generated-sources/annotations):
* Support for toBuilder().
* Support for streams (see javadoc for [Builder](./builder/src/main/java/io/helidon/builder/Builder.java)).
* Support for attribute visitors (see [test-builder](./tests/builder/src/main/java/io/helidon/builder/test/testsubjects/package-info.java)).
* Support for attribute validation (see ConfiguredOption#required() and [builder](./tests/builder/src/main/java/io/helidon/builder/test/testsubjects/package-info.java)).

The implementation of the processor also allows for a provider-based extensibility mechanism.
* Support for attribute validation (see ConfiguredOption#required() and [test-builder](./tests/builder/src/main/java/io/helidon/builder/test/testsubjects/package-info.java)).
* 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)).

## Modules
* [builder](./builder) - provides the compile-time annotations, as well as optional runtime supporting types.
* [processor-spi](./processor-spi) - defines the Builder Processor SPI runtime definitions used by builder tooling. This module is only needed at compile time.
* [processor-tools](./processor-tools) - provides the concrete creators & code generators. This module is only needed at compile time.
* [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.
* [tests/builder](./tests/builder) - internal tests that can also serve as examples on usage.
* [tests/builder](./tests/builder) - tests that can also serve as examples for usage.

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

## Usage
See [tests/builder](./tests/builder) for usage examples.
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@
/**
* A functional interface that can be used to visit all attributes of this type.
* <p>
* This type is used when {@link Builder#requireLibraryDependencies()} is used.
* This type is used when {@link Builder#requireLibraryDependencies()} is used. When it is turned off, however, an equivalent
* type will be code-generated into each generated bean.
*
* @param <T> type of the user defined context this attribute visitor supports
*/
@FunctionalInterface
// important note: this class is also code generated - please keep this in synch with generated code
public interface AttributeVisitor<T> {

/**
Expand Down
46 changes: 46 additions & 0 deletions builder/builder/src/main/java/io/helidon/builder/Builder.java
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,52 @@
*/
boolean allowNulls() default DEFAULT_ALLOW_NULLS;

/**
* The interceptor implementation type. See {@link BuilderInterceptor} for further details. Any interceptor applied will be called
* prior to validation. The interceptor implementation can be any lambda-like implementation for the {@link BuilderInterceptor}
* functional interface. This means that the implementation should declare a public method that matches the following:
* <pre>{@code
* Builder intercept(Builder builder);
* }
* </pre>
* Note that the method name must be named <i>intercept</i>.
*
* @return the interceptor implementation class
*/
Class<?> interceptor() default Void.class;

/**
* The (static) interceptor method to call on the {@link #interceptor()} implementation type in order to create the interceptor.
* If left undefined then the {@code new} operator will be called on the type. If provided then the method must be public
* and take no arguments. Example (see the create() method):
* <pre>{@code
* public class CustomBuilderInterceptor { // implements BuilderInterceptor
* public CustomBuilderInterceptor() {
* }
*
* public static CustomBuilderInterceptor create() {
* ...
* }
*
* public Builder intercept(Builder builder) {
* ...
* }
* }
* }
* </pre>
* <p>
* This attribute is ignored if the {@link #interceptor()} class type is left undefined.
* Note that the method must return an instance of the Builder, and there must be a public method that matches the following:
* <pre>{@code
* public Builder intercept(Builder builder);
* }
* </pre>
* Note that the method name must be named <i>intercept</i>.
*
* @return the interceptor create method
*/
String interceptorCreateMethod() default "";

/**
* The list implementation type to apply, defaulting to {@link #DEFAULT_LIST_TYPE}.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright (c) 2022 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.helidon.builder;

/**
* Provides a contract by which the {@link Builder}-annotated builder type can be intercepted (i.e., including decoration or
* mutation).
* <p>
* This type is used when {@link Builder#requireLibraryDependencies} is used. When it is turned off, however, an equivalent
* type will be code-generated into each generated bean.
* Note also that in this situation your interceptor implementation does not need to implement this interface contract,
* but instead must adhere to the following:
* <ul>
* <li>The implementation class type must provide a no-arg accessible constructor available to the generated class, unless
* the {@link io.helidon.builder.Builder#interceptorCreateMethod()} is used.
* <li>The implementation class type must provide a method-compatible (lambda) signature to the {@link #intercept} method.
* <li>Any exceptions that might be thrown from the {@link #intercept} method must be an unchecked exception type.
* </ul>
*
* @param <T> the type of the bean builder to intercept
*
* @see io.helidon.builder.Builder#interceptor()
*/
@FunctionalInterface
public interface BuilderInterceptor<T> {

/**
* Provides the ability to intercept (i.e., including decoration or mutation) the target.
*
* @param target the target being intercepted
* @return the mutated or replaced target (must not be null)
*/
T intercept(T target);

}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public class RequiredAttributeVisitor implements AttributeVisitor<Object> {
/**
* Default constructor.
*/
// important note: this needs to remain public since it will be new'ed from code-generated builder processing ...
// important note: this class is also code generated - please keep this in synch with generated code
public RequiredAttributeVisitor() {
this(Builder.DEFAULT_ALLOW_NULLS);
}
Expand All @@ -53,13 +53,13 @@ public RequiredAttributeVisitor() {
*
* @param allowNullsByDefault true if nulls should be allowed
*/
// important note: this needs to remain public since it will be new'ed from code-generated builder processing ...
// important note: this class is also code generated - please keep this in synch with generated code
public RequiredAttributeVisitor(boolean allowNullsByDefault) {
this.allowNullsByDefault = allowNullsByDefault;
}

@Override
// important note: this needs to remain public since it will be new'ed from code-generated builder processing ...
// important note: this class is also code generated - please keep this in synch with generated code
public void visit(String attrName,
Supplier<Object> valueSupplier,
Map<String, Object> meta,
Expand Down Expand Up @@ -90,7 +90,7 @@ public void visit(String attrName,
*
* @throws java.lang.IllegalStateException when any attributes are in violation with the validation policy
*/
// important note: this needs to remain public since it will be new'ed from code-generated builder processing ...
// important note: this class is also code generated - please keep this in synch with generated code
public void validate() {
if (!errors.isEmpty()) {
throw new IllegalStateException(String.join(", ", errors));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
Expand All @@ -36,6 +34,7 @@ public class DefaultTypeInfo implements TypeInfo {
private final String typeKind;
private final List<AnnotationAndValue> annotations;
private final List<TypedElementName> elementInfo;
private final List<TypedElementName> otherElementInfo;
private final TypeInfo superTypeInfo;

/**
Expand All @@ -47,8 +46,9 @@ public class DefaultTypeInfo implements TypeInfo {
protected DefaultTypeInfo(Builder b) {
this.typeName = b.typeName;
this.typeKind = b.typeKind;
this.annotations = Collections.unmodifiableList(new LinkedList<>(b.annotations));
this.elementInfo = Collections.unmodifiableList(new LinkedList<>(b.elementInfo));
this.annotations = List.copyOf(b.annotations);
this.elementInfo = List.copyOf(b.elementInfo);
this.otherElementInfo = List.copyOf(b.otherElementInfo);
this.superTypeInfo = b.superTypeInfo;
}

Expand Down Expand Up @@ -81,6 +81,11 @@ public List<TypedElementName> elementInfo() {
return elementInfo;
}

@Override
public List<TypedElementName> otherElementInfo() {
return otherElementInfo;
}

@Override
public Optional<TypeInfo> superTypeInfo() {
return Optional.ofNullable(superTypeInfo);
Expand All @@ -99,6 +104,7 @@ public String toString() {
protected String toStringInner() {
return "typeName=" + typeName()
+ ", elementInfo=" + elementInfo()
+ ", annotations=" + annotations()
+ ", superTypeInfo=" + superTypeInfo();
}

Expand All @@ -108,7 +114,7 @@ protected String toStringInner() {
public static class Builder implements io.helidon.common.Builder<Builder, DefaultTypeInfo> {
private final List<AnnotationAndValue> annotations = new ArrayList<>();
private final List<TypedElementName> elementInfo = new ArrayList<>();

private final List<TypedElementName> otherElementInfo = new ArrayList<>();
private TypeName typeName;
private String typeKind;

Expand Down Expand Up @@ -198,7 +204,32 @@ public Builder elementInfo(Collection<TypedElementName> val) {
*/
public Builder addElementInfo(TypedElementName val) {
Objects.requireNonNull(val);
elementInfo.add(Objects.requireNonNull(val));
elementInfo.add(val);
return this;
}

/**
* Sets the otherElementInfo to val.
*
* @param val the value
* @return this fluent builder
*/
public Builder otherElementInfo(Collection<TypedElementName> val) {
Objects.requireNonNull(val);
this.otherElementInfo.clear();
this.otherElementInfo.addAll(val);
return this;
}

/**
* Adds a single otherElementInfo val.
*
* @param val the value
* @return this fluent builder
*/
public Builder addOtherElementInfo(TypedElementName val) {
Objects.requireNonNull(val);
otherElementInfo.add(Objects.requireNonNull(val));
return this;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
import io.helidon.pico.types.TypedElementName;

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

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

/**
* The elements that make up the type.
* The elements that make up the type that are relevant for processing.
*
* @return the elements that make up the type
* @return the elements that make up the type that are relevant for processing
*/
List<TypedElementName> elementInfo();

/**
* The elements that make up this type that are considered "other", or being skipped because they are irrelevant to processing.
*
* @return the elements that still make up the type, but are otherwise deemed irrelevant for processing
*/
List<TypedElementName> otherElementInfo();

/**
* The parent/super class for this type info.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,21 +100,36 @@ public static boolean validateAndParseMethodName(String methodName,
return invalidMethod(methodName, throwIfInvalid, "invalid method name (must start with get or is)");
}

return validMethod(methodName.substring(3), attributeNameRef, throwIfInvalid);
return validMethod(methodName, attributeNameRef, throwIfInvalid);
}

/**
* Returns true if the word provided is considered to be a reserved word and should otherwise be avoided from generation.
*
* @param word the word
* @return true if it appears to be a reserved word
*/
public static boolean isReservedWord(String word) {
word = word.toLowerCase();
return word.equals("class") || word.equals("interface") || word.equals("package") || word.equals("static")
|| word.equals("final") || word.equals("public") || word.equals("protected") || word.equals("private")
|| word.equals("abstract");
}

private static boolean validMethod(String name,
AtomicReference<Optional<List<String>>> attributeNameRef,
boolean throwIfInvalid) {
assert (name.trim().equals(name));
char c = name.charAt(0);
String attrName = name.substring(3);
char c = attrName.charAt(0);

if (!validMethodCase(name, c, throwIfInvalid)) {
if (!validMethodCase(attrName, c, throwIfInvalid)) {
return false;
}

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

return true;
}
Expand All @@ -130,7 +145,8 @@ private static boolean validBooleanIsMethod(String name,
}

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

return true;
}
Expand Down
Loading

0 comments on commit 8d58dae

Please sign in to comment.