diff --git a/builder/README.md b/builder/README.md index 35f94258158..62884107602 100644 --- a/builder/README.md +++ b/builder/README.md @@ -13,6 +13,12 @@ The Helidon Builder 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 ConfiguredOption#required. +* Default - accomplished alternatively via Helidon's ConfiguredOption#value. + +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 toBuilder() methods +* AllArgsConstructor - must instead use one of the toBuilder() 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 @@ -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 Builder: -* Write an implementation of BuilderCreator having a higher-than-default Weighted value as compared to DefaultBuilderCreator. -* 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. diff --git a/builder/builder/src/main/java/io/helidon/builder/AttributeVisitor.java b/builder/builder/src/main/java/io/helidon/builder/AttributeVisitor.java index 91e0d9d7433..ff9bdf88623 100644 --- a/builder/builder/src/main/java/io/helidon/builder/AttributeVisitor.java +++ b/builder/builder/src/main/java/io/helidon/builder/AttributeVisitor.java @@ -22,11 +22,13 @@ /** * A functional interface that can be used to visit all attributes of this type. *

- * 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 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 { /** diff --git a/builder/builder/src/main/java/io/helidon/builder/Builder.java b/builder/builder/src/main/java/io/helidon/builder/Builder.java index 72d145431df..2e9a23d4bf9 100644 --- a/builder/builder/src/main/java/io/helidon/builder/Builder.java +++ b/builder/builder/src/main/java/io/helidon/builder/Builder.java @@ -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: + *

{@code
+     *    Builder intercept(Builder builder);
+     * }
+     * 
+ * Note that the method name must be named intercept. + * + * @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): + *
{@code
+     *  public class CustomBuilderInterceptor { // implements BuilderInterceptor
+     *      public CustomBuilderInterceptor() {
+     *      }
+     *
+     *      public static CustomBuilderInterceptor create() {
+     *          ...
+     *      }
+     *
+     *      public Builder intercept(Builder builder) {
+     *          ...
+     *      }
+     *  }
+     * }
+     * 
+ *

+ * 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: + *

{@code
+     *    public Builder intercept(Builder builder);
+     * }
+     * 
+ * Note that the method name must be named intercept. + * + * @return the interceptor create method + */ + String interceptorCreateMethod() default ""; + /** * The list implementation type to apply, defaulting to {@link #DEFAULT_LIST_TYPE}. * diff --git a/builder/builder/src/main/java/io/helidon/builder/BuilderInterceptor.java b/builder/builder/src/main/java/io/helidon/builder/BuilderInterceptor.java new file mode 100644 index 00000000000..289e846e192 --- /dev/null +++ b/builder/builder/src/main/java/io/helidon/builder/BuilderInterceptor.java @@ -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). + *

+ * 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: + *

+ * + * @param the type of the bean builder to intercept + * + * @see io.helidon.builder.Builder#interceptor() + */ +@FunctionalInterface +public interface BuilderInterceptor { + + /** + * 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); + +} diff --git a/builder/builder/src/main/java/io/helidon/builder/RequiredAttributeVisitor.java b/builder/builder/src/main/java/io/helidon/builder/RequiredAttributeVisitor.java index aaee24ed362..d3787ae8a8b 100644 --- a/builder/builder/src/main/java/io/helidon/builder/RequiredAttributeVisitor.java +++ b/builder/builder/src/main/java/io/helidon/builder/RequiredAttributeVisitor.java @@ -43,7 +43,7 @@ public class RequiredAttributeVisitor implements AttributeVisitor { /** * 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); } @@ -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 valueSupplier, Map meta, @@ -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)); diff --git a/builder/processor-spi/src/main/java/io/helidon/builder/processor/spi/DefaultTypeInfo.java b/builder/processor-spi/src/main/java/io/helidon/builder/processor/spi/DefaultTypeInfo.java index 9dd65c6b32f..1548cb9cd9b 100644 --- a/builder/processor-spi/src/main/java/io/helidon/builder/processor/spi/DefaultTypeInfo.java +++ b/builder/processor-spi/src/main/java/io/helidon/builder/processor/spi/DefaultTypeInfo.java @@ -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; @@ -36,6 +34,7 @@ public class DefaultTypeInfo implements TypeInfo { private final String typeKind; private final List annotations; private final List elementInfo; + private final List otherElementInfo; private final TypeInfo superTypeInfo; /** @@ -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; } @@ -81,6 +81,11 @@ public List elementInfo() { return elementInfo; } + @Override + public List otherElementInfo() { + return otherElementInfo; + } + @Override public Optional superTypeInfo() { return Optional.ofNullable(superTypeInfo); @@ -99,6 +104,7 @@ public String toString() { protected String toStringInner() { return "typeName=" + typeName() + ", elementInfo=" + elementInfo() + + ", annotations=" + annotations() + ", superTypeInfo=" + superTypeInfo(); } @@ -108,7 +114,7 @@ protected String toStringInner() { public static class Builder implements io.helidon.common.Builder { private final List annotations = new ArrayList<>(); private final List elementInfo = new ArrayList<>(); - + private final List otherElementInfo = new ArrayList<>(); private TypeName typeName; private String typeKind; @@ -198,7 +204,32 @@ public Builder elementInfo(Collection 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 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; } diff --git a/builder/processor-spi/src/main/java/io/helidon/builder/processor/spi/TypeInfo.java b/builder/processor-spi/src/main/java/io/helidon/builder/processor/spi/TypeInfo.java index e269c37874f..73eadde6c37 100644 --- a/builder/processor-spi/src/main/java/io/helidon/builder/processor/spi/TypeInfo.java +++ b/builder/processor-spi/src/main/java/io/helidon/builder/processor/spi/TypeInfo.java @@ -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 { @@ -50,12 +51,19 @@ public interface TypeInfo { List 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 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 otherElementInfo(); + /** * The parent/super class for this type info. * diff --git a/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/BeanUtils.java b/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/BeanUtils.java index 36a94a657c3..93b4786757b 100644 --- a/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/BeanUtils.java +++ b/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/BeanUtils.java @@ -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>> 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; } @@ -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; } diff --git a/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/BodyContext.java b/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/BodyContext.java index 19783dd34b1..d2af1a286a3 100644 --- a/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/BodyContext.java +++ b/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/BodyContext.java @@ -28,6 +28,7 @@ import io.helidon.builder.processor.spi.TypeInfo; import io.helidon.pico.types.AnnotationAndValue; import io.helidon.pico.types.DefaultAnnotationAndValue; +import io.helidon.pico.types.DefaultTypeName; import io.helidon.pico.types.TypeName; import io.helidon.pico.types.TypedElementName; @@ -66,10 +67,14 @@ public class BodyContext { private final String mapType; private final String setType; private final boolean hasParent; + private final boolean hasAnyBuilderClashingMethodNames; + private final boolean isExtendingAnAbstractClass; private final TypeName ctorBuilderAcceptTypeName; private final String genericBuilderClassDecl; private final String genericBuilderAliasDecl; private final String genericBuilderAcceptAliasDecl; + private final TypeName interceptorTypeName; + private final String interceptorCreateMethod; /** * Constructor. @@ -96,9 +101,15 @@ public class BodyContext { this.listType = toListImplType(builderTriggerAnnotation, typeInfo); this.mapType = toMapImplType(builderTriggerAnnotation, typeInfo); this.setType = toSetImplType(builderTriggerAnnotation, typeInfo); - gatherAllAttributeNames(this, typeInfo); + try { + gatherAllAttributeNames(this, typeInfo); + } catch (Exception e) { + throw new IllegalStateException("Failed while processing: " + typeInfo.typeName(), e); + } assert (allTypeInfos.size() == allAttributeNames.size()); this.hasParent = Objects.nonNull(parentTypeName.get()); + this.hasAnyBuilderClashingMethodNames = determineIfHasAnyClashingMethodNames(); + this.isExtendingAnAbstractClass = typeInfo.typeKind().equals("CLASS"); this.ctorBuilderAcceptTypeName = (hasParent) ? typeInfo.typeName() : (Objects.nonNull(parentAnnotationType.get()) && typeInfo.elementInfo().isEmpty() @@ -106,6 +117,13 @@ public class BodyContext { this.genericBuilderClassDecl = "Builder"; this.genericBuilderAliasDecl = ("B".equals(typeInfo.typeName().className())) ? "BU" : "B"; this.genericBuilderAcceptAliasDecl = ("T".equals(typeInfo.typeName().className())) ? "TY" : "T"; + String interceptorType = searchForBuilderAnnotation("interceptor", builderTriggerAnnotation, typeInfo); + this.interceptorTypeName = (interceptorType == null || Void.class.getName().equals(interceptorType)) + ? null : DefaultTypeName.createFromTypeName(interceptorType); + String interceptorCreateMethod = + searchForBuilderAnnotation("interceptorCreateMethod", builderTriggerAnnotation, typeInfo); + this.interceptorCreateMethod = (interceptorCreateMethod == null || interceptorCreateMethod.isEmpty()) + ? null : interceptorCreateMethod; } /** @@ -287,6 +305,24 @@ public boolean hasParent() { return hasParent; } + /** + * Returns true if any getter methods from the target clash with any builder method name. + * + * @return true if there is a clash + */ + public boolean hasAnyBuilderClashingMethodNames() { + return hasAnyBuilderClashingMethodNames; + } + + /** + * Returns true if this builder is extending an abstract class as a target. + * + * @return true if the target is an abstract class + */ + public boolean isExtendingAnAbstractClass() { + return isExtendingAnAbstractClass; + } + /** * Returns the streamable accept type of the builder and constructor. * @@ -323,6 +359,48 @@ protected String genericBuilderAcceptAliasDecl() { return genericBuilderAcceptAliasDecl; } + /** + * Returns the interceptor implementation type name. + * See {@link io.helidon.builder.Builder#interceptor()}. + * + * @return the interceptor type name + */ + public Optional interceptorTypeName() { + return Optional.ofNullable(interceptorTypeName); + } + + /** + * Returns the interceptor create method name. + * See {@link io.helidon.builder.Builder#interceptorCreateMethod()}. + * + * @return the interceptor create method name + */ + public Optional interceptorCreateMethod() { + return Optional.ofNullable(interceptorCreateMethod); + } + + /** + * Checks whether there is an "other" method that matches the signature. + * + * @param name the method name + * @param typeInfo the type info to check, which will look through the parent chain + * @return true if there is any matches + */ + public boolean hasOtherMethod(String name, + TypeInfo typeInfo) { + for (TypedElementName elem : typeInfo.otherElementInfo()) { + if (elem.elementName().equals(name)) { + return true; + } + } + + if (typeInfo.superTypeInfo().isPresent()) { + return hasOtherMethod(name, typeInfo.superTypeInfo().get()); + } + + return false; + } + /** * returns the bean attribute name of a particular method. * @@ -472,7 +550,8 @@ private static void gatherAllAttributeNames(BodyContext ctx, Optional alternateName = alternateNames.get().orElse(Collections.emptyList()).stream() .filter(it -> !it.equals(currentAttrName)) .findFirst(); - if (alternateName.isPresent() && !ctx.map.containsKey(alternateName.get())) { + if (alternateName.isPresent() && !ctx.map.containsKey(alternateName.get()) + && !BeanUtils.isReservedWord(alternateName.get())) { beanAttributeName = alternateName.get(); existing = ctx.map.get(beanAttributeName); } @@ -527,4 +606,14 @@ private static void populateMap(Map map, } } + private boolean determineIfHasAnyClashingMethodNames() { + return allAttributeNames().stream().anyMatch(this::isBuilderClashingMethodName); + } + + private boolean isBuilderClashingMethodName(String beanAttributeName) { + return beanAttributeName.equals("identity") + || beanAttributeName.equals("get") + || beanAttributeName.equals("toStringInner"); + } + } diff --git a/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/BuilderTypeTools.java b/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/BuilderTypeTools.java index 7c4a000f561..5b178e8b91f 100644 --- a/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/BuilderTypeTools.java +++ b/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/BuilderTypeTools.java @@ -63,6 +63,8 @@ @Weight(Weighted.DEFAULT_WEIGHT - 1) public class BuilderTypeTools implements TypeInfoCreatorProvider { + private static final boolean ACCEPT_ABSTRACT_CLASS_TARGETS = true; + /** * Default constructor. */ @@ -84,8 +86,8 @@ public Optional createTypeInfo(AnnotationAndValue annotation, return Optional.empty(); } - if (element.getKind() != ElementKind.INTERFACE && element.getKind() != ElementKind.ANNOTATION_TYPE) { - String msg = annotation.typeName() + " is intended to be used on interfaces only: " + element; + if (!isAcceptableBuilderTarget(element)) { + String msg = annotation.typeName() + " is not intended to be targeted to this type: " + element; processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, msg); throw new IllegalStateException(msg); } @@ -97,12 +99,13 @@ public Optional createTypeInfo(AnnotationAndValue annotation, .filter(it -> !it.getParameters().isEmpty()) .collect(Collectors.toList()); if (!problems.isEmpty()) { - String msg = "only simple getters with 0 args are supported: " + element + ": " + problems; + String msg = "only simple getters with no arguments are supported: " + element + ": " + problems; processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, msg); throw new IllegalStateException(msg); } - Collection elementInfo = toElementInfo(element, processingEnv); + Collection elementInfo = toElementInfo(element, processingEnv, true); + Collection otherElementInfo = toElementInfo(element, processingEnv, false); return Optional.of(DefaultTypeInfo.builder() .typeName(typeName) .typeKind(String.valueOf(element.getKind())) @@ -110,35 +113,57 @@ public Optional createTypeInfo(AnnotationAndValue annotation, .createAnnotationAndValueListFromElement(element, processingEnv.getElementUtils())) .elementInfo(elementInfo) + .otherElementInfo(otherElementInfo) .update(it -> toTypeInfo(annotation, element, processingEnv).ifPresent(it::superTypeInfo)) .build()); } + /** + * Determines if the target element with the {@link io.helidon.builder.Builder} annotation is an acceptable element type. + * If it is not acceptable then the caller is expected to throw an exception or log an error, etc. + * + * @param element the element + * @return true if the element is acceptable + */ + protected boolean isAcceptableBuilderTarget(Element element) { + final ElementKind kind = element.getKind(); + final Set modifiers = element.getModifiers(); + boolean isAcceptable = (kind == ElementKind.INTERFACE + || kind == ElementKind.ANNOTATION_TYPE + || (ACCEPT_ABSTRACT_CLASS_TARGETS + && (kind == ElementKind.CLASS && modifiers.contains(Modifier.ABSTRACT)))); + return isAcceptable; + } + /** * Translation the arguments to a collection of {@link io.helidon.pico.types.TypedElementName}'s. * - * @param element the typed element (i.e., class) - * @param processingEnv the processing env + * @param element the typed element (i.e., class) + * @param processingEnv the processing env + * @param wantWhatWeCanAccept pass true to get the elements we can accept to process, false for the other ones * @return the collection of typed elements */ - protected Collection toElementInfo(TypeElement element, ProcessingEnvironment processingEnv) { + protected Collection toElementInfo(TypeElement element, + ProcessingEnvironment processingEnv, + boolean wantWhatWeCanAccept) { return element.getEnclosedElements().stream() .filter(it -> it.getKind() == ElementKind.METHOD) .map(ExecutableElement.class::cast) - .filter(this::canAccept) + .filter(it -> (wantWhatWeCanAccept == canAccept(it))) .map(it -> createTypedElementNameFromElement(it, processingEnv.getElementUtils())) .collect(Collectors.toList()); } /** - * Returns true if the executable element passed is acceptable for processing (i.e., not a static and not a default method). + * Returns true if the executable element passed is acceptable for processing (i.e., not a static and not a default method + * on interfaces, and abstract methods on abstract classes). * * @param ee the executable element - * @return true if not default and not static + * @return true if not able to accept */ protected boolean canAccept(ExecutableElement ee) { Set mods = ee.getModifiers(); - return !mods.contains(Modifier.DEFAULT) && !mods.contains(Modifier.STATIC); + return mods.contains(Modifier.ABSTRACT); } private Optional toTypeInfo(AnnotationAndValue annotation, @@ -147,7 +172,7 @@ private Optional toTypeInfo(AnnotationAndValue annotation, List ifaces = element.getInterfaces(); if (ifaces.size() > 1) { processingEnv.getMessager() - .printMessage(Diagnostic.Kind.ERROR, "currently only supports one parent interface: " + element); + .printMessage(Diagnostic.Kind.ERROR, "only supports one parent interface: " + element); } else if (ifaces.isEmpty()) { return Optional.empty(); } diff --git a/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/DefaultBuilderCreatorProvider.java b/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/DefaultBuilderCreatorProvider.java index e6cc3f58c38..965c992e5d3 100644 --- a/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/DefaultBuilderCreatorProvider.java +++ b/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/DefaultBuilderCreatorProvider.java @@ -33,6 +33,7 @@ import io.helidon.builder.Annotated; import io.helidon.builder.AttributeVisitor; import io.helidon.builder.Builder; +import io.helidon.builder.BuilderInterceptor; import io.helidon.builder.RequiredAttributeVisitor; import io.helidon.builder.Singular; import io.helidon.builder.processor.spi.BuilderCreatorProvider; @@ -85,25 +86,18 @@ public Set> supportedAnnotationTypes() { @Override public List create(TypeInfo typeInfo, AnnotationAndValue builderAnnotation) { try { - Optional abstractImplTypeName = toAbstractImplTypeName(typeInfo.typeName(), builderAnnotation); - Optional implTypeName = toImplTypeName(typeInfo.typeName(), builderAnnotation); - if (implTypeName.isEmpty()) { - return Collections.emptyList(); - } - preValidate(implTypeName.get(), typeInfo, builderAnnotation); + TypeName abstractImplTypeName = toAbstractImplTypeName(typeInfo.typeName(), builderAnnotation); + TypeName implTypeName = toImplTypeName(typeInfo.typeName(), builderAnnotation); + preValidate(implTypeName, typeInfo, builderAnnotation); LinkedList builds = new LinkedList<>(); - if (abstractImplTypeName.isPresent()) { - TypeName typeName = abstractImplTypeName.get(); - builds.add(DefaultTypeAndBody.builder() - .typeName(typeName) - .body(toBody(createBodyContext(false, typeName, typeInfo, builderAnnotation))) - .build()); - } - TypeName typeName = implTypeName.get(); builds.add(DefaultTypeAndBody.builder() - .typeName(typeName) - .body(toBody(createBodyContext(true, typeName, typeInfo, builderAnnotation))) + .typeName(abstractImplTypeName) + .body(toBody(createBodyContext(false, abstractImplTypeName, typeInfo, builderAnnotation))) + .build()); + builds.add(DefaultTypeAndBody.builder() + .typeName(implTypeName) + .body(toBody(createBodyContext(true, implTypeName, typeInfo, builderAnnotation))) .build()); return postValidate(builds); @@ -139,14 +133,14 @@ protected List postValidate(List builds) { * * @param typeName the target interface that the builder applies to * @param builderAnnotation the builder annotation triggering the build - * @return the abstract type name of the implementation, or empty if this should not be code generated + * @return the abstract type name of the implementation */ - protected Optional toAbstractImplTypeName(TypeName typeName, - AnnotationAndValue builderAnnotation) { + protected TypeName toAbstractImplTypeName(TypeName typeName, + AnnotationAndValue builderAnnotation) { String toPackageName = toPackageName(typeName.packageName(), builderAnnotation); String prefix = toAbstractImplTypePrefix(builderAnnotation); String suffix = toImplTypeSuffix(builderAnnotation); - return Optional.of(DefaultTypeName.create(toPackageName, prefix + typeName.className() + suffix)); + return DefaultTypeName.create(toPackageName, prefix + typeName.className() + suffix); } /** @@ -154,14 +148,14 @@ protected Optional toAbstractImplTypeName(TypeName typeName, * * @param typeName the target interface that the builder applies to * @param builderAnnotation the builder annotation triggering the build - * @return the type name of the implementation, or empty if this should not be code generated + * @return the type name of the implementation */ - protected Optional toImplTypeName(TypeName typeName, - AnnotationAndValue builderAnnotation) { + protected TypeName toImplTypeName(TypeName typeName, + AnnotationAndValue builderAnnotation) { String toPackageName = toPackageName(typeName.packageName(), builderAnnotation); String prefix = toImplTypePrefix(builderAnnotation); String suffix = toImplTypeSuffix(builderAnnotation); - return Optional.of(DefaultTypeName.create(toPackageName, prefix + typeName.className() + suffix)); + return DefaultTypeName.create(toPackageName, prefix + typeName.className() + suffix); } /** @@ -193,7 +187,7 @@ protected String toBody(BodyContext ctx) { appendFields(builder, ctx); appendCtor(builder, ctx); appendExtraPostCtorCode(builder, ctx); - appendInterfaceBasedGetters(builder, ctx); + appendInterfaceBasedGetters(builder, ctx, "", false); appendBasicGetters(builder, ctx); appendMetaAttributes(builder, ctx); appendToStringMethod(builder, ctx); @@ -219,18 +213,46 @@ protected void appendFooter(StringBuilder builder, builder.append("}\n"); } + /** + * Appends any interceptor on the builder. + * + * @param builder the builder + * @param ctx the context + * @param builderTag the tag (variable name) used for the builder arg + */ + protected void maybeAppendInterceptor(StringBuilder builder, + BodyContext ctx, + String builderTag) { + assert (!builderTag.equals("interceptor")); + if (ctx.interceptorTypeName().isPresent()) { + String impl = ctx.interceptorTypeName().get().name(); + builder.append("\t\t\t").append(impl).append(" interceptor = "); + if (ctx.interceptorCreateMethod().isEmpty()) { + builder.append("new ").append(impl).append("();\n"); + } else { + builder.append(ctx.interceptorTypeName().get()) + .append(".").append(ctx.interceptorCreateMethod().get()).append("();\n"); + } + builder.append("\t\t\t").append(builderTag) + .append(" = (Builder) interceptor.intercept(").append(builderTag).append(");\n"); + } + } + /** * Appends the simple {@link io.helidon.config.metadata.ConfiguredOption#required()} validation inside the build() method. * - * @param builder the builder - * @param ctx the context + * @param builder the builder + * @param ctx the context + * @param builderTag the tag (variable name) used for the builder arg */ - protected void appendRequiredValidator(StringBuilder builder, - BodyContext ctx) { + protected void appendRequiredVisitor(StringBuilder builder, + BodyContext ctx, + String builderTag) { + assert (!builderTag.equals("visitor")); if (ctx.includeMetaAttributes()) { builder.append("\t\t\tRequiredAttributeVisitor visitor = new RequiredAttributeVisitor(") .append(ctx.allowNulls()).append(");\n" - + "\t\t\tvisitAttributes(visitor, null);\n" + + "\t\t\t").append(builderTag).append(".visitAttributes(visitor, null);\n" + "\t\t\tvisitor.validate();\n"); } } @@ -247,13 +269,6 @@ protected void appendBasicGetters(StringBuilder builder, return; } - if (Objects.nonNull(ctx.parentAnnotationType().get())) { - builder.append("\t@Override\n"); - builder.append("\tpublic Class annotationType() {\n"); - builder.append("\t\treturn ").append(ctx.typeInfo().superTypeInfo().get().typeName()).append(".class;\n"); - builder.append("\t}\n\n"); - } - if (!ctx.hasParent() && ctx.hasStreamSupportOnImpl()) { builder.append("\t@Override\n" + "\tpublic T get() {\n" @@ -340,34 +355,36 @@ protected void appendHeader(StringBuilder builder, } builder.append("class ").append(ctx.implTypeName().className()); - if (ctx.hasParent() || ctx.doingConcreteType()) { + Optional baseExtendsTypeName = baseExtendsTypeName(ctx); + if (baseExtendsTypeName.isEmpty() && ctx.isExtendingAnAbstractClass()) { + baseExtendsTypeName = Optional.of(ctx.typeInfo().typeName()); + } + if (ctx.hasParent() || ctx.doingConcreteType() || baseExtendsTypeName.isPresent()) { builder.append(" extends "); } if (ctx.doingConcreteType()) { - builder.append(toAbstractImplTypeName(ctx.typeInfo().typeName(), ctx.builderTriggerAnnotation()).get()); + builder.append(toAbstractImplTypeName(ctx.typeInfo().typeName(), ctx.builderTriggerAnnotation())); } else { if (ctx.hasParent()) { - builder.append(toAbstractImplTypeName(ctx.parentTypeName().get(), ctx.builderTriggerAnnotation()).get()); - } else { - Optional baseExtendsTypeName = baseExtendsTypeName(ctx); - if (baseExtendsTypeName.isPresent()) { - builder.append(" extends ").append(baseExtendsTypeName.get().fqName()).append("\n\t\t\t\t\t\t\t\t\t\t"); - } + builder.append(toAbstractImplTypeName(ctx.parentTypeName().get(), ctx.builderTriggerAnnotation())); + } else if (baseExtendsTypeName.isPresent()) { + builder.append(baseExtendsTypeName.get().fqName()); } - if (!ctx.hasParent() && ctx.hasStreamSupportOnImpl()) { - builder.append("<").append(ctx.genericBuilderAcceptAliasDecl()).append(" extends ") - .append(ctx.implTypeName().className()).append(">"); + LinkedList impls = new LinkedList<>(); + if (!ctx.isExtendingAnAbstractClass()) { + impls.add(ctx.typeInfo().typeName().fqName()); } - - builder.append(" implements ").append(ctx.typeInfo().typeName()); if (!ctx.hasParent() && ctx.hasStreamSupportOnImpl()) { - builder.append(", Supplier<").append(ctx.genericBuilderAcceptAliasDecl()).append(">"); + impls.add("Supplier<" + ctx.genericBuilderAcceptAliasDecl() + ">"); } - List extraImplementContracts = extraImplementedTypeNames(ctx); - extraImplementContracts.forEach(t -> builder.append(",\n\t\t\t\t\t\t\t\t\t\t\t").append(t.fqName())); + extraImplementContracts.forEach(t -> impls.add(t.fqName())); + + if (!impls.isEmpty()) { + builder.append(" implements ").append(String.join(", ", impls)); + } } builder.append(" {\n"); @@ -432,6 +449,7 @@ protected void appendExtraImports(StringBuilder builder, if (ctx.doingConcreteType()) { builder.append("import ").append(RequiredAttributeVisitor.class.getName()).append(";\n"); } + builder.append("import ").append(BuilderInterceptor.class.getName()).append(";\n"); builder.append("\n"); } } @@ -448,17 +466,19 @@ protected void appendToStringMethod(StringBuilder builder, return; } - builder.append("\t@Override\n"); - builder.append("\tpublic String toString() {\n"); - builder.append("\t\treturn ").append(ctx.typeInfo().typeName()); - builder.append(".class.getSimpleName() + "); + if (!ctx.hasOtherMethod("toString", ctx.typeInfo())) { + builder.append("\t@Override\n"); + builder.append("\tpublic String toString() {\n"); + builder.append("\t\treturn ").append(ctx.typeInfo().typeName()); + builder.append(".class.getSimpleName() + "); - String instanceIdRef = instanceIdRef(ctx); - if (!instanceIdRef.isBlank()) { - builder.append("\"{\" + ").append(instanceIdRef).append(" + \"}\" + "); + String instanceIdRef = instanceIdRef(ctx); + if (!instanceIdRef.isBlank()) { + builder.append("\"{\" + ").append(instanceIdRef).append(" + \"}\" + "); + } + builder.append("\"(\" + toStringInner() + \")\";\n"); + builder.append("\t}\n\n"); } - builder.append("\"(\" + toStringInner() + \")\";\n"); - builder.append("\t}\n\n"); } /** @@ -486,8 +506,8 @@ protected void appendExtraMethods(StringBuilder builder, } /** - * Adds extra inner classes to write on the builder. This default implementation will write the AttributeVisitor inner class - * if this is the root interface (i.e., hasParent is false), as well as the RequiredAttributeVisitor. + * Adds extra inner classes to write on the builder. This default implementation will write the {@code AttributeVisitor} and + * {@code RequiredAttributeVisitor} inner classes on the base abstract parent (ie, hasParent is false). * * @param builder the builder * @param ctx the context @@ -631,13 +651,17 @@ protected void appendExtraBuilderFields(StringBuilder builder, } /** - * Adds extra builder pre-steps. + * Adds extra builder build() method pre-steps prior to the builder being built into the target. * * @param builder the builder * @param ctx the context + * @param builderTag the tag (variable name) used for the builder arg */ protected void appendBuilderBuildPreSteps(StringBuilder builder, - BodyContext ctx) { + BodyContext ctx, + String builderTag) { + maybeAppendInterceptor(builder, ctx, builderTag); + appendRequiredVisitor(builder, ctx, builderTag); } /** @@ -1025,7 +1049,12 @@ private void appendBuilder(StringBuilder builder, appendExtraBuilderFields(builder, ctx); appendBuilderBody(builder, ctx); -// appendExtraBuilderMethods(builder, ctx); + if (ctx.hasAnyBuilderClashingMethodNames()) { + builder.append("\t\t// *** IMPORTANT NOTE: There are getter methods that clash with the base Builder methods ***\n"); + appendInterfaceBasedGetters(builder, ctx, "\t//", true); + } else { + appendInterfaceBasedGetters(builder, ctx, "\t", true); + } if (ctx.doingConcreteType()) { if (ctx.hasParent()) { @@ -1034,9 +1063,9 @@ private void appendBuilder(StringBuilder builder, GenerateJavadoc.buildMethod(builder); } builder.append("\t\tpublic ").append(ctx.implTypeName()).append(" build() {\n"); - appendRequiredValidator(builder, ctx); - appendBuilderBuildPreSteps(builder, ctx); - builder.append("\t\t\treturn new ").append(ctx.implTypeName().className()).append("(this);\n"); + builder.append("\t\t\tBuilder b = this;\n"); + appendBuilderBuildPreSteps(builder, ctx, "b"); + builder.append("\t\t\treturn new ").append(ctx.implTypeName().className()).append("(b);\n"); builder.append("\t\t}\n"); } else { int i = 0; @@ -1055,7 +1084,8 @@ private void appendBuilder(StringBuilder builder, String basicAttributeName = "" + Character.toLowerCase(beanAttributeName.charAt(2)) + beanAttributeName.substring(3); - if (!ctx.allAttributeNames().contains(basicAttributeName)) { + if (!BeanUtils.isReservedWord(basicAttributeName) + && !ctx.allAttributeNames().contains(basicAttributeName)) { appendSetter(builder, ctx, beanAttributeName, basicAttributeName, method); } } @@ -1173,7 +1203,7 @@ private void appendBuilderBody(StringBuilder builder, if (typeName.isList() || typeName.isMap() || typeName.isSet()) { continue; } - addField(builder, method, typeName, beanAttributeName); + addBuilderField(builder, ctx, method, typeName, beanAttributeName); } builder.append("\n"); } @@ -1192,13 +1222,14 @@ private void appendBuilderBody(StringBuilder builder, builder.append("\t\t}\n\n"); } - private void addField(StringBuilder builder, - TypedElementName method, - TypeName type, - String beanAttributeName) { - + private void addBuilderField(StringBuilder builder, + BodyContext ctx, + TypedElementName method, + TypeName type, + String beanAttributeName) { GenerateJavadoc.builderField(builder, method); - builder.append("\t\tprotected ").append(type.array() ? type.fqName() : type.name()).append(" ") + builder.append("\t\tprivate "); + builder.append(type.array() ? type.fqName() : type.name()).append(" ") .append(beanAttributeName); Optional defaultVal = toConfiguredOptionValue(method, true, true); if (defaultVal.isPresent()) { @@ -1252,7 +1283,7 @@ private void appendBuilderHeader(StringBuilder builder, if (ctx.doingConcreteType()) { builder.append(" extends "); - builder.append(toAbstractImplTypeName(ctx.typeInfo().typeName(), ctx.builderTriggerAnnotation()).get()); + builder.append(toAbstractImplTypeName(ctx.typeInfo().typeName(), ctx.builderTriggerAnnotation())); builder.append(".").append(ctx.genericBuilderClassDecl()); builder.append("<").append(ctx.genericBuilderClassDecl()).append(", ").append(ctx.ctorBuilderAcceptTypeName()) .append("> {\n"); @@ -1261,40 +1292,46 @@ private void appendBuilderHeader(StringBuilder builder, builder.append("<").append(ctx.genericBuilderAliasDecl()).append(", "); builder.append(ctx.genericBuilderAcceptAliasDecl()).append(">, ").append(ctx.genericBuilderAcceptAliasDecl()) .append(" extends "); - builder.append(ctx.ctorBuilderAcceptTypeName()).append("> "); + builder.append(ctx.ctorBuilderAcceptTypeName()).append(">"); if (ctx.hasParent()) { - builder.append("extends ") - .append(toAbstractImplTypeName(ctx.parentTypeName().get(), ctx.builderTriggerAnnotation()).get()) + builder.append(" extends ") + .append(toAbstractImplTypeName(ctx.parentTypeName().get(), ctx.builderTriggerAnnotation())) .append(".").append(ctx.genericBuilderClassDecl()); builder.append("<").append(ctx.genericBuilderAliasDecl()) .append(", ").append(ctx.genericBuilderAcceptAliasDecl()); builder.append(">"); } else { Optional baseExtendsTypeName = baseExtendsBuilderTypeName(ctx); + if (baseExtendsTypeName.isEmpty() && ctx.isExtendingAnAbstractClass()) { + baseExtendsTypeName = Optional.of(ctx.typeInfo().typeName()); + } if (baseExtendsTypeName.isPresent()) { - builder.append("\n\t\t\t\t\t\t\t\t\t\t" - + "extends ") - .append(baseExtendsTypeName.get().fqName()) - .append("\n\t\t\t\t\t\t\t\t\t\t"); + builder.append("\n\t\t\t\t\t\t\t\t\t\textends ") + .append(baseExtendsTypeName.get().fqName()); } } - if (ctx.hasStreamSupportOnBuilder()) { - builder.append("implements Supplier<").append(ctx.genericBuilderAcceptAliasDecl()).append(">"); + LinkedList impls = new LinkedList<>(); + if (!ctx.isExtendingAnAbstractClass() && !ctx.hasAnyBuilderClashingMethodNames()) { + impls.add(ctx.typeInfo().typeName().name()); } - if (!ctx.hasParent()) { + if (ctx.hasStreamSupportOnBuilder() && !ctx.requireLibraryDependencies()) { + impls.add("Supplier<" + ctx.genericBuilderAcceptAliasDecl() + ">"); + } + if (ctx.requireLibraryDependencies()) { - builder.append(", io.helidon.common.Builder<").append(ctx.genericBuilderAliasDecl()) - .append(", ").append(ctx.genericBuilderAcceptAliasDecl()).append(">"); - } else { - builder.append("/*, io.helidon.common.Builder<").append(ctx.genericBuilderAliasDecl()) - .append(", ").append(ctx.genericBuilderAcceptAliasDecl()).append("> */"); + impls.add(io.helidon.common.Builder.class.getName() + + "<" + ctx.genericBuilderAliasDecl() + ", " + ctx.genericBuilderAcceptAliasDecl() + ">"); } + + List extraImplementBuilderContracts = extraImplementedBuilderContracts(ctx); + extraImplementBuilderContracts.forEach(t -> impls.add(t.fqName())); } - List extraImplementBuilderContracts = extraImplementedBuilderContracts(ctx); - extraImplementBuilderContracts.forEach(t -> builder.append(",\n\t\t\t\t\t\t\t\t\t\t\t").append(t.fqName())); + if (!impls.isEmpty()) { + builder.append(" implements ").append(String.join(", ", impls)); + } builder.append(" {\n"); } @@ -1311,7 +1348,9 @@ private void appendToBuilderMethods(StringBuilder builder, } private void appendInterfaceBasedGetters(StringBuilder builder, - BodyContext ctx) { + BodyContext ctx, + String prefix, + boolean isBuilder) { if (ctx.doingConcreteType()) { return; } @@ -1319,14 +1358,30 @@ private void appendInterfaceBasedGetters(StringBuilder builder, int i = 0; for (String beanAttributeName : ctx.allAttributeNames()) { TypedElementName method = ctx.allTypeInfos().get(i); - appendAnnotations(builder, method.annotations(), "\t"); - builder.append("\t@Override\n"); - builder.append("\tpublic ").append(toGenerics(method, false)).append(" ").append(method.elementName()) + String extraPrefix = prefix + "\t"; + appendAnnotations(builder, method.annotations(), extraPrefix); + builder.append(extraPrefix) + .append("@Override\n"); + builder.append(extraPrefix) + .append("public ").append(toGenerics(method, false)).append(" ").append(method.elementName()) .append("() {\n"); - builder.append("\t\treturn ").append(beanAttributeName).append(";\n"); - builder.append("\t}\n\n"); + builder.append(extraPrefix) + .append("\treturn ").append(beanAttributeName).append(";\n"); + builder.append(extraPrefix) + .append("}\n\n"); i++; } + + if (Objects.nonNull(ctx.parentAnnotationType().get())) { + builder.append(prefix) + .append("\t@Override\n"); + builder.append(prefix) + .append("\tpublic Class annotationType() {\n"); + builder.append(prefix) + .append("\t\treturn ").append(ctx.typeInfo().superTypeInfo().get().typeName()).append(".class;\n"); + builder.append(prefix) + .append("\t}\n\n"); + } } private void appendCtor(StringBuilder builder, @@ -1345,7 +1400,6 @@ private void appendCtor(StringBuilder builder, builder.append(" b) {\n"); appendExtraCtorCode(builder, ctx, "b"); appendCtorCodeBody(builder, ctx, "b"); -// builder.append("\t}\n"); } builder.append("\t}\n\n"); @@ -1356,13 +1410,13 @@ private void appendCtor(StringBuilder builder, * * @param builder the builder * @param ctx the context - * @param builderTag the builder tag + * @param builderTag the tag (variable name) used for the builder arg */ protected void appendCtorCodeBody(StringBuilder builder, BodyContext ctx, String builderTag) { if (ctx.hasParent()) { - builder.append("\t\tsuper(b);\n"); + builder.append("\t\tsuper(").append(builderTag).append(");\n"); } int i = 0; for (String beanAttributeName : ctx.allAttributeNames()) { @@ -1371,15 +1425,18 @@ protected void appendCtorCodeBody(StringBuilder builder, if (method.typeName().isList()) { builder.append("Collections.unmodifiableList(new ") - .append(ctx.listType()).append("<>(b.").append(beanAttributeName).append("));\n"); + .append(ctx.listType()).append("<>(").append(builderTag).append(".") + .append(beanAttributeName).append("));\n"); } else if (method.typeName().isMap()) { builder.append("Collections.unmodifiableMap(new ") - .append(ctx.mapType()).append("<>(b.").append(beanAttributeName).append("));\n"); + .append(ctx.mapType()).append("<>(").append(builderTag).append(".") + .append(beanAttributeName).append("));\n"); } else if (method.typeName().isSet()) { builder.append("Collections.unmodifiableSet(new ") - .append(ctx.setType()).append("<>(b.").append(beanAttributeName).append("));\n"); + .append(ctx.setType()).append("<>(").append(builderTag).append(".") + .append(beanAttributeName).append("));\n"); } else { - builder.append("b.").append(beanAttributeName).append(";\n"); + builder.append(builderTag).append(".").append(beanAttributeName).append(";\n"); } } } @@ -1390,40 +1447,44 @@ private void appendHashCodeAndEquals(StringBuilder builder, return; } - builder.append("\t@Override\n"); - builder.append("\tpublic int hashCode() {\n"); - if (ctx.hasParent()) { - builder.append("\t\tint hashCode = super.hashCode();\n"); - } else { - builder.append("\t\tint hashCode = 1;\n"); - } - List methods = new ArrayList<>(); - for (TypedElementName method : ctx.allTypeInfos()) { - methods.add(method.elementName() + "()"); + if (!ctx.hasOtherMethod("hashCode", ctx.typeInfo())) { + builder.append("\t@Override\n"); + builder.append("\tpublic int hashCode() {\n"); + if (ctx.hasParent()) { + builder.append("\t\tint hashCode = super.hashCode();\n"); + } else { + builder.append("\t\tint hashCode = 1;\n"); + } + List methods = new ArrayList<>(); + for (TypedElementName method : ctx.allTypeInfos()) { + methods.add(method.elementName() + "()"); + } + builder.append("\t\thashCode = 31 * hashCode + Objects.hash(").append(String.join(", ", methods)).append(");\n"); + builder.append("\t\treturn hashCode;\n"); + builder.append("\t}\n\n"); } - builder.append("\t\thashCode = 31 * hashCode + Objects.hash(").append(String.join(", ", methods)).append(");\n"); - builder.append("\t\treturn hashCode;\n"); - builder.append("\t}\n\n"); - builder.append("\t@Override\n"); - builder.append("\tpublic boolean equals(Object another) {\n"); - builder.append("\t\tif (this == another) {\n\t\t\treturn true;\n\t\t}\n"); - builder.append("\t\tif (!(another instanceof ").append(ctx.typeInfo().typeName()).append(")) {\n"); - builder.append("\t\t\treturn false;\n"); - builder.append("\t\t}\n"); - builder.append("\t\t").append(ctx.typeInfo().typeName()).append(" other = (") - .append(ctx.typeInfo().typeName()).append(") another;\n"); - if (ctx.hasParent()) { - builder.append("\t\tboolean equals = super.equals(other);\n"); - } else { - builder.append("\t\tboolean equals = true;\n"); - } - for (TypedElementName method : ctx.allTypeInfos()) { - builder.append("\t\tequals &= Objects.equals(").append(method.elementName()).append("(), other.") - .append(method.elementName()).append("());\n"); + if (!ctx.hasOtherMethod("equals", ctx.typeInfo())) { + builder.append("\t@Override\n"); + builder.append("\tpublic boolean equals(Object another) {\n"); + builder.append("\t\tif (this == another) {\n\t\t\treturn true;\n\t\t}\n"); + builder.append("\t\tif (!(another instanceof ").append(ctx.typeInfo().typeName()).append(")) {\n"); + builder.append("\t\t\treturn false;\n"); + builder.append("\t\t}\n"); + builder.append("\t\t").append(ctx.typeInfo().typeName()).append(" other = (") + .append(ctx.typeInfo().typeName()).append(") another;\n"); + if (ctx.hasParent()) { + builder.append("\t\tboolean equals = super.equals(other);\n"); + } else { + builder.append("\t\tboolean equals = true;\n"); + } + for (TypedElementName method : ctx.allTypeInfos()) { + builder.append("\t\tequals &= Objects.equals(").append(method.elementName()).append("(), other.") + .append(method.elementName()).append("());\n"); + } + builder.append("\t\treturn equals;\n"); + builder.append("\t}\n\n"); } - builder.append("\t\treturn equals;\n"); - builder.append("\t}\n\n"); } private void appendInnerToStringMethod(StringBuilder builder, @@ -1527,7 +1588,6 @@ private void appendDefaultValueAssignment(StringBuilder builder, private void appendOverridesOfDefaultValues(StringBuilder builder, BodyContext ctx) { - boolean first = true; for (TypedElementName method : ctx.typeInfo().elementInfo()) { String beanAttributeName = toBeanAttributeName(method, ctx.beanStyleRequired()); if (!ctx.allAttributeNames().contains(beanAttributeName)) { diff --git a/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/GenerateVisitorSupport.java b/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/GenerateVisitorSupport.java index 9756d164652..b7427e75375 100644 --- a/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/GenerateVisitorSupport.java +++ b/builder/processor-tools/src/main/java/io/helidon/builder/processor/tools/GenerateVisitorSupport.java @@ -34,6 +34,7 @@ static void appendExtraInnerClasses(StringBuilder builder, && !ctx.requireLibraryDependencies()) { builder.append("\n\n\t/**\n" + "\t * A functional interface that can be used to visit all attributes of this type.\n" + + "\t *\n" + "\t * @param type of user defined context" + "\t */\n"); builder.append("\t@FunctionalInterface\n" diff --git a/builder/processor/src/main/java/io/helidon/builder/processor/BuilderProcessor.java b/builder/processor/src/main/java/io/helidon/builder/processor/BuilderProcessor.java index d1a67b8891b..19977c8d4ff 100644 --- a/builder/processor/src/main/java/io/helidon/builder/processor/BuilderProcessor.java +++ b/builder/processor/src/main/java/io/helidon/builder/processor/BuilderProcessor.java @@ -109,7 +109,8 @@ public void init(ProcessingEnvironment processingEnv) { } @Override - public boolean process(Set annotations, RoundEnvironment roundEnv) { + public boolean process(Set annotations, + RoundEnvironment roundEnv) { if (roundEnv.processingOver() || tools == null || PRODUCERS.isEmpty()) { elementsProcessed.clear(); return false; @@ -146,7 +147,8 @@ public boolean process(Set annotations, RoundEnvironment * @param element the element being processed * @throws IOException if unable to write the generated class */ - protected void process(Class annoType, Element element) throws IOException { + protected void process(Class annoType, + Element element) throws IOException { AnnotationMirror am = BuilderTypeTools.findAnnotationMirror(annoType.getName(), element.getAnnotationMirrors()) .orElseThrow(() -> new IllegalArgumentException("Cannot find annotation mirror for " + annoType diff --git a/builder/tests/builder/pom.xml b/builder/tests/builder/pom.xml index 9444fa1709d..c82616d4352 100644 --- a/builder/tests/builder/pom.xml +++ b/builder/tests/builder/pom.xml @@ -31,14 +31,6 @@ helidon-builder-test-builder Helidon Builder Tests - - true - true - true - true - true - - io.helidon.builder diff --git a/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/AbstractWithCustomMethods.java b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/AbstractWithCustomMethods.java new file mode 100644 index 00000000000..e9d3f12fa5a --- /dev/null +++ b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/AbstractWithCustomMethods.java @@ -0,0 +1,114 @@ +/* + * 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.test.testsubjects; + +import java.util.Optional; + +import io.helidon.builder.Builder; + +/** + * Usage of an abstract class target, as well as overriding {@link #toString()}, {@link #hashCode()}, and {@link #equals(Object)}. + */ +@Builder(interceptor = GeneralInterceptor.class) +public abstract class AbstractWithCustomMethods { + + /** + * Used for testing. + * + * @return ignored, here for testing purposes only + */ + public abstract String name(); + + /** + * Used for testing. + * + * @return ignored, here for testing purposes only + */ + public abstract boolean isStatic(); + + /** + * Used for testing. + * + * @return ignored, here for testing purposes only + */ + public abstract boolean isClass(); + + /** + * Used for testing. + * + * @return ignored, here for testing purposes only + */ + public abstract boolean isInterface(); + + /** + * Used for testing. + * + * @return ignored, here for testing purposes only + */ + public abstract boolean isAbstract(); + + /** + * Used for testing. + * + * @return ignored, here for testing purposes only + */ + public abstract boolean isFinal(); + + /** + * Used for testing. + * + * @return ignored, here for testing purposes only + */ + public abstract boolean isPublic(); + + /** + * Used for testing. + * + * @return ignored, here for testing purposes only + */ + public abstract boolean isProtected(); + + /** + * Used for testing. + * + * @return ignored, here for testing purposes only + */ + public abstract Optional getAbstract(); + + /** + * Used for testing. + * + * @return ignored, here for testing purposes only + */ + public abstract boolean isPrivate(); + + @Override + public String toString() { + return name(); + } + + @Override + public int hashCode() { + return 1; + } + + @Override + public boolean equals(Object another) { + return true; + } + +} diff --git a/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/BeanBuilderInterceptor.java b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/BeanBuilderInterceptor.java new file mode 100644 index 00000000000..87560c41347 --- /dev/null +++ b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/BeanBuilderInterceptor.java @@ -0,0 +1,34 @@ +/* + * 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.test.testsubjects; + +import io.helidon.builder.BuilderInterceptor; + +/** + * See {@link InterceptedBean}. + */ +class BeanBuilderInterceptor implements BuilderInterceptor { + private int callCount; + + @Override + public DefaultInterceptedBean.Builder intercept(DefaultInterceptedBean.Builder target) { + if (callCount++ > 0) { + throw new AssertionError(); + } + return target.helloMessage("Hello " + target.name()); + } +} diff --git a/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/GeneralInterceptor.java b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/GeneralInterceptor.java new file mode 100644 index 00000000000..847869c0844 --- /dev/null +++ b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/GeneralInterceptor.java @@ -0,0 +1,52 @@ +/* + * 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.test.testsubjects; + +import java.util.LinkedList; +import java.util.List; + +import io.helidon.common.Builder; + +/** + * Here for testing. + */ +@Deprecated +@SuppressWarnings("unchecked") +public class GeneralInterceptor { + private static final List INTERCEPT_CALLS = new LinkedList<>(); + + /** + * Generic interceptor. + * + * @param builder generic builder + * @return the builder + */ + public Object intercept(Builder builder) { + INTERCEPT_CALLS.add(builder); + return builder; + } + + /** + * Gets all interceptor calls ever made. + * + * @return all interceptor calls + */ + public static List getInterceptCalls() { + return INTERCEPT_CALLS; + } + +} diff --git a/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/InterceptedBean.java b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/InterceptedBean.java new file mode 100644 index 00000000000..bcc798333a5 --- /dev/null +++ b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/InterceptedBean.java @@ -0,0 +1,43 @@ +/* + * 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.test.testsubjects; + +import io.helidon.builder.Builder; +import io.helidon.config.metadata.ConfiguredOption; + +/** + * Demonstrates interception of builders. + */ +@Builder(interceptor = BeanBuilderInterceptor.class) +public interface InterceptedBean { + + /** + * The name to say hello to. + * + * @return the name + */ + @ConfiguredOption(required = true) + String name(); + + /** + * The hello message will be explicitly overridden to say "hello {@link #name()}". + * + * @return the hello message + */ + String helloMessage(); + +} diff --git a/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/package-info.java b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/package-info.java index 245b94dfffb..51a236d59d9 100644 --- a/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/package-info.java +++ b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/package-info.java @@ -15,6 +15,6 @@ */ /** - * Test subjects for the Pico Builder. + * Test subjects for the Builder. */ package io.helidon.builder.test.testsubjects; diff --git a/builder/tests/builder/src/test/java/io/helidon/builder/test/AbstractWithCustomMethodsTest.java b/builder/tests/builder/src/test/java/io/helidon/builder/test/AbstractWithCustomMethodsTest.java new file mode 100644 index 00000000000..87acea1dc7b --- /dev/null +++ b/builder/tests/builder/src/test/java/io/helidon/builder/test/AbstractWithCustomMethodsTest.java @@ -0,0 +1,52 @@ +/* + * 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.test; + +import io.helidon.builder.test.testsubjects.AbstractWithCustomMethods; +import io.helidon.builder.test.testsubjects.DefaultAbstractWithCustomMethods; +import io.helidon.builder.test.testsubjects.GeneralInterceptor; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +class AbstractWithCustomMethodsTest { + + /** + * Testing more corner cases.. + */ + @Test + void testIt() { + AbstractWithCustomMethods val = DefaultAbstractWithCustomMethods.builder() + .name("test") + .isStatic(true) + .isClass(false) + .getAbstract("x") + .build(); + assertThat(val.toString(), + equalTo("test")); + assertThat(val.hashCode(), + equalTo(1)); + assertThat(val.equals(this), + equalTo(true)); + + assertThat(GeneralInterceptor.getInterceptCalls().size(), + equalTo(1)); + } + +} diff --git a/builder/tests/builder/src/test/java/io/helidon/builder/test/InterceptedBeanTest.java b/builder/tests/builder/src/test/java/io/helidon/builder/test/InterceptedBeanTest.java new file mode 100644 index 00000000000..7ae779c376a --- /dev/null +++ b/builder/tests/builder/src/test/java/io/helidon/builder/test/InterceptedBeanTest.java @@ -0,0 +1,43 @@ +/* + * 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.test; + +import io.helidon.builder.test.testsubjects.DefaultInterceptedBean; +import io.helidon.builder.test.testsubjects.InterceptedBean; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; + +class InterceptedBeanTest { + + @Test + void testMutation() { + InterceptedBean val = DefaultInterceptedBean.builder() + .name("Larry") + .build(); + assertThat(val.name(), equalTo("Larry")); + assertThat(val.helloMessage(), equalTo("Hello Larry")); + + InterceptedBean val2 = DefaultInterceptedBean.builder() + .name("Larry") + .build(); + assertThat(val, equalTo(val2)); + } + +} diff --git a/builder/tests/nodeps/pom.xml b/builder/tests/nodeps/pom.xml new file mode 100644 index 00000000000..20ca76bf744 --- /dev/null +++ b/builder/tests/nodeps/pom.xml @@ -0,0 +1,93 @@ + + + + + + io.helidon.builder.tests + helidon-builder-tests-project + 4.0.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + helidon-builder-test-nodeps + Helidon Builder No Dependencies Tests + A test that excludes all Helidon dependencies to ensure that builder works in such conditions + + + + io.helidon.builder + helidon-builder + ${helidon.version} + provided + + + io.helidon.builder + helidon-builder + + + io.helidon.common + helidon-common + + + + + io.helidon.builder + helidon-builder-processor + provided + + + org.hamcrest + hamcrest-all + test + + + org.junit.jupiter + junit-jupiter-api + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + -XprintProcessorInfo + -XprintRounds + -verbose + + true + + + io.helidon.builder + helidon-builder-processor + ${helidon.version} + + + + + + + + diff --git a/builder/tests/nodeps/src/main/java/io/helidon/builder/test/nodeps/NoDepsBeanBuilderInterceptor.java b/builder/tests/nodeps/src/main/java/io/helidon/builder/test/nodeps/NoDepsBeanBuilderInterceptor.java new file mode 100644 index 00000000000..b9f4cfffdbd --- /dev/null +++ b/builder/tests/nodeps/src/main/java/io/helidon/builder/test/nodeps/NoDepsBeanBuilderInterceptor.java @@ -0,0 +1,41 @@ +/* + * 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.test.nodeps; + +/** + * See {@link NoDepsInterceptedBean}. Notice how the Builder annotation on {@link NoDepsInterceptedBean} sets the + * {@link io.helidon.builder.Builder#requireLibraryDependencies()} attribute to {@code false}, which is why this class does not + * need to implement {@link io.helidon.builder.BuilderInterceptor}. + */ +class NoDepsBeanBuilderInterceptor /* implements Interceptor */ { + private int callCount; + + private NoDepsBeanBuilderInterceptor() { + } + + static NoDepsBeanBuilderInterceptor create() { + return new NoDepsBeanBuilderInterceptor(); + } + +// @Override + DefaultNoDepsInterceptedBean.Builder intercept(DefaultNoDepsInterceptedBean.Builder target) { + if (callCount++ > 0) { + throw new AssertionError(); + } + return target.helloMessage("Hello " + target.name()); + } +} diff --git a/builder/tests/nodeps/src/main/java/io/helidon/builder/test/nodeps/NoDepsInterceptedBean.java b/builder/tests/nodeps/src/main/java/io/helidon/builder/test/nodeps/NoDepsInterceptedBean.java new file mode 100644 index 00000000000..4ee82189d1e --- /dev/null +++ b/builder/tests/nodeps/src/main/java/io/helidon/builder/test/nodeps/NoDepsInterceptedBean.java @@ -0,0 +1,43 @@ +/* + * 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.test.nodeps; + +import io.helidon.builder.Builder; + +/** + * Demonstrates interception of builders. + */ +@Builder(requireLibraryDependencies = false, + interceptor = NoDepsBeanBuilderInterceptor.class, + interceptorCreateMethod = "create") +public interface NoDepsInterceptedBean { + + /** + * The name to say hello to. + * + * @return the name + */ + String name(); + + /** + * The hello message will be explicitly overridden to say "hello {@link #name()}". + * + * @return the hello message + */ + String helloMessage(); + +} diff --git a/builder/tests/nodeps/src/main/java/io/helidon/builder/test/nodeps/package-info.java b/builder/tests/nodeps/src/main/java/io/helidon/builder/test/nodeps/package-info.java new file mode 100644 index 00000000000..72424eef580 --- /dev/null +++ b/builder/tests/nodeps/src/main/java/io/helidon/builder/test/nodeps/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Test subjects for the Builder. + */ +package io.helidon.builder.test.nodeps; diff --git a/builder/tests/nodeps/src/test/java/io/helidon/builder/test/nodeps/NoDepsBuilderInterceptorBeanTest.java b/builder/tests/nodeps/src/test/java/io/helidon/builder/test/nodeps/NoDepsBuilderInterceptorBeanTest.java new file mode 100644 index 00000000000..01ac458cae8 --- /dev/null +++ b/builder/tests/nodeps/src/test/java/io/helidon/builder/test/nodeps/NoDepsBuilderInterceptorBeanTest.java @@ -0,0 +1,40 @@ +/* + * 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.test.nodeps; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; + +class NoDepsBuilderInterceptorBeanTest { + + @Test + void testMutation() { + NoDepsInterceptedBean val = DefaultNoDepsInterceptedBean.builder() + .name("Larry") + .build(); + assertThat(val.name(), equalTo("Larry")); + assertThat(val.helloMessage(), equalTo("Hello Larry")); + + NoDepsInterceptedBean val2 = DefaultNoDepsInterceptedBean.builder() + .name("Larry") + .build(); + assertThat(val, equalTo(val2)); + } + +} diff --git a/builder/tests/pom.xml b/builder/tests/pom.xml index ae271e397dc..bf2a7364288 100644 --- a/builder/tests/pom.xml +++ b/builder/tests/pom.xml @@ -39,7 +39,7 @@ true true - + true true true true @@ -47,6 +47,7 @@ builder + nodeps diff --git a/pico/builder-config/README.md b/pico/builder-config/README.md new file mode 100644 index 00000000000..eb5083914e0 --- /dev/null +++ b/pico/builder-config/README.md @@ -0,0 +1,23 @@ +# pico-builder-config + +This is a specialization of the [builder](../builder) that extends the builder to support additional integration with Helidon's configuration sub-system. It adds support for the [@ConfigBean](builder-config/src/main/java/io/helidon/pico/builder/config/ConfigBean.java) annotation. When applied to a target interface it will map that interface to configuration via a new toBuilder method generated on the implementation as follows: + +```java + ... + + public static Builder toBuilder(io.helidon.common.config.Config cfg) { + ... + } + + ... +``` + +There are a few additional caveats to understand about ConfigBean and its supporting infrastructure. + +* @Builder can be used in conjunction with @ConfigBean. All attributed will be honored exception one... +* Builder.requireLibraryDependencies is not supported. All generated configuration beans and builders will minimally require a compile-time and runtime dependency on Helidon's common-config module. But for full fidelity support of Helidon's config one should instead use the full config module. + +## Modules +* [builder-config](builder-config) - annotations and other SPI types. +* [processor](processor) - the annotation processor that should be used when using ConfigBeans. +* [tests](tests) - tests that can also serve as examples for usage. diff --git a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigBeanBase.java b/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigBeanBase.java index bfe60a7b04f..0014605078d 100644 --- a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigBeanBase.java +++ b/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/ConfigBeanBase.java @@ -31,6 +31,7 @@ public abstract class ConfigBeanBase implements ConfigBeanCommon { /** * Protected constructor for initializing the generated config bean instance variables. + * * @param b the builder * @param instanceId the instance id */ diff --git a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/DefaultConfigResolver.java b/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/DefaultConfigResolver.java index 6fbe11c79d2..f9cbaa80c1d 100644 --- a/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/DefaultConfigResolver.java +++ b/pico/builder-config/builder-config/src/main/java/io/helidon/pico/builder/config/spi/DefaultConfigResolver.java @@ -31,7 +31,7 @@ * The default implementation of {@link ConfigResolver} simply resolves against {@link io.helidon.common.config.Config} directly. */ @Singleton -@Weight(Weighted.DEFAULT_WEIGHT - 1) +@Weight(Weighted.DEFAULT_WEIGHT - 1) // allow all other creators to take precedence over us... public class DefaultConfigResolver implements ConfigResolver, ConfigResolverProvider { /**