diff --git a/bom/pom.xml b/bom/pom.xml index 5f05f10f071..aa97afec567 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -1394,7 +1394,7 @@ io.helidon.pico - helidon-pico-api + helidon-pico ${helidon.version} diff --git a/pico/api/pom.xml b/pico/api/pom.xml deleted file mode 100644 index 334daa33d0e..00000000000 --- a/pico/api/pom.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - io.helidon.pico - helidon-pico-project - 4.0.0-SNAPSHOT - ../pom.xml - - 4.0.0 - - helidon-pico-api - Helidon Pico API - - diff --git a/pico/api/src/main/java/io/helidon/pico/api/package-info.java b/pico/api/src/main/java/io/helidon/pico/api/package-info.java deleted file mode 100644 index 1da67056a63..00000000000 --- a/pico/api/src/main/java/io/helidon/pico/api/package-info.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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. - */ - -/** - * Types that are Pico API/consumer facing. The Pico API provide types that are generally useful at compile time - * to assign special meaning to the type. In this way it also helps with readability and intentions of the code itself. - * - */ -package io.helidon.pico.api; diff --git a/pico/builder/builder/README.md b/pico/builder/builder/README.md index 2f98cf90362..1d96528cbd4 100644 --- a/pico/builder/builder/README.md +++ b/pico/builder/builder/README.md @@ -2,4 +2,4 @@ This module can either be used compile-time only or at runtime as well depending upon your usage. -See the [main](../README.md) document for details. +See the [main](../README.md) for details. diff --git a/pico/builder/builder/src/main/java/io/helidon/pico/builder/spi/RequiredAttributeVisitor.java b/pico/builder/builder/src/main/java/io/helidon/pico/builder/spi/RequiredAttributeVisitor.java index 1b0740bf455..2a87b9958be 100644 --- a/pico/builder/builder/src/main/java/io/helidon/pico/builder/spi/RequiredAttributeVisitor.java +++ b/pico/builder/builder/src/main/java/io/helidon/pico/builder/spi/RequiredAttributeVisitor.java @@ -16,6 +16,7 @@ package io.helidon.pico.builder.spi; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Objects; @@ -38,7 +39,7 @@ */ @Deprecated public class RequiredAttributeVisitor implements AttributeVisitor { - private List errors; + private final List errors = new ArrayList<>(); /** * Default constructor. @@ -64,20 +65,17 @@ public void visit(String attrName, return; } - if (Objects.isNull(errors)) { - errors = new java.util.LinkedList<>(); - } errors.add("'" + attrName + "' is a required attribute and should not be null"); } /** * Performs the validation. Any errors will result in a thrown error. * - * @throws java.lang.AssertionError when any attributes are in violation with the validation policy + * @throws java.lang.IllegalStateException when any attributes are in violation with the validation policy */ public void validate() { - if (Objects.nonNull(errors) && !errors.isEmpty()) { - throw new AssertionError(String.join(", ", errors)); + if (!errors.isEmpty()) { + throw new IllegalStateException(String.join(", ", errors)); } } diff --git a/pico/builder/processor-spi/src/main/java/io/helidon/pico/builder/processor/spi/DefaultTypeInfo.java b/pico/builder/processor-spi/src/main/java/io/helidon/pico/builder/processor/spi/DefaultTypeInfo.java index 8b0fbf028fa..258e30a0f77 100644 --- a/pico/builder/processor-spi/src/main/java/io/helidon/pico/builder/processor/spi/DefaultTypeInfo.java +++ b/pico/builder/processor-spi/src/main/java/io/helidon/pico/builder/processor/spi/DefaultTypeInfo.java @@ -16,12 +16,11 @@ package io.helidon.pico.builder.processor.spi; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -48,13 +47,20 @@ public class DefaultTypeInfo implements TypeInfo { protected DefaultTypeInfo(Builder b) { this.typeName = b.typeName; this.typeKind = b.typeKind; - this.annotations = Objects.isNull(b.annotations) - ? Collections.emptyList() : Collections.unmodifiableList(new LinkedList<>(b.annotations)); - this.elementInfo = Objects.isNull(b.elementInfo) - ? Collections.emptyList() : Collections.unmodifiableList(new LinkedList<>(b.elementInfo)); + this.annotations = Collections.unmodifiableList(new LinkedList<>(b.annotations)); + this.elementInfo = Collections.unmodifiableList(new LinkedList<>(b.elementInfo)); this.superTypeInfo = b.superTypeInfo; } + /** + * Creates a new builder for this type. + * + * @return the fluent builder + */ + public static Builder builder() { + return new Builder(); + } + @Override public TypeName typeName() { return typeName; @@ -96,26 +102,16 @@ protected String toStringInner() { + ", superTypeInfo=" + superTypeInfo(); } - - /** - * Creates a new builder for this type. - * - * @return the fluent builder - */ - public static Builder builder() { - return new Builder(); - } - - /** * Builder for this type. */ - public static class Builder { + public static class Builder implements io.helidon.common.Builder { + private final List annotations = new ArrayList<>(); + private final List elementInfo = new ArrayList<>(); + private TypeName typeName; private String typeKind; - private List annotations; - private List elementInfo; - private Map defaultValueMap; + private TypeInfo superTypeInfo; /** @@ -124,6 +120,16 @@ public static class Builder { protected Builder() { } + /** + * Builds the instance. + * + * @return the built instance + */ + @Override + public DefaultTypeInfo build() { + return new DefaultTypeInfo(this); + } + /** * Sets the typeName to val. * @@ -153,7 +159,9 @@ public Builder typeKind(String val) { * @return this fluent builder */ public Builder annotations(Collection val) { - this.annotations = new LinkedList<>(Objects.requireNonNull(val)); + Objects.requireNonNull(val); + this.annotations.clear(); + this.annotations.addAll(val); return this; } @@ -164,9 +172,7 @@ public Builder annotations(Collection val) { * @return this fluent builder */ public Builder addAnnotation(AnnotationAndValue val) { - if (Objects.isNull(annotations)) { - annotations = new LinkedList<>(); - } + Objects.requireNonNull(val); annotations.add(Objects.requireNonNull(val)); return this; } @@ -178,7 +184,9 @@ public Builder addAnnotation(AnnotationAndValue val) { * @return this fluent builder */ public Builder elementInfo(Collection val) { - this.elementInfo = new LinkedList<>(Objects.requireNonNull(val)); + Objects.requireNonNull(val); + this.elementInfo.clear(); + this.elementInfo.addAll(val); return this; } @@ -189,39 +197,11 @@ public Builder elementInfo(Collection val) { * @return this fluent builder */ public Builder addElementInfo(TypedElementName val) { - if (Objects.isNull(elementInfo)) { - elementInfo = new LinkedList<>(); - } + Objects.requireNonNull(val); elementInfo.add(Objects.requireNonNull(val)); return this; } - /** - * Sets the defaultValueMap to val. - * - * @param val the value - * @return this fluent builder - */ - public Builder defaultValueMap(Map val) { - this.defaultValueMap = new LinkedHashMap<>(Objects.requireNonNull(val)); - return this; - } - - /** - * Adds a singular defaultValue val. - * - * @param key the key - * @param val the value - * @return this fluent builder - */ - public Builder addDefaultValue(TypedElementName key, String val) { - if (Objects.isNull(defaultValueMap)) { - defaultValueMap = new LinkedHashMap<>(); - } - defaultValueMap.put(key, val); - return this; - } - /** * Sets the superTypeInfo to val. * @@ -229,18 +209,10 @@ public Builder addDefaultValue(TypedElementName key, String val) { * @return this fluent builder */ public Builder superTypeInfo(TypeInfo val) { + Objects.requireNonNull(val); this.superTypeInfo = val; return this; } - - /** - * Builds the instance. - * - * @return the built instance - */ - public DefaultTypeInfo build() { - return new DefaultTypeInfo(this); - } } } diff --git a/pico/builder/processor-tools/src/main/java/io/helidon/pico/builder/processor/tools/BodyContext.java b/pico/builder/processor-tools/src/main/java/io/helidon/pico/builder/processor/tools/BodyContext.java new file mode 100644 index 00000000000..a5cd612e864 --- /dev/null +++ b/pico/builder/processor-tools/src/main/java/io/helidon/pico/builder/processor/tools/BodyContext.java @@ -0,0 +1,497 @@ +/* + * 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.pico.builder.processor.tools; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; + +import io.helidon.pico.builder.processor.spi.TypeInfo; +import io.helidon.pico.builder.spi.BeanUtils; +import io.helidon.pico.types.AnnotationAndValue; +import io.helidon.pico.types.DefaultAnnotationAndValue; +import io.helidon.pico.types.TypeName; +import io.helidon.pico.types.TypedElementName; + +import static io.helidon.pico.builder.processor.tools.DefaultBuilderCreator.BUILDER_ANNO_TYPE_NAME; +import static io.helidon.pico.builder.processor.tools.DefaultBuilderCreator.DEFAULT_INCLUDE_META_ATTRIBUTES; +import static io.helidon.pico.builder.processor.tools.DefaultBuilderCreator.DEFAULT_LIST_TYPE; +import static io.helidon.pico.builder.processor.tools.DefaultBuilderCreator.DEFAULT_MAP_TYPE; +import static io.helidon.pico.builder.processor.tools.DefaultBuilderCreator.DEFAULT_REQUIRE_LIBRARY_DEPENDENCIES; +import static io.helidon.pico.builder.processor.tools.DefaultBuilderCreator.DEFAULT_SET_TYPE; +import static io.helidon.pico.builder.processor.tools.DefaultBuilderCreator.SUPPORT_STREAMS_ON_BUILDER; +import static io.helidon.pico.builder.processor.tools.DefaultBuilderCreator.SUPPORT_STREAMS_ON_IMPL; + +/** + * Represents the context of the body being code generated. + */ +public class BodyContext { + private final boolean doingConcreteType; + private final TypeName implTypeName; + private final TypeInfo typeInfo; + private final AnnotationAndValue builderAnnotation; + private final Map map = new LinkedHashMap<>(); + private final List allTypeInfos = new ArrayList<>(); + private final List allAttributeNames = new ArrayList<>(); + private final AtomicReference parentTypeName = new AtomicReference<>(); + private final AtomicReference parentAnnotationType = new AtomicReference<>(); + private final boolean hasStreamSupportOnImpl; + private final boolean hasStreamSupportOnBuilder; + private final boolean includeMetaAttributes; + private final boolean requireLibraryDependencies; + private final boolean isBeanStyleRequired; + private final String listType; + private final String mapType; + private final String setType; + private final boolean hasParent; + private final TypeName ctorBuilderAcceptTypeName; + private final String genericBuilderClassDecl; + private final String genericBuilderAliasDecl; + private final String genericBuilderAcceptAliasDecl; + + /** + * Constructor. + * + * @param doingConcreteType true if the concrete type is being generated, otherwise the abstract class + * @param implTypeName the impl type name + * @param typeInfo the type info + * @param builderAnnotation the builder annotation + */ + BodyContext(boolean doingConcreteType, + TypeName implTypeName, + TypeInfo typeInfo, + AnnotationAndValue builderAnnotation) { + this.doingConcreteType = doingConcreteType; + this.implTypeName = implTypeName; + this.typeInfo = typeInfo; + this.builderAnnotation = builderAnnotation; + this.hasStreamSupportOnImpl = hasStreamSupportOnImpl(doingConcreteType, builderAnnotation); + this.hasStreamSupportOnBuilder = hasStreamSupportOnBuilder(doingConcreteType, builderAnnotation); + this.includeMetaAttributes = toIncludeMetaAttributes(builderAnnotation, typeInfo); + this.requireLibraryDependencies = toRequireLibraryDependencies(builderAnnotation, typeInfo); + this.isBeanStyleRequired = toRequireBeanStyle(builderAnnotation, typeInfo); + this.listType = toListImplType(builderAnnotation, typeInfo); + this.mapType = toMapImplType(builderAnnotation, typeInfo); + this.setType = toSetImplType(builderAnnotation, typeInfo); + gatherAllAttributeNames(this, typeInfo); + assert (allTypeInfos.size() == allAttributeNames.size()); + this.hasParent = Objects.nonNull(parentTypeName.get()); + this.ctorBuilderAcceptTypeName = (hasParent) + ? typeInfo.typeName() + : ( + Objects.nonNull(parentAnnotationType.get()) && typeInfo.elementInfo().isEmpty() + ? typeInfo.superTypeInfo().get().typeName() : typeInfo.typeName()); + this.genericBuilderClassDecl = "Builder"; + this.genericBuilderAliasDecl = ("B".equals(typeInfo.typeName().className())) ? "BU" : "B"; + this.genericBuilderAcceptAliasDecl = ("T".equals(typeInfo.typeName().className())) ? "TY" : "T"; + } + + /** + * Returns true if we are currently processing the concrete builder type. + * + * @return true if we are processing the concrete type + */ + protected boolean doingConcreteType() { + return doingConcreteType; + } + + /** + * Returns the impl type name. + * + * @return the type name + */ + protected TypeName implTypeName() { + return implTypeName; + } + + /** + * Returns the type info. + * + * @return the type info + */ + protected TypeInfo typeInfo() { + return typeInfo; + } + + /** + * Returns the builder annotation that triggers things. + * + * @return the builder annotation + */ + protected AnnotationAndValue builderAnnotation() { + return builderAnnotation; + } + + /** + * Returns the map of all type elements in the entire hierarchy. + * + * @return the map of elements by name + */ + protected Map map() { + return map; + } + + /** + * Returns the list of all type elements. + * + * @return the list of type elements + */ + protected List allTypeInfos() { + return allTypeInfos; + } + + /** + * Returns the list of all attributes names. + * + * @return the list of attribute names + */ + protected List allAttributeNames() { + return allAttributeNames; + } + + /** + * Returns the parent type name of the builder. + * + * @return the parent type name + */ + protected AtomicReference parentTypeName() { + return parentTypeName; + } + + /** + * Returns the parent annotation type. + * + * @return the parent annotation type + */ + protected AtomicReference parentAnnotationType() { + return parentAnnotationType; + } + + /** + * Returns true if there is stream support included on the generated class. + * + * @return true if stream support enabled + */ + protected boolean hasStreamSupportOnImpl() { + return hasStreamSupportOnImpl; + } + + /** + * Returns true if there is stream support included on the builder generated class. + * + * @return true if stream support enabled + */ + protected boolean hasStreamSupportOnBuilder() { + return hasStreamSupportOnBuilder; + } + + /** + * Returns true if meta attributes should be generated. + * + * @return true if meta attributes should be generated + */ + protected boolean includeMetaAttributes() { + return includeMetaAttributes; + } + + /** + * Returns true if Helidon library dependencies should be expected. + * + * @return true if Helidon library dependencies are expected + */ + protected boolean requireLibraryDependencies() { + return requireLibraryDependencies; + } + + /** + * Returns true if bean "getter" and "is" style is required. + * + * @return true if bean style is required + */ + protected boolean isBeanStyleRequired() { + return isBeanStyleRequired; + } + + /** + * Returns the list type generated. + * + * @return the list type + */ + protected String listType() { + return listType; + } + + /** + * Returns the map type generated. + * + * @return the map type + */ + protected String mapType() { + return mapType; + } + + /** + * Returns the set type generated. + * + * @return the set type + */ + protected String setType() { + return setType; + } + + /** + * Returns true if the current type has a parent. + * + * @return true if current has parent + */ + protected boolean hasParent() { + return hasParent; + } + + /** + * Returns the streamable accept type of the builder and constructor. + * + * @return the builder accept type + */ + protected TypeName ctorBuilderAcceptTypeName() { + return ctorBuilderAcceptTypeName; + } + + /** + * Returns the generic declaration for the builder class type. + * + * @return the generic declaration + */ + protected String genericBuilderClassDecl() { + return genericBuilderClassDecl; + } + + /** + * Returns the builder generics alias name for the type being built. + * + * @return the builder generics alias name + */ + protected String genericBuilderAliasDecl() { + return genericBuilderAliasDecl; + } + + /** + * Returns the builder generics alias name for the builder itself. + * + * @return the builder generics alias name + */ + protected String genericBuilderAcceptAliasDecl() { + return genericBuilderAcceptAliasDecl; + } + + /** + * returns the bean attribute name of a particular method. + * + * @param method the method + * @param isBeanStyleRequired is bean style required + * @return the bean attribute name + */ + protected static String toBeanAttributeName(TypedElementName method, + boolean isBeanStyleRequired) { + AtomicReference>> refAttrNames = new AtomicReference<>(); + BeanUtils.validateAndParseMethodName(method.elementName(), method.typeName().name(), isBeanStyleRequired, refAttrNames); + List attrNames = (refAttrNames.get().isEmpty()) ? Collections.emptyList() : refAttrNames.get().get(); + if (!isBeanStyleRequired) { + return (!attrNames.isEmpty()) ? attrNames.get(0) : method.elementName(); + } + return Objects.requireNonNull(attrNames.get(0)); + } + + private static boolean hasStreamSupportOnImpl(boolean ignoreDoingConcreteClass, + AnnotationAndValue ignoreBuilderAnnotation) { + return SUPPORT_STREAMS_ON_IMPL; + } + + private static boolean hasStreamSupportOnBuilder(boolean ignoreDoingConcreteClass, + AnnotationAndValue ignoreBuilderAnnotation) { + return SUPPORT_STREAMS_ON_BUILDER; + } + + /** + * In support of {@link io.helidon.pico.builder.Builder#includeMetaAttributes()}. + */ + private static boolean toIncludeMetaAttributes(AnnotationAndValue builderAnnotation, + TypeInfo typeInfo) { + String val = searchForBuilderAnnotation("includeMetaAttributes", builderAnnotation, typeInfo); + return val == null ? DEFAULT_INCLUDE_META_ATTRIBUTES : Boolean.parseBoolean(val); + } + + /** + * In support of {@link io.helidon.pico.builder.Builder#requireLibraryDependencies()}. + */ + private static boolean toRequireLibraryDependencies(AnnotationAndValue builderAnnotation, + TypeInfo typeInfo) { + String val = searchForBuilderAnnotation("requireLibraryDependencies", builderAnnotation, typeInfo); + return val == null ? DEFAULT_REQUIRE_LIBRARY_DEPENDENCIES : Boolean.parseBoolean(val); + } + + /** + * In support of {@link io.helidon.pico.builder.Builder#requireBeanStyle()}. + */ + private static boolean toRequireBeanStyle(AnnotationAndValue builderAnnotation, + TypeInfo typeInfo) { + String val = searchForBuilderAnnotation("requireBeanStyle", builderAnnotation, typeInfo); + return Boolean.parseBoolean(val); + } + + /** + * In support of {@link io.helidon.pico.builder.Builder#listImplType()}. + */ + private static String toListImplType(AnnotationAndValue builderAnnotation, + TypeInfo typeInfo) { + String type = searchForBuilderAnnotation("listImplType", builderAnnotation, typeInfo); + return (!BuilderTypeTools.hasNonBlankValue(type)) ? DEFAULT_LIST_TYPE : type; + } + + /** + * In support of {@link io.helidon.pico.builder.Builder#mapImplType()} ()}. + */ + private static String toMapImplType(AnnotationAndValue builderAnnotation, + TypeInfo typeInfo) { + String type = searchForBuilderAnnotation("mapImplType", builderAnnotation, typeInfo); + return (!BuilderTypeTools.hasNonBlankValue(type)) ? DEFAULT_MAP_TYPE : type; + } + + /** + * In support of {@link io.helidon.pico.builder.Builder#setImplType()}. + */ + private static String toSetImplType(AnnotationAndValue builderAnnotation, + TypeInfo typeInfo) { + String type = searchForBuilderAnnotation("setImplType", builderAnnotation, typeInfo); + return (!BuilderTypeTools.hasNonBlankValue(type)) ? DEFAULT_SET_TYPE : type; + } + + private static String searchForBuilderAnnotation(String key, + AnnotationAndValue builderAnnotation, + TypeInfo typeInfo) { + String val = builderAnnotation.value(key).orElse(null); + if (val != null) { + return val; + } + + if (!builderAnnotation.typeName().equals(BUILDER_ANNO_TYPE_NAME)) { + builderAnnotation = DefaultAnnotationAndValue + .findFirst(BUILDER_ANNO_TYPE_NAME.name(), typeInfo.annotations()).orElse(null); + if (Objects.nonNull(builderAnnotation)) { + val = builderAnnotation.value(key).orElse(null); + } + } + + return val; + } + + private static void gatherAllAttributeNames(BodyContext ctx, + TypeInfo typeInfo) { + TypeInfo superTypeInfo = typeInfo.superTypeInfo().orElse(null); + if (Objects.nonNull(superTypeInfo)) { + Optional superBuilderAnnotation = DefaultAnnotationAndValue + .findFirst(ctx.builderAnnotation.typeName().name(), superTypeInfo.annotations()); + if (superBuilderAnnotation.isEmpty()) { + gatherAllAttributeNames(ctx, superTypeInfo); + } else { + populateMap(ctx.map, superTypeInfo, ctx.isBeanStyleRequired); + } + + if (Objects.isNull(ctx.parentTypeName.get()) + && superTypeInfo.typeKind().equals("INTERFACE")) { + ctx.parentTypeName.set(superTypeInfo.typeName()); + } else if (Objects.isNull(ctx.parentAnnotationType.get()) + && superTypeInfo.typeKind().equals("ANNOTATION_TYPE")) { + ctx.parentAnnotationType.set(superTypeInfo.typeName()); + } + } + + for (TypedElementName method : typeInfo.elementInfo()) { + String beanAttributeName = toBeanAttributeName(method, ctx.isBeanStyleRequired); + TypedElementName existing = ctx.map.get(beanAttributeName); + if (Objects.nonNull(existing) + && BeanUtils.isBooleanType(method.typeName().name()) + && method.elementName().startsWith("is")) { + AtomicReference>> alternateNames = new AtomicReference<>(); + BeanUtils.validateAndParseMethodName(method.elementName(), + method.typeName().name(), true, alternateNames); + assert (Objects.nonNull(alternateNames.get())); + final String currentAttrName = beanAttributeName; + Optional alternateName = alternateNames.get().orElse(Collections.emptyList()).stream() + .filter(it -> !it.equals(currentAttrName)) + .findFirst(); + if (alternateName.isPresent() && !ctx.map.containsKey(alternateName.get())) { + beanAttributeName = alternateName.get(); + existing = ctx.map.get(beanAttributeName); + } + } + + if (Objects.nonNull(existing)) { + if (!existing.typeName().equals(method.typeName())) { + throw new IllegalStateException(method + " cannot redefine types from super for " + beanAttributeName); + } + + // allow the subclass to override the defaults, etc. + Objects.requireNonNull(ctx.map.put(beanAttributeName, method)); + int pos = ctx.allAttributeNames.indexOf(beanAttributeName); + if (pos >= 0) { + ctx.allTypeInfos.set(pos, method); + } + continue; + } + + Object prev = ctx.map.put(beanAttributeName, method); + assert (Objects.isNull(prev)); + + ctx.allTypeInfos.add(method); + if (ctx.allAttributeNames.contains(beanAttributeName)) { + throw new IllegalStateException("duplicate attribute name: " + beanAttributeName + " processing " + typeInfo); + } + ctx.allAttributeNames.add(beanAttributeName); + } + } + + private static void populateMap(Map map, + TypeInfo typeInfo, + boolean isBeanStyleRequired) { + if (typeInfo.superTypeInfo().isPresent()) { + populateMap(map, typeInfo.superTypeInfo().get(), isBeanStyleRequired); + } + + for (TypedElementName method : typeInfo.elementInfo()) { + String beanAttributeName = toBeanAttributeName(method, isBeanStyleRequired); + TypedElementName existing = map.get(beanAttributeName); + if (Objects.nonNull(existing)) { + if (!existing.typeName().equals(method.typeName())) { + throw new IllegalStateException(method + " cannot redefine types from super for " + beanAttributeName); + } + + // allow the subclass to override the defaults, etc. + Objects.requireNonNull(map.put(beanAttributeName, method)); + } else { + Object prev = map.put(beanAttributeName, method); + assert (Objects.isNull(prev)); + } + } + } + +} diff --git a/pico/builder/processor-tools/src/main/java/io/helidon/pico/builder/processor/tools/BuilderTypeTools.java b/pico/builder/processor-tools/src/main/java/io/helidon/pico/builder/processor/tools/BuilderTypeTools.java index d61817bd93e..9b28b8ebcb1 100644 --- a/pico/builder/processor-tools/src/main/java/io/helidon/pico/builder/processor/tools/BuilderTypeTools.java +++ b/pico/builder/processor-tools/src/main/java/io/helidon/pico/builder/processor/tools/BuilderTypeTools.java @@ -17,6 +17,7 @@ package io.helidon.pico.builder.processor.tools; import java.lang.annotation.Annotation; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; @@ -61,9 +62,6 @@ */ @Weight(Weighted.DEFAULT_WEIGHT - 1) public class BuilderTypeTools implements TypeInfoCreator { - - private final System.Logger logger = System.getLogger(getClass().getName()); - /** * Default constructor. */ @@ -109,7 +107,7 @@ public Optional createTypeInfo(AnnotationAndValue annotation, .typeKind(String.valueOf(element.getKind())) .annotations(BuilderTypeTools.createAnnotationAndValueListFromElement(element, processingEnv.getElementUtils())) .elementInfo(elementInfo) - .superTypeInfo(toTypeInfo(annotation, element, processingEnv).orElse(null)) + .update(it -> toTypeInfo(annotation, element, processingEnv).ifPresent(it::superTypeInfo)) .build()); } @@ -191,15 +189,19 @@ public static Optional createTypeNameFromElement(Element type) return createTypeNameFromMirror(((ExecutableElement) type).getReturnType()); } - String className = type.getSimpleName().toString(); + List classNames = new ArrayList<>(); + classNames.add(type.getSimpleName().toString()); while (Objects.nonNull(type.getEnclosingElement()) && ElementKind.PACKAGE != type.getEnclosingElement().getKind()) { - className = type.getEnclosingElement().getSimpleName() + "." + className; + classNames.add(type.getEnclosingElement().getSimpleName().toString()); type = type.getEnclosingElement(); } - return Optional.of(Objects.isNull(type.getEnclosingElement()) - ? DefaultTypeName.create(type.toString(), className) - : DefaultTypeName.create(type.getEnclosingElement().toString(), className)); + Collections.reverse(classNames); + String className = String.join(".", classNames); + + Element packageName = type.getEnclosingElement() == null ? type : type.getEnclosingElement(); + + return Optional.of(DefaultTypeName.create(packageName.toString(), className)); } /** @@ -238,7 +240,7 @@ public static Optional createTypeNameFromMirror(TypeMirror type type = double.class; break; default: - throw new AssertionError("unknown primitive type: " + kind); + throw new IllegalStateException("unknown primitive type: " + kind); } return Optional.of(DefaultTypeName.create(type)); @@ -261,20 +263,21 @@ public static Optional createTypeNameFromMirror(TypeMirror type if (typeMirror instanceof DeclaredType) { DeclaredType declaredType = (DeclaredType) typeMirror; - DefaultTypeName result = createTypeNameFromElement(declaredType.asElement()).orElse(null); - List typeParams = declaredType.getTypeArguments().stream() + List typeParams = declaredType.getTypeArguments() + .stream() .map(BuilderTypeTools::createTypeNameFromMirror) - .filter(Optional::isPresent) - .map(Optional::orElseThrow) - .filter(Objects::nonNull) + .flatMap(Optional::stream) .collect(Collectors.toList()); - if (!typeParams.isEmpty()) { - result = result.toBuilder().typeArguments(typeParams).build(); + + DefaultTypeName result = createTypeNameFromElement(declaredType.asElement()).orElse(null); + if (typeParams.isEmpty() || result == null) { + return Optional.ofNullable(result); } - return Optional.of(result); + + return Optional.of(result.toBuilder().typeArguments(typeParams).build()); } - throw new AssertionError("unknown type mirror: " + typeMirror); + throw new IllegalStateException("Unknown type mirror: " + typeMirror); } /** @@ -301,10 +304,8 @@ public static Optional findAnnotationMirror(String a public static Optional createAnnotationAndValueFromMirror(AnnotationMirror am, Elements elements) { Optional val = createTypeNameFromMirror(am.getAnnotationType()); - if (val.isEmpty()) { - return Optional.empty(); - } - return Optional.ofNullable(DefaultAnnotationAndValue.create(val.get(), extractValues(am, Optional.of(elements)))); + + return val.map(it -> DefaultAnnotationAndValue.create(it, extractValues(am, elements))); } /** @@ -330,12 +331,8 @@ public static List createAnnotationAndValueListFromElement(E * @return the extracted values */ public static Map extractValues(AnnotationMirror am, - Optional elements) { - if (elements.isPresent()) { - return extractValues(elements.get().getElementValuesWithDefaults(am)); - } - - return extractValues(am.getElementValues()); + Elements elements) { + return extractValues(elements.getElementValuesWithDefaults(am)); } /** @@ -384,14 +381,16 @@ public static TypedElementName createTypedElementNameFromElement(Element v, createAnnotationAndValueListFromElement(((DeclaredType) returnType).asElement(), elements); } AnnotationValue annotationValue = ee.getDefaultValue(); - defaultValue = Objects.isNull(annotationValue) - ? null : annotationValue.accept(new ToStringAnnotationValueVisitor() + defaultValue = annotationValue == null + ? null + : annotationValue.accept(new ToStringAnnotationValueVisitor() .mapBooleanToNull(true) .mapVoidToNull(true) .mapBlankArrayToNull(true) .mapEmptyStringToNull(true) .mapToSourceDeclaration(true), null); } + componentTypeNames = componentTypeNames == null ? List.of() : componentTypeNames; return DefaultTypedElementName.builder() .typeName(type) diff --git a/pico/builder/processor-tools/src/main/java/io/helidon/pico/builder/processor/tools/DefaultBuilderCreator.java b/pico/builder/processor-tools/src/main/java/io/helidon/pico/builder/processor/tools/DefaultBuilderCreator.java index 54d26bed6bc..7483543ffa2 100644 --- a/pico/builder/processor-tools/src/main/java/io/helidon/pico/builder/processor/tools/DefaultBuilderCreator.java +++ b/pico/builder/processor-tools/src/main/java/io/helidon/pico/builder/processor/tools/DefaultBuilderCreator.java @@ -20,7 +20,6 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -28,7 +27,6 @@ import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; import java.util.stream.Collectors; @@ -51,22 +49,24 @@ import io.helidon.pico.types.TypeName; import io.helidon.pico.types.TypedElementName; +import static io.helidon.pico.builder.processor.tools.BodyContext.toBeanAttributeName; + /** * Default implementation for {@link io.helidon.pico.builder.processor.spi.BuilderCreator}. */ @Weight(Weighted.DEFAULT_WEIGHT - 1) // allow all other creators to take precedence over us... public class DefaultBuilderCreator implements BuilderCreator { - private static final boolean DEFAULT_INCLUDE_META_ATTRIBUTES = true; - private static final boolean DEFAULT_REQUIRE_LIBRARY_DEPENDENCIES = true; - private static final String DEFAULT_IMPL_PREFIX = Builder.DEFAULT_IMPL_PREFIX; - private static final String DEFAULT_ABSTRACT_IMPL_PREFIX = Builder.DEFAULT_ABSTRACT_IMPL_PREFIX; - private static final String DEFAULT_SUFFIX = Builder.DEFAULT_SUFFIX; - private static final String DEFAULT_LIST_TYPE = Builder.DEFAULT_LIST_TYPE.getName(); - private static final String DEFAULT_MAP_TYPE = Builder.DEFAULT_MAP_TYPE.getName(); - private static final String DEFAULT_SET_TYPE = Builder.DEFAULT_SET_TYPE.getName(); - private static final TypeName BUILDER_ANNO_TYPE_NAME = DefaultTypeName.create(Builder.class); - private static final boolean SUPPORT_STREAMS_ON_IMPL = false; - private static final boolean SUPPORT_STREAMS_ON_BUILDER = true; + static final boolean DEFAULT_INCLUDE_META_ATTRIBUTES = true; + static final boolean DEFAULT_REQUIRE_LIBRARY_DEPENDENCIES = true; + static final String DEFAULT_IMPL_PREFIX = Builder.DEFAULT_IMPL_PREFIX; + static final String DEFAULT_ABSTRACT_IMPL_PREFIX = Builder.DEFAULT_ABSTRACT_IMPL_PREFIX; + static final String DEFAULT_SUFFIX = Builder.DEFAULT_SUFFIX; + static final String DEFAULT_LIST_TYPE = Builder.DEFAULT_LIST_TYPE.getName(); + static final String DEFAULT_MAP_TYPE = Builder.DEFAULT_MAP_TYPE.getName(); + static final String DEFAULT_SET_TYPE = Builder.DEFAULT_SET_TYPE.getName(); + static final TypeName BUILDER_ANNO_TYPE_NAME = DefaultTypeName.create(Builder.class); + static final boolean SUPPORT_STREAMS_ON_IMPL = false; + static final boolean SUPPORT_STREAMS_ON_BUILDER = true; /** * Default constructor. @@ -133,125 +133,6 @@ protected List postValidate(List builds) { return builds; } - /** - * In support of {@link io.helidon.pico.builder.Builder#packageName()}. - */ - private String toPackageName(String packageName, - AnnotationAndValue builderAnnotation) { - String packageNameFromAnno = builderAnnotation.value("packageName").orElse(null); - if (Objects.isNull(packageNameFromAnno) || packageNameFromAnno.isBlank()) { - return packageName; - } else if (packageNameFromAnno.startsWith(".")) { - return packageName + packageNameFromAnno; - } else { - return packageNameFromAnno; - } - } - - /** - * In support of {@link io.helidon.pico.builder.Builder#abstractImplPrefix()}. - */ - private String toAbstractImplTypePrefix(AnnotationAndValue builderAnnotation) { - return builderAnnotation.value("abstractImplPrefix").orElse(DEFAULT_ABSTRACT_IMPL_PREFIX); - } - - /** - * In support of {@link io.helidon.pico.builder.Builder#implPrefix()}. - */ - private String toImplTypePrefix(AnnotationAndValue builderAnnotation) { - return builderAnnotation.value("implPrefix").orElse(DEFAULT_IMPL_PREFIX); - } - - /** - * In support of {@link io.helidon.pico.builder.Builder#implSuffix()}. - */ - private String toImplTypeSuffix(AnnotationAndValue builderAnnotation) { - return builderAnnotation.value("implSuffix").orElse(DEFAULT_SUFFIX); - } - - /** - * In support of {@link io.helidon.pico.builder.Builder#includeMetaAttributes()}. - */ - private static boolean toIncludeMetaAttributes(AnnotationAndValue builderAnnotation, - TypeInfo typeInfo) { - String val = searchForBuilderAnnotation("includeMetaAttributes", builderAnnotation, typeInfo); - return Objects.isNull(val) ? DEFAULT_INCLUDE_META_ATTRIBUTES : Boolean.parseBoolean(val); - } - - /** - * In support of {@link io.helidon.pico.builder.Builder#requireLibraryDependencies()}. - */ - private static boolean toRequireLibraryDependencies(AnnotationAndValue builderAnnotation, - TypeInfo typeInfo) { - String val = searchForBuilderAnnotation("requireLibraryDependencies", builderAnnotation, typeInfo); - return Objects.isNull(val) ? DEFAULT_REQUIRE_LIBRARY_DEPENDENCIES : Boolean.parseBoolean(val); - } - - /** - * In support of {@link io.helidon.pico.builder.Builder#requireBeanStyle()}. - */ - private static boolean toRequireBeanStyle(AnnotationAndValue builderAnnotation, - TypeInfo typeInfo) { - String val = searchForBuilderAnnotation("requireBeanStyle", builderAnnotation, typeInfo); - return Boolean.parseBoolean(val); - } - - /** - * In support of {@link io.helidon.pico.builder.Builder#listImplType()}. - */ - private static String toListImplType(AnnotationAndValue builderAnnotation, - TypeInfo typeInfo) { - String type = searchForBuilderAnnotation("listImplType", builderAnnotation, typeInfo); - return (!BuilderTypeTools.hasNonBlankValue(type)) ? DEFAULT_LIST_TYPE : type; - } - - /** - * In support of {@link io.helidon.pico.builder.Builder#mapImplType()} ()}. - */ - private static String toMapImplType(AnnotationAndValue builderAnnotation, - TypeInfo typeInfo) { - String type = searchForBuilderAnnotation("mapImplType", builderAnnotation, typeInfo); - return (!BuilderTypeTools.hasNonBlankValue(type)) ? DEFAULT_MAP_TYPE : type; - } - - /** - * In support of {@link io.helidon.pico.builder.Builder#setImplType()}. - */ - private static String toSetImplType(AnnotationAndValue builderAnnotation, - TypeInfo typeInfo) { - String type = searchForBuilderAnnotation("setImplType", builderAnnotation, typeInfo); - return (!BuilderTypeTools.hasNonBlankValue(type)) ? DEFAULT_SET_TYPE : type; - } - - private static boolean hasStreamSupportOnImpl(boolean ignoreDoingConcreteClass, - AnnotationAndValue ignoreBuilderAnnotation) { - return SUPPORT_STREAMS_ON_IMPL; - } - - private static boolean hasStreamSupportOnBuilder(boolean ignoreDoingConcreteClass, - AnnotationAndValue ignoreBuilderAnnotation) { - return SUPPORT_STREAMS_ON_BUILDER; - } - - private static String searchForBuilderAnnotation(String key, - AnnotationAndValue builderAnnotation, - TypeInfo typeInfo) { - String val = builderAnnotation.value(key).orElse(null); - if (Objects.nonNull(val)) { - return val; - } - - if (!builderAnnotation.typeName().equals(BUILDER_ANNO_TYPE_NAME)) { - builderAnnotation = DefaultAnnotationAndValue - .findFirst(BUILDER_ANNO_TYPE_NAME, typeInfo.annotations(), false).orElse(null); - if (Objects.nonNull(builderAnnotation)) { - val = builderAnnotation.value(key).orElse(null); - } - } - - return val; - } - /** * Constructs the abstract implementation type name for what is code generated. * @@ -282,70 +163,6 @@ protected Optional toImplTypeName(TypeName typeName, return Optional.of(DefaultTypeName.create(toPackageName, prefix + typeName.className() + suffix)); } - /** - * Represents the context of the body being code generated. - */ - protected static class BodyContext { - private final boolean doingConcreteType; - private final TypeName implTypeName; - private final TypeInfo typeInfo; - private final AnnotationAndValue builderAnnotation; - private final Map map = new LinkedHashMap<>(); - private final List allTypeInfos = new ArrayList<>(); - private final List allAttributeNames = new ArrayList<>(); - private final AtomicReference parentTypeName = new AtomicReference<>(); - private final AtomicReference parentAnnotationType = new AtomicReference<>(); - private final boolean hasStreamSupportOnImpl; - private final boolean hasStreamSupportOnBuilder; - private final boolean includeMetaAttributes; - private final boolean requireLibraryDependencies; - private final boolean isBeanStyleRequired; - private final String listType; - private final String mapType; - private final String setType; - private final boolean hasParent; - private final TypeName ctorBuilderAcceptTypeName; - private final String genericBuilderClassDecl; - private final String genericBuilderAliasDecl; - private final String genericBuilderAcceptAliasDecl; - - /** - * Constructor. - * - * @param doingConcreteType true if the concrete type is being generated, otherwise the abstract class - * @param implTypeName the impl type name - * @param typeInfo the type info - * @param builderAnnotation the builder annotation - */ - protected BodyContext(boolean doingConcreteType, - TypeName implTypeName, - TypeInfo typeInfo, - AnnotationAndValue builderAnnotation) { - this.doingConcreteType = doingConcreteType; - this.implTypeName = implTypeName; - this.typeInfo = typeInfo; - this.builderAnnotation = builderAnnotation; - this.hasStreamSupportOnImpl = hasStreamSupportOnImpl(doingConcreteType, builderAnnotation); - this.hasStreamSupportOnBuilder = hasStreamSupportOnBuilder(doingConcreteType, builderAnnotation); - this.includeMetaAttributes = toIncludeMetaAttributes(builderAnnotation, typeInfo); - this.requireLibraryDependencies = toRequireLibraryDependencies(builderAnnotation, typeInfo); - this.isBeanStyleRequired = toRequireBeanStyle(builderAnnotation, typeInfo); - this.listType = toListImplType(builderAnnotation, typeInfo); - this.mapType = toMapImplType(builderAnnotation, typeInfo); - this.setType = toSetImplType(builderAnnotation, typeInfo); - gatherAllAttributeNames(this, typeInfo); - assert (allTypeInfos.size() == allAttributeNames.size()); - this.hasParent = Objects.nonNull(parentTypeName.get()); - this.ctorBuilderAcceptTypeName = (hasParent) - ? typeInfo.typeName() - : (Objects.nonNull(parentAnnotationType.get()) && typeInfo.elementInfo().isEmpty() - ? typeInfo.superTypeInfo().get().typeName() : typeInfo.typeName()); - this.genericBuilderClassDecl = "Builder"; - this.genericBuilderAliasDecl = ("B".equals(typeInfo.typeName().className())) ? "BU" : "B"; - this.genericBuilderAcceptAliasDecl = ("T".equals(typeInfo.typeName().className())) ? "TY" : "T"; - } - } - /** * Creates the context for the class being built. * @@ -400,602 +217,169 @@ protected void appendFooter(StringBuilder builder, builder.append("}\n"); } - private void appendBuilder(StringBuilder builder, - BodyContext ctx) { - appendBuilderHeader(builder, ctx); - appendExtraBuilderFields(builder, ctx.genericBuilderClassDecl, ctx.builderAnnotation, - ctx.typeInfo, ctx.parentTypeName.get(), ctx.allAttributeNames, ctx.allTypeInfos); - appendBuilderBody(builder, ctx); - - appendExtraBuilderMethods(builder, ctx); + /** + * Appends the simple {@link io.helidon.config.metadata.ConfiguredOption#required()} validation inside the build() method. + * + * @param builder the builder + * @param ctx the context + */ + protected void appendRequiredValidator(StringBuilder builder, + BodyContext ctx) { + if (ctx.includeMetaAttributes()) { + builder.append("\t\t\tRequiredAttributeVisitor visitor = new RequiredAttributeVisitor();\n" + + "\t\t\tvisitAttributes(visitor, null);\n" + + "\t\t\tvisitor.validate();\n"); + } + } - if (ctx.doingConcreteType) { - if (ctx.hasParent) { - builder.append("\t\t@Override\n"); - } else { - builder.append("\t\t/**\n" - + "\t\t * Builds the instance.\n" - + "\t\t *\n" - + "\t\t * @return the built instance\n" - + "\t\t * @throws java.lang.AssertionError if any required attributes are missing\n" - + "\t\t */\n"); - } - 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}\n"); - } else { - int i = 0; - for (String beanAttributeName : ctx.allAttributeNames) { - TypedElementName method = ctx.allTypeInfos.get(i); - boolean isList = isList(method); - boolean isMap = !isList && isMap(method); - boolean isSet = !isMap && isSet(method); - boolean ignoredUpLevel = isSet || isList; - appendSetter(builder, beanAttributeName, Optional.empty(), method, ctx); - if (isList || isMap || isSet) { - // NOP - } else { - boolean isBoolean = BeanUtils.isBooleanType(method.typeName().name()); - if (isBoolean && beanAttributeName.startsWith("is")) { - // possibly overload setter to strip the "is"... - String basicAttributeName = "" - + Character.toLowerCase(beanAttributeName.charAt(2)) - + beanAttributeName.substring(3); - if (!ctx.allAttributeNames.contains(basicAttributeName)) { - appendSetter(builder, beanAttributeName, Optional.of(basicAttributeName), method, ctx); - } - } - } + /** + * Adds the basic getters to the generated builder output. + * + * @param builder the builder + * @param ctx the context + */ + protected void appendBasicGetters(StringBuilder builder, + BodyContext ctx) { + if (ctx.doingConcreteType()) { + return; + } - maybeAppendSingularSetter(builder, method, beanAttributeName, isList, isMap, isSet, ctx); - i++; - } + 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.requireLibraryDependencies) { - builder.append("\t\t/**\n" - + "\t\t * Build the instance from this builder.\n" - + "\t\t *\n" - + "\t\t * @return instance of the built type\n" - + "\t\t */\n" - + "\t\tpublic abstract ").append(ctx.genericBuilderAcceptAliasDecl) - .append(" build();\n\n"); + if (!ctx.hasParent() && ctx.hasStreamSupportOnImpl()) { + builder.append("\t@Override\n" + + "\tpublic T get() {\n" + + "\t\treturn (T) this;\n" + + "\t}\n\n"); + } + } - if (ctx.hasStreamSupportOnBuilder) { - builder.append("\t\t/**\n" - + "\t\t * Update the builder in a fluent API way.\n" - + "\t\t *\n" - + "\t\t * @param consumer consumer of the builder instance\n" - + "\t\t * @return updated builder instance\n" - + "\t\t */\n"); - builder.append("\t\tpublic B update(Consumer<") - .append(ctx.genericBuilderAcceptAliasDecl) - .append("> consumer) {\n" - + "\t\t\tconsumer.accept(get());\n" - + "\t\t\treturn identity();\n" - + "\t\t}\n\n"); - } + /** + * Appends meta attribute related methods. + * + * @param builder the builder + * @param ctx the context + */ + protected void appendMetaAttributes(StringBuilder builder, + BodyContext ctx) { + if (!ctx.doingConcreteType() && ctx.includeMetaAttributes()) { + builder.append("\tprivate static Map> __calcMeta() {\n"); + builder.append("\t\tMap> metaProps = new java.util.LinkedHashMap<>();\n"); - if (!ctx.requireLibraryDependencies) { - builder.append("\t\t/**\n" - + "\t\t * Instance of this builder as the correct type.\n" - + "\t\t *\n" - + "\t\t * @return this instance typed to correct type\n" - + "\t\t */\n"); - builder.append("\t\t@SuppressWarnings(\"unchecked\")\n"); - builder.append("\t\tprotected ").append(ctx.genericBuilderAliasDecl).append(" identity() {\n" - + "\t\t\treturn (") - .append(ctx.genericBuilderAliasDecl).append(") this;\n" - + "\t\t}\n\n" - + "\t\t@Override\n" - + "\t\tpublic ") - .append(ctx.genericBuilderAcceptAliasDecl).append(" get() {\n" - + "\t\t\treturn (") - .append(ctx.genericBuilderAcceptAliasDecl).append(") build();\n" - + "\t\t}\n\n"); - } - } + AtomicBoolean needsCustomMapOf = new AtomicBoolean(); + appendMetaProps(builder, "metaProps", + ctx.typeInfo(), ctx.map(), ctx.allAttributeNames(), ctx.allTypeInfos(), needsCustomMapOf); + builder.append("\t\treturn metaProps;\n"); + builder.append("\t}\n\n"); - if (ctx.hasStreamSupportOnBuilder || ctx.requireLibraryDependencies) { - builder.append("\t\t@Override\n"); - } - builder.append("\t\tpublic void accept(").append(ctx.genericBuilderAcceptAliasDecl).append(" val) {\n"); - if (ctx.hasParent) { - builder.append("\t\t\tsuper.accept(val);\n"); + if (needsCustomMapOf.get()) { + appendCustomMapOf(builder); } - builder.append("\t\t\tacceptThis(val);\n"); - builder.append("\t\t}\n\n"); - builder.append("\t\tprivate void acceptThis(").append(ctx.genericBuilderAcceptAliasDecl).append(" val) {\n"); - builder.append("\t\t\tif (Objects.isNull(val)) {\n" - + "\t\t\t\treturn;\n" - + "\t\t\t}\n"); - i = 0; - for (String beanAttributeName : ctx.allAttributeNames) { - TypedElementName method = ctx.allTypeInfos.get(i++); - String getterName = method.elementName(); - builder.append("\t\t\t").append(beanAttributeName).append("("); - boolean isList = isList(method); - boolean isMap = !isList && isMap(method); - boolean isSet = !isMap && isSet(method); - if (isList || isSet) { - builder.append("(java.util.Collection) "); - } - builder.append("val.").append(getterName).append("());\n"); - } - builder.append("\t\t}\n"); + GenerateMethod.internalMetaAttributes(builder); } - - // end of the generated builder inner class here - builder.append("\t}\n"); } - private void appendBuilderBody(StringBuilder builder, BodyContext ctx) { - if (!ctx.doingConcreteType) { - int i = 0; - for (String beanAttributeName : ctx.allAttributeNames) { - TypedElementName method = ctx.allTypeInfos.get(i); - TypeName type = method.typeName(); - builder.append("\t\t/**\n" - + "\t\t * field value for {@code " + method + "()}.\n" - + "\t\t */\n"); - builder.append("\t\tprotected ").append(type.array() ? type.fqName() : type.name()).append(" ") - .append(beanAttributeName); - Optional defaultVal = toConfiguredOptionValue(method, true, true); - if (defaultVal.isPresent()) { - builder.append(" = "); - appendDefaultValueAssignment(builder, method, defaultVal.get()); - } - builder.append(";\n"); - i++; - } - builder.append("\n"); + /** + * Adds the fields part of the generated builder. + * + * @param builder the builder + * @param ctx the context + */ + protected void appendFields(StringBuilder builder, + BodyContext ctx) { + if (ctx.doingConcreteType()) { + return; } - builder.append("\t\t/**\n" - + "\t\t * The fluent builder constructor.\n" - + "\t\t *\n" - + "\t\t * @param val the value to copy to initialize the builder attributes\n" - + "\t\t */\n"); - if (ctx.doingConcreteType) { - builder.append("\t\tprotected ").append(ctx.genericBuilderClassDecl).append("("); - builder.append(ctx.ctorBuilderAcceptTypeName).append(" val) {\n"); - builder.append("\t\t\tsuper(val);\n"); - } else { - builder.append("\t\tprotected ").append(ctx.genericBuilderClassDecl).append("(") - .append(ctx.genericBuilderAcceptAliasDecl).append(" val) {\n"); - if (ctx.hasParent) { - builder.append("\t\t\tsuper(val);\n"); - } - appendOverridesOfDefaultValues(builder, ctx); - builder.append("\t\t\tacceptThis(val);\n"); + for (int i = 0; i < ctx.allTypeInfos().size(); i++) { + TypedElementName method = ctx.allTypeInfos().get(i); + String beanAttributeName = ctx.allAttributeNames().get(i); + appendAnnotations(builder, method.annotations(), "\t"); + builder.append("\tprivate "); + builder.append(getFieldModifier()); + builder.append(toGenerics(method, false)).append(" "); + builder.append(beanAttributeName).append(";\n"); } - builder.append("\t\t}\n\n"); } - private void appendBuilderHeader(StringBuilder builder, - BodyContext ctx) { - builder.append("\n\t/**\n" - + "\t * The fluent builder for this type.\n" - + "\t *\n"); - if (!ctx.doingConcreteType) { - builder.append("\t * @param <").append(ctx.genericBuilderAliasDecl).append(">\tthe type of the builder\n"); - builder.append("\t * @param <").append(ctx.genericBuilderAcceptAliasDecl) - .append(">\tthe type of the built instance\n"); - } - builder.append("\t */\n"); - builder.append("\tpublic "); - if (!ctx.doingConcreteType) { + /** + * Adds the header part of the generated builder. + * + * @param builder the builder + * @param ctx the context + */ + protected void appendHeader(StringBuilder builder, + BodyContext ctx) { + builder.append("package ").append(ctx.implTypeName().packageName()).append(";\n\n"); + builder.append("import java.util.Collections;\n"); + builder.append("import java.util.List;\n"); + builder.append("import java.util.Map;\n"); + builder.append("import java.util.Set;\n"); + builder.append("import java.util.Objects;\n\n"); + appendExtraImports(builder, ctx); + + builder.append("/**\n"); + String type = (ctx.doingConcreteType()) ? "Concrete" : "Abstract"; + builder.append(" * ").append(type).append(" implementation w/ builder for {@link "); + builder.append(ctx.typeInfo().typeName()).append("}.\n"); + builder.append(" */\n"); + builder.append(BuilderTemplateHelper.getDefaultGeneratedSticker(getClass().getSimpleName())).append("\n"); + builder.append("@SuppressWarnings(\"unchecked\")\t\n"); + appendAnnotations(builder, ctx.typeInfo().annotations(), ""); + builder.append("public "); + if (!ctx.doingConcreteType()) { builder.append("abstract "); } - builder.append("static class ").append(ctx.genericBuilderClassDecl); + builder.append("class ").append(ctx.implTypeName().className()); - if (ctx.doingConcreteType) { + if (ctx.hasParent() || ctx.doingConcreteType()) { builder.append(" extends "); - builder.append(toAbstractImplTypeName(ctx.typeInfo.typeName(), ctx.builderAnnotation).get()); - builder.append(".").append(ctx.genericBuilderClassDecl); - builder.append("<").append(ctx.genericBuilderClassDecl).append(", ").append(ctx.ctorBuilderAcceptTypeName) - .append("> {\n"); + } + + if (ctx.doingConcreteType()) { + builder.append(toAbstractImplTypeName(ctx.typeInfo().typeName(), ctx.builderAnnotation()).get()); } else { - builder.append("<").append(ctx.genericBuilderAliasDecl).append(" extends ").append(ctx.genericBuilderClassDecl); - builder.append("<").append(ctx.genericBuilderAliasDecl).append(", "); - builder.append(ctx.genericBuilderAcceptAliasDecl).append(">, ").append(ctx.genericBuilderAcceptAliasDecl) - .append(" extends "); - builder.append(ctx.ctorBuilderAcceptTypeName).append("> "); - if (ctx.hasParent) { - builder.append("extends ").append(toAbstractImplTypeName(ctx.parentTypeName.get(), ctx.builderAnnotation).get()) - .append(".").append(ctx.genericBuilderClassDecl); - builder.append("<").append(ctx.genericBuilderAliasDecl).append(", ").append(ctx.genericBuilderAcceptAliasDecl); - builder.append(">"); - } else if (ctx.hasStreamSupportOnBuilder) { - builder.append("implements Supplier<").append(ctx.genericBuilderAcceptAliasDecl) - .append(">, Consumer<").append(ctx.genericBuilderAcceptAliasDecl).append(">"); + if (ctx.hasParent()) { + builder.append(toAbstractImplTypeName(ctx.parentTypeName().get(), ctx.builderAnnotation()).get()); } - if (!ctx.hasParent) { - 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("> */"); - } + + if (!ctx.hasParent() && ctx.hasStreamSupportOnImpl()) { + builder.append("<").append(ctx.genericBuilderAcceptAliasDecl()).append(" extends ") + .append(ctx.implTypeName().className()).append(">"); } - builder.append(" {\n"); + builder.append(" implements ").append(ctx.typeInfo().typeName()); + if (!ctx.hasParent() && ctx.hasStreamSupportOnImpl()) { + builder.append(", Supplier<").append(ctx.genericBuilderAcceptAliasDecl()).append(">"); + } } + + builder.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 - */ - protected void appendRequiredValidator(StringBuilder builder, - BodyContext ctx) { - if (ctx.includeMetaAttributes) { - builder.append("\t\t\tRequiredAttributeVisitor visitor = new RequiredAttributeVisitor();\n" - + "\t\t\tvisitAttributes(visitor, null);\n" - + "\t\t\tvisitor.validate();\n"); - } - } - - private void appendToBuilderMethods(StringBuilder builder, - BodyContext ctx) { - if (!ctx.doingConcreteType) { - return; - } - - builder.append("\t/**\n" - + "\t * Creates a builder for this type.\n" - + "\t *\n"); - builder.append("\t * @return A builder for {@link "); - builder.append(ctx.typeInfo.typeName()); - builder.append("}\n\t */\n"); - builder.append("\tpublic static ").append(ctx.genericBuilderClassDecl); - builder.append(" builder() {\n"); - builder.append("\t\treturn new Builder((").append(ctx.typeInfo.typeName()).append(") null);\n"); - builder.append("\t}\n\n"); - - builder.append("\t/**\n" - + "\t * Creates a builder for this type, initialized with the attributes from the values passed" - + ".\n\n"); - builder.append("\t * @param val the value to copy to initialize the builder attributes\n"); - builder.append("\t * @return A builder for {@link ").append(ctx.typeInfo.typeName()); - builder.append("}\n\t */\n"); - - builder.append("\tpublic static ").append(ctx.genericBuilderClassDecl); - builder.append(" toBuilder(").append(ctx.ctorBuilderAcceptTypeName).append(" val) {\n"); - builder.append("\t\treturn new Builder(val);\n"); - builder.append("\t}\n\n"); - - String decl = "public static Builder toBuilder({args}) {"; - appendExtraToBuilderBuilderFunctions(builder, decl, ctx); - } - - private void appendInterfaceBasedGetters(StringBuilder builder, - BodyContext ctx) { - if (ctx.doingConcreteType) { - return; - } - - 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()) - .append("() {\n"); - builder.append("\t\treturn ").append(beanAttributeName).append(";\n"); - builder.append("\t}\n\n"); - i++; - } - } - - private void appendCtor(StringBuilder builder, - BodyContext ctx) { - builder.append("\n\t/**\n" - + "\t * Constructor using the builder argument.\n" - + "\t *\n" - + "\t * @param b\tthe builder\n" - + "\t */\n"); - builder.append("\tprotected ").append(ctx.implTypeName.className()); - builder.append("("); - builder.append(ctx.genericBuilderClassDecl); - if (ctx.doingConcreteType) { - builder.append(" b) {\n"); - builder.append("\t\tsuper(b);\n"); - } else { - if (!ctx.doingConcreteType) { - builder.append(""); - } - builder.append(" b) {\n"); - appendExtraCtorCode(builder, ctx.hasParent, "b", ctx.typeInfo); - appendCtorCode(builder, "b", ctx); - } - - builder.append("\t}\n\n"); - } - - private void appendHashCodeAndEquals(StringBuilder builder, - BodyContext ctx) { - if (ctx.doingConcreteType) { - 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 = 0;\n"); - } - for (TypedElementName method : ctx.allTypeInfos) { - builder.append("\t\thashCode ^= Objects.hashCode(").append(method.elementName()).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"); - } - builder.append("\t\treturn equals;\n"); - builder.append("\t}\n\n"); - } - - private void appendInnerToStringMethod(StringBuilder builder, - BodyContext ctx) { - if (ctx.doingConcreteType) { - return; - } - - builder.append("\t/**\n" - + "\t * Produces the inner portion of the toString() output (i.e., what is between the parens).\n" - + "\t *\n" - + "\t * @return portion of the toString output\n" - + "\t */\n"); - if (ctx.hasParent) { - builder.append("\t@Override\n"); - } - builder.append("\tprotected String toStringInner() {\n"); - if (ctx.hasParent) { - builder.append("\t\tString result = super.toStringInner();\n"); - if (!ctx.allAttributeNames.isEmpty()) { - builder.append("\t\tif (!result.isEmpty() && !result.endsWith(\", \")) {\n"); - builder.append("\t\t\tresult += \", \";\n"); - builder.append("\t\t}\n"); - } - } else { - builder.append("\t\tString result = \"\";\n"); - } - - int i = 0; - for (String beanAttributeName : ctx.allAttributeNames) { - TypedElementName method = ctx.allTypeInfos.get(i++); - TypeName typeName = method.typeName(); - builder.append("\t\tresult += \"").append(beanAttributeName).append("=\" + "); - if (typeName.array()) { - builder.append("(Objects.isNull(").append(beanAttributeName).append(") ? null : "); - if (typeName.primitive()) { - builder.append("\"not-null\""); - } else { - builder.append("java.util.Arrays.asList("); - builder.append(method.elementName()).append("())"); - } - builder.append(")"); - } else { - builder.append(method.elementName()).append("()"); - } - if (i < ctx.allAttributeNames.size()) { - builder.append(" + \", \""); - } - builder.append(";\n"); - } - builder.append("\t\treturn result;\n"); - builder.append("\t}\n\n"); - } - - /** - * Adds the basic getters to the generated builder output. - * - * @param builder the builder - * @param ctx the context - */ - protected void appendBasicGetters(StringBuilder builder, - BodyContext ctx) { - if (ctx.doingConcreteType) { - 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" - + "\t\treturn (T) this;\n" - + "\t}\n\n"); - } - } - - /** - * Appends meta attribute related methods. - * - * @param builder the builder - * @param ctx the context - */ - protected void appendMetaAttributes(StringBuilder builder, - BodyContext ctx) { - if (!ctx.doingConcreteType && ctx.includeMetaAttributes) { - builder.append("\tprivate static Map> __calcMeta() {\n"); - builder.append("\t\tMap> metaProps = new java.util.LinkedHashMap<>();\n"); - - AtomicBoolean needsCustomMapOf = new AtomicBoolean(); - appendMetaProps(builder, "metaProps", - ctx.typeInfo, ctx.map, ctx.allAttributeNames, ctx.allTypeInfos, needsCustomMapOf); - builder.append("\t\treturn metaProps;\n"); - builder.append("\t}\n\n"); - - if (needsCustomMapOf.get()) { - appendCustomMapOf(builder); - } - - builder.append("\t/**\n" - + "\t * The map of meta attributes describing each element of this type.\n" - + "\t *\n" - + "\t * @return the map of meta attributes using the key being the attribute name\n" - + "\t */\n"); - builder.append("\tpublic static Map> __metaAttributes() {\n" - + "\t\treturn META_PROPS;\n" - + "\t}\n\n"); - } - } - - /** - * Adds the fields part of the generated builder. - * - * @param builder the builder - * @param ctx the context - */ - protected void appendFields(StringBuilder builder, - BodyContext ctx) { - if (ctx.doingConcreteType) { - return; - } - - for (int i = 0; i < ctx.allTypeInfos.size(); i++) { - TypedElementName method = ctx.allTypeInfos.get(i); - String beanAttributeName = ctx.allAttributeNames.get(i); - appendAnnotations(builder, method.annotations(), "\t"); - builder.append("\tprivate "); - builder.append(getFieldModifier()); - builder.append(toGenerics(method, false)).append(" "); - builder.append(beanAttributeName).append(";\n"); - } - } - - /** - * Adds the header part of the generated builder. - * - * @param builder the builder - * @param ctx the context - */ - protected void appendHeader(StringBuilder builder, - BodyContext ctx) { - builder.append("package ").append(ctx.implTypeName.packageName()).append(";\n\n"); - builder.append("import java.util.Collections;\n"); - builder.append("import java.util.List;\n"); - builder.append("import java.util.Map;\n"); - builder.append("import java.util.Objects;\n\n"); - appendExtraImports(builder, ctx); - - builder.append("/**\n"); - String type = (ctx.doingConcreteType) ? "Concrete" : "Abstract"; - builder.append(" * ").append(type).append(" implementation w/ builder for {@link "); - builder.append(ctx.typeInfo.typeName()).append("}.\n"); - builder.append(" */\n"); - builder.append(BuilderTemplateHelper.getDefaultGeneratedSticker(getClass().getSimpleName())).append("\n"); - builder.append("@SuppressWarnings(\"unchecked\")\t\n"); - appendAnnotations(builder, ctx.typeInfo.annotations(), ""); - builder.append("public "); - if (!ctx.doingConcreteType) { - builder.append("abstract "); - } - builder.append("class ").append(ctx.implTypeName.className()); - - if (ctx.hasParent || ctx.doingConcreteType) { - builder.append(" extends "); - } - - if (ctx.doingConcreteType) { - builder.append(toAbstractImplTypeName(ctx.typeInfo.typeName(), ctx.builderAnnotation).get()); - } else { - if (ctx.hasParent) { - builder.append(toAbstractImplTypeName(ctx.parentTypeName.get(), ctx.builderAnnotation).get()); - } - - if (!ctx.hasParent && ctx.hasStreamSupportOnImpl) { - builder.append("<").append(ctx.genericBuilderAcceptAliasDecl).append(" extends ") - .append(ctx.implTypeName.className()).append(">"); - } - - builder.append(" implements ").append(ctx.typeInfo.typeName()); - if (!ctx.hasParent && ctx.hasStreamSupportOnImpl) { - builder.append(", Supplier<").append(ctx.genericBuilderAcceptAliasDecl).append(">"); - } - } - - builder.append(" {\n"); - } - - private void appendDefaultValueAssignment(StringBuilder builder, - TypedElementName method, - String defaultVal) { - TypeName type = method.typeName(); - boolean isOptional = type.name().equals(Optional.class.getName()); - if (isOptional) { - builder.append(Optional.class.getName()).append(".of("); - if (!type.typeArguments().isEmpty()) { - type = type.typeArguments().get(0); - } - } - - boolean isString = type.name().equals(String.class.getName()) && !type.array(); - boolean isCharArr = type.fqName().equals("char[]"); - if ((isString || isCharArr) && !defaultVal.startsWith("\"")) { - builder.append("\""); - } - - builder.append(defaultVal); - - if ((isString || isCharArr) && !defaultVal.endsWith("\"")) { - builder.append("\""); - if (isCharArr) { - builder.append(".toCharArray()"); - } - } - - if (isOptional) { - builder.append(")"); - } - } - - /** - * Adds extra imports to the generated builder. + * Adds extra imports to the generated builder. * * @param builder the builder * @param ctx the context */ protected void appendExtraImports(StringBuilder builder, BodyContext ctx) { - if (!ctx.doingConcreteType) { + if (!ctx.doingConcreteType()) { builder.append("import java.util.function.Consumer;\n"); builder.append("import java.util.function.Supplier;\n"); builder.append("\n"); } - if (ctx.requireLibraryDependencies) { + if (ctx.requireLibraryDependencies()) { builder.append("import ").append(AttributeVisitor.class.getName()).append(";\n"); - if (ctx.doingConcreteType) { + if (ctx.doingConcreteType()) { builder.append("import ").append(RequiredAttributeVisitor.class.getName()).append(";\n"); } builder.append("\n"); @@ -1010,13 +394,13 @@ protected void appendExtraImports(StringBuilder builder, */ protected void appendToStringMethod(StringBuilder builder, BodyContext ctx) { - if (ctx.doingConcreteType) { + if (ctx.doingConcreteType()) { return; } builder.append("\t@Override\n"); builder.append("\tpublic String toString() {\n"); - builder.append("\t\treturn ").append(ctx.typeInfo.typeName()); + builder.append("\t\treturn ").append(ctx.typeInfo().typeName()); builder.append(".class.getSimpleName() + \"(\" + toStringInner() + \")\";\n"); builder.append("\t}\n\n"); } @@ -1030,7 +414,7 @@ protected void appendToStringMethod(StringBuilder builder, */ protected void appendExtraMethods(StringBuilder builder, BodyContext ctx) { - if (ctx.includeMetaAttributes) { + if (ctx.includeMetaAttributes()) { appendVisitAttributes(builder, "", false, ctx); } } @@ -1044,80 +428,7 @@ protected void appendExtraMethods(StringBuilder builder, */ protected void appendExtraInnerClasses(StringBuilder builder, BodyContext ctx) { - if (ctx.doingConcreteType) { - return; - } - - if (!ctx.hasParent - && ctx.includeMetaAttributes - && !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"); - builder.append("\t@FunctionalInterface\n" - + "\tpublic static interface AttributeVisitor {\n" - + "\t\t/**\n" - + "\t\t * Visits the attribute named 'attrName'.\n" - + "\t\t *\n" - + "\t\t * @param attrName\t\tthe attribute name\n" - + "\t\t * @param valueSupplier\tthe attribute value supplier\n" - + "\t\t * @param meta\t\t\tthe meta information for the attribute\n" - + "\t\t * @param userDefinedCtx a user defined context that can be used for holding an " - + "object of your choosing\n" - + "\t\t * @param type\t\t\tthe type of the attribute\n" - + "\t\t * @param typeArgument\tthe type arguments (if type is a parameterized / generic " - + "type)\n" - + "\t\t */\n" - + "\t\tvoid visit(String attrName, Supplier valueSupplier, " - + "Map meta, Object userDefinedCtx, Class " - + "type, Class... typeArgument);\n" - + "\t}"); - - builder.append("\n\n\t/**\n" - + "\t * An implementation of {@link AttributeVisitor} that will validate each attribute to " - + "enforce not-null. The source\n" - + "\t * must be annotated with {@code ConfiguredOption(required=true)} for this to be " - + "enforced.\n" - + "\t */\n"); - builder.append("\tprotected static class RequiredAttributeVisitor implements AttributeVisitor {\n" - + "\t\tprivate List errors;\n" - + "\n" - + "\t\t/**\n" - + "\t\t * Default Constructor.\n" - + "\t\t */\n" - + "\t\tprotected RequiredAttributeVisitor() {\n" - + "\t\t}\n\n"); - builder.append("\t\t@Override\n" - + "\t\tpublic void visit(String attrName,\n" - + "\t\t\t\t\t\t Supplier valueSupplier,\n" - + "\t\t\t\t\t\t Map meta,\n" - + "\t\t\t\t\t\t Object userDefinedCtx,\n" - + "\t\t\t\t\t\t Class type,\n" - + "\t\t\t\t\t\t Class... typeArgument) {\n" - + "\t\t\tboolean required = Boolean.valueOf((String) meta.get(\"required\"));\n" - + "\t\t\tif (!required) {\n" - + "\t\t\t\treturn;\n" - + "\t\t\t}\n" - + "\t\t\t\n" - + "\t\t\tObject val = valueSupplier.get();\n" - + "\t\t\tif (Objects.nonNull(val)) {\n" - + "\t\t\t\treturn;\n" - + "\t\t\t}\n" - + "\t\t\t\n" - + "\t\t\tif (Objects.isNull(errors)) {\n" - + "\t\t\t\t errors = new java.util.LinkedList<>();\n" - + "\t\t\t}\n" - + "\t\t\terrors.add(\"'\" + attrName + \"' is a required attribute and should not be null\")" - + ";\n" - + "\t\t}\n" - + "\n" - + "\t\tvoid validate() {\n" - + "\t\t\tif (Objects.nonNull(errors) && !errors.isEmpty()) {\n" - + "\t\t\t\tthrow new AssertionError(String.join(\", \", errors));\n" - + "\t\t\t}\n" - + "\t\t}\n" - + "\t}\n"); - } + GenerateVisitor.appendAttributeVisitors(builder, ctx); } /** @@ -1141,26 +452,20 @@ protected void appendVisitAttributes(StringBuilder builder, String extraTabs, boolean beanNameRef, BodyContext ctx) { - if (ctx.hasParent) { + if (ctx.hasParent()) { builder.append(extraTabs).append("\t@Override\n"); } else { - builder.append(extraTabs).append("\t/**\n"); - builder.append(extraTabs).append("\t * Visits all attributes of " + ctx.typeInfo.typeName() + ", calling the {@link " - + "AttributeVisitor} for each.\n"); - builder.append(extraTabs).append("\t *\n"); - builder.append(extraTabs).append("\t * @param visitor\t\t\tthe visitor called for each attribute\n"); - builder.append(extraTabs).append("\t * @param userDefinedCtx\tany object you wish to pass to each visit call\n"); - builder.append(extraTabs).append("\t */\n"); + GenerateJavadoc.visitAttributes(builder, ctx, extraTabs); } builder.append(extraTabs).append("\tpublic void visitAttributes(AttributeVisitor visitor, Object userDefinedCtx) {\n"); - if (ctx.hasParent) { + if (ctx.hasParent()) { builder.append(extraTabs).append("\t\tsuper.visitAttributes(visitor, userDefinedCtx);\n"); } // void visit(String key, Object value, Object userDefinedCtx, Class type, Class... typeArgument); int i = 0; - for (String attrName : ctx.allAttributeNames) { - TypedElementName method = ctx.allTypeInfos.get(i); + for (String attrName : ctx.allAttributeNames()) { + TypedElementName method = ctx.allTypeInfos().get(i); String typeName = method.typeName().declaredName(); List typeArgs = method.typeName().typeArguments().stream() .map(it -> it.declaredName() + ".class") @@ -1186,35 +491,6 @@ protected void appendVisitAttributes(StringBuilder builder, builder.append(extraTabs).append("\t}\n\n"); } - private void appendCtorCode(StringBuilder builder, - String ignoredBuilderTag, - BodyContext ctx) { - if (ctx.hasParent) { - builder.append("\t\tsuper(b);\n"); - } - int i = 0; - for (String beanAttributeName : ctx.allAttributeNames) { - TypedElementName method = ctx.allTypeInfos.get(i++); - builder.append("\t\tthis.").append(beanAttributeName).append(" = "); - - if (isList(method)) { - builder.append("Objects.isNull(b.").append(beanAttributeName).append(")\n"); - builder.append("\t\t\t? Collections.emptyList() : Collections.unmodifiableList(new ") - .append(ctx.listType).append("<>(b.").append(beanAttributeName).append("));\n"); - } else if (isMap(method)) { - builder.append("Objects.isNull(b.").append(beanAttributeName).append(")\n"); - builder.append("\t\t\t? Collections.emptyMap() : Collections.unmodifiableMap(new ") - .append(ctx.mapType).append("<>(b.").append(beanAttributeName).append("));\n"); - } else if (isSet(method)) { - builder.append("Objects.isNull(b.").append(beanAttributeName).append(")\n"); - builder.append("\t\t\t? Collections.emptySet() : Collections.unmodifiableSet(new ") - .append(ctx.setType).append("<>(b.").append(beanAttributeName).append("));\n"); - } else { - builder.append("b.").append(beanAttributeName).append(";\n"); - } - } - } - /** * Adds extra default ctor code. * @@ -1247,10 +523,8 @@ protected void appendExtraPostCtorCode(StringBuilder builder, */ protected void appendExtraFields(StringBuilder builder, BodyContext ctx) { - if (!ctx.doingConcreteType && ctx.includeMetaAttributes) { - builder.append("\t/**\n" - + "\t * meta-props.\n" - + "\t */\n"); + if (!ctx.doingConcreteType() && ctx.includeMetaAttributes()) { + GenerateJavadoc.internalMetaPropsField(builder); builder.append("\tprotected static final Map> META_PROPS = " + "Collections.unmodifiableMap(__calcMeta());\n"); } @@ -1288,61 +562,6 @@ protected void appendExtraBuilderFields(StringBuilder builder, List allTypeInfos) { } - private void appendOverridesOfDefaultValues(StringBuilder builder, - BodyContext ctx) { - boolean first = true; - for (TypedElementName method : ctx.typeInfo.elementInfo()) { - String beanAttributeName = toBeanAttributeName(method, ctx.isBeanStyleRequired); - if (!ctx.allAttributeNames.contains(beanAttributeName)) { - // candidate for override... - String thisDefault = toConfiguredOptionValue(method, true, true).orElse(null); - String superDefault = superValue(ctx.typeInfo.superTypeInfo(), beanAttributeName, ctx.isBeanStyleRequired); - if (BuilderTypeTools.hasNonBlankValue(thisDefault) && !Objects.equals(thisDefault, superDefault)) { - if (first) { - builder.append("\t\t\tif (Objects.isNull(val)) {\n"); - first = false; - } - appendDefaultOverride(builder, beanAttributeName, method, thisDefault); - } - } - } - - if (!first) { - builder.append("\t\t\t}\n"); - } - } - - private String superValue(Optional optSuperTypeInfo, - String elemName, - boolean isBeanStyleRequired) { - if (optSuperTypeInfo.isEmpty()) { - return null; - } - TypeInfo superTypeInfo = optSuperTypeInfo.get(); - Optional method = superTypeInfo.elementInfo().stream() - .filter(it -> toBeanAttributeName(it, isBeanStyleRequired).equals(elemName)) - .findFirst(); - if (method.isPresent()) { - Optional defaultValue = toConfiguredOptionValue(method.get(), true, true); - if (defaultValue.isPresent() && BuilderTypeTools.hasNonBlankValue(defaultValue.get())) { - return defaultValue.orElse(null); - } - } else { - return superValue(superTypeInfo.superTypeInfo(), elemName, isBeanStyleRequired); - } - - return null; - } - - private void appendDefaultOverride(StringBuilder builder, - String attrName, - TypedElementName method, - String override) { - builder.append("\t\t\t\t").append(attrName).append("("); - appendDefaultValueAssignment(builder, method, override); - builder.append(");\n"); - } - /** * Adds extra builder pre-steps. * @@ -1362,11 +581,11 @@ protected void appendBuilderBuildPreSteps(StringBuilder builder, */ protected void appendExtraBuilderMethods(StringBuilder builder, BodyContext ctx) { - if (ctx.doingConcreteType) { + if (ctx.doingConcreteType()) { return; } - if (ctx.includeMetaAttributes) { + if (ctx.includeMetaAttributes()) { appendVisitAttributes(builder, "\t", true, ctx); } } @@ -1399,67 +618,6 @@ protected void appendMetaProps(StringBuilder builder, .append(");\n")); } - private void appendCustomMapOf(StringBuilder builder) { - builder.append("\tprivate static Map __mapOf(Object... args) {\n" - + "\t\tMap result = new java.util.LinkedHashMap<>(args.length / 2);\n" - + "\t\tint i = 0;\n" - + "\t\twhile (i < args.length) {\n" - + "\t\t\tresult.put((String) args[i], args[i + 1]);\n" - + "\t\t\ti += 2;\n" - + "\t\t}\n" - + "\t\treturn result;\n" - + "\t}\n\n"); - } - - private String mapOf(String attrName, - TypedElementName method, - AtomicBoolean needsCustomMapOf) { - final Optional configuredOptions = DefaultAnnotationAndValue - .findFirst(ConfiguredOption.class.getName(), method.annotations()); - - TypeName typeName = method.typeName(); - String typeDecl = "\"type\", " + typeName.name() + ".class"; - if (!typeName.typeArguments().isEmpty()) { - int pos = typeName.typeArguments().size() - 1; - typeDecl += ", \"componentType\", " + typeName.typeArguments().get(pos).name() + ".class"; - } - - String key = (configuredOptions.isEmpty()) - ? null : configuredOptions.get().value("key").orElse(null); - key = normalizeConfiguredOptionKey(key, attrName, method); - if (BuilderTypeTools.hasNonBlankValue(key)) { - typeDecl += ", " + quotedTupleOf("key", key); - } - String defaultValue = method.defaultValue().orElse(null); - - if (configuredOptions.isEmpty() && !BuilderTypeTools.hasNonBlankValue(defaultValue)) { - return "Map.of(" + typeDecl + ")"; - } - - needsCustomMapOf.set(true); - StringBuilder result = new StringBuilder(); - result.append("__mapOf(").append(typeDecl); - - if (configuredOptions.isEmpty()) { - if (defaultValue.startsWith("{")) { - defaultValue = "new String[] " + defaultValue; - } - result.append(", "); - result.append(quotedValueOf("value")).append(", ").append(defaultValue); - } else { - configuredOptions.get().values().entrySet().stream() - .filter(e -> BuilderTypeTools.hasNonBlankValue(e.getValue())) - .filter(e -> !e.getKey().equals("key")) - .forEach((e) -> { - result.append(", "); - result.append(quotedTupleOf(e.getKey(), e.getValue())); - }); - } - result.append(")"); - - return result.toString(); - } - /** * Normalize the configured option key. * @@ -1474,116 +632,6 @@ protected String normalizeConfiguredOptionKey(String key, return BuilderTypeTools.hasNonBlankValue(key) ? key : ""; } - private String quotedTupleOf(String key, - String val) { - assert (Objects.nonNull(key)); - assert (BuilderTypeTools.hasNonBlankValue(val)) : key; - if (key.equals("value") && ConfiguredOption.UNCONFIGURED.equals(val)) { - val = ConfiguredOption.class.getName() + ".UNCONFIGURED"; - } else { - val = quotedValueOf(val); - } - return quotedValueOf(key) + ", " + val; - } - - private String quotedValueOf(String val) { - if (val.startsWith("\"") && val.endsWith("\"")) { - return val; - } - - return "\"" + val + "\""; - } - - private static void gatherAllAttributeNames(BodyContext ctx, - TypeInfo typeInfo) { - TypeInfo superTypeInfo = typeInfo.superTypeInfo().orElse(null); - if (Objects.nonNull(superTypeInfo)) { - Optional superBuilderAnnotation = DefaultAnnotationAndValue - .findFirst(ctx.builderAnnotation.typeName(), superTypeInfo.annotations(), false); - if (superBuilderAnnotation.isEmpty()) { - gatherAllAttributeNames(ctx, superTypeInfo); - } else { - populateMap(ctx.map, superTypeInfo, ctx.isBeanStyleRequired); - } - - if (Objects.isNull(ctx.parentTypeName.get()) - && superTypeInfo.typeKind().equals("INTERFACE")) { - ctx.parentTypeName.set(superTypeInfo.typeName()); - } else if (Objects.isNull(ctx.parentAnnotationType.get()) - && superTypeInfo.typeKind().equals("ANNOTATION_TYPE")) { - ctx.parentAnnotationType.set(superTypeInfo.typeName()); - } - } - - for (TypedElementName method : typeInfo.elementInfo()) { - String beanAttributeName = toBeanAttributeName(method, ctx.isBeanStyleRequired); - TypedElementName existing = ctx.map.get(beanAttributeName); - if (Objects.nonNull(existing) - && BeanUtils.isBooleanType(method.typeName().name()) - && method.elementName().startsWith("is")) { - AtomicReference>> alternateNames = new AtomicReference<>(); - BeanUtils.validateAndParseMethodName(method.elementName(), - method.typeName().name(), true, alternateNames); - assert (Objects.nonNull(alternateNames.get())); - final String currentAttrName = beanAttributeName; - Optional alternateName = alternateNames.get().orElse(Collections.emptyList()).stream() - .filter(it -> !it.equals(currentAttrName)) - .findFirst(); - if (alternateName.isPresent() && !ctx.map.containsKey(alternateName.get())) { - beanAttributeName = alternateName.get(); - existing = ctx.map.get(beanAttributeName); - } - } - - if (Objects.nonNull(existing)) { - if (!existing.typeName().equals(method.typeName())) { - throw new IllegalStateException(method + " cannot redefine types from super for " + beanAttributeName); - } - - // allow the subclass to override the defaults, etc. - Objects.requireNonNull(ctx.map.put(beanAttributeName, method)); - int pos = ctx.allAttributeNames.indexOf(beanAttributeName); - if (pos >= 0) { - ctx.allTypeInfos.set(pos, method); - } - continue; - } - - Object prev = ctx.map.put(beanAttributeName, method); - assert (Objects.isNull(prev)); - - ctx.allTypeInfos.add(method); - if (ctx.allAttributeNames.contains(beanAttributeName)) { - throw new AssertionError("duplicate attribute name: " + beanAttributeName + " processing " + typeInfo); - } - ctx.allAttributeNames.add(beanAttributeName); - } - } - - private static void populateMap(Map map, - TypeInfo typeInfo, - boolean isBeanStyleRequired) { - if (typeInfo.superTypeInfo().isPresent()) { - populateMap(map, typeInfo.superTypeInfo().get(), isBeanStyleRequired); - } - - for (TypedElementName method : typeInfo.elementInfo()) { - String beanAttributeName = toBeanAttributeName(method, isBeanStyleRequired); - TypedElementName existing = map.get(beanAttributeName); - if (Objects.nonNull(existing)) { - if (!existing.typeName().equals(method.typeName())) { - throw new IllegalStateException(method + " cannot redefine types from super for " + beanAttributeName); - } - - // allow the subclass to override the defaults, etc. - Objects.requireNonNull(map.put(beanAttributeName, method)); - } else { - Object prev = map.put(beanAttributeName, method); - assert (Objects.isNull(prev)); - } - } - } - /** * Appends the singular setter methods on the builder. * @@ -1603,38 +651,7 @@ protected void maybeAppendSingularSetter(StringBuilder builder, String singularVal = toValue(Singular.class, method, false, false).orElse(null); if (Objects.nonNull(singularVal) && (isList || isMap || isSet)) { char[] methodName = reverseBeanName(singularVal.isBlank() ? maybeSingularFormOf(beanAttributeName) : singularVal); - builder.append("\t\t/**\n"); - builder.append("\t\t * Singular setter for '").append(beanAttributeName).append("'.\n"); - builder.append("\t\t *\n"); - if (isMap) { - builder.append("\t\t * @param key the key\n"); - } - builder.append("\t\t * @param val the new value\n"); - builder.append("\t\t * @return this fluent builder\n"); - builder.append("\t\t * @see #").append(method.elementName()).append("()\n"); - builder.append("\t\t */\n"); - builder.append("\t\tpublic ").append(ctx.genericBuilderAliasDecl).append(" add") - .append(methodName).append("(").append(toGenericsDecl(method)).append(") {\n"); - builder.append("\t\t\tif (Objects.isNull(").append(beanAttributeName).append(")) {\n"); - builder.append("\t\t\t\t").append(beanAttributeName).append(" = new "); - if (isList) { - builder.append(ctx.listType); - } else if (isMap) { - builder.append(ctx.mapType); - } else { // isSet - builder.append(ctx.setType); - } - - builder.append("<>();\n"); - builder.append("\t\t\t}\n"); - builder.append("\t\t\tthis.").append(beanAttributeName); - if (isList || isSet) { - builder.append(".add(val);\n"); - } else { // isMap - builder.append(".put(key, val);\n"); - } - builder.append("\t\t\treturn identity();\n"); - builder.append("\t\t}\n\n"); + GenerateMethod.singularSetter(builder, ctx, method, beanAttributeName, methodName); } } @@ -1657,73 +674,70 @@ protected static String maybeSingularFormOf(String beanAttributeName) { * * @param mainBuilder the builder * @param beanAttributeName the bean attribute name - * @param optMethodName the optional method name + * @param methodName the method name * @param method the method * @param ctx the body context */ protected void appendSetter(StringBuilder mainBuilder, String beanAttributeName, - Optional optMethodName, + String methodName, TypedElementName method, BodyContext ctx) { - String methodName = optMethodName.orElse(null); - if (Objects.isNull(methodName)) { - methodName = Objects.requireNonNull(beanAttributeName); - } - boolean isList = isList(method); - boolean isMap = !isList && isMap(method); - boolean isSet = !isMap && isSet(method); + + TypeName typeName = method.typeName(); + boolean isList = typeName.isList(); + boolean isMap = !isList && typeName.isMap(); + boolean isSet = !isMap && typeName.isSet(); boolean upLevel = isSet || isList; StringBuilder builder = new StringBuilder(); - builder.append("\t\t/**\n"); - builder.append("\t\t * Setter for '").append(beanAttributeName).append("'.\n"); - builder.append("\t\t *\n"); - builder.append("\t\t * @param val the new value\n"); - builder.append("\t\t * @return this fluent builder\n"); - builder.append("\t\t * @see #").append(method.elementName()).append("()\n"); - builder.append("\t\t */\n"); - builder.append("\t\tpublic ").append(ctx.genericBuilderAliasDecl).append(" ").append(methodName).append("(") + GenerateJavadoc.setter(builder, beanAttributeName, method); + builder.append("\t\tpublic ").append(ctx.genericBuilderAliasDecl()).append(" ").append(methodName).append("(") .append(toGenerics(method, upLevel)).append(" val) {\n"); - builder.append("\t\t\tthis.").append(beanAttributeName).append(" = "); + + /* + Make sure that arguments are not null + */ + builder.append("\t\t\tObjects.requireNonNull(val);\n"); + + /* + Assign field, or update collection + */ + builder.append("\t\t\tthis.") + .append(beanAttributeName); if (isList) { - builder.append("Objects.isNull(val) ? null : new ").append(ctx.listType).append("<>(val);\n"); + builder.append(".clear();\n"); + builder.append("\t\t\tthis.") + .append(beanAttributeName) + .append(".addAll(val);\n"); } else if (isMap) { - builder.append("Objects.isNull(val) ? null : new ").append(ctx.mapType).append("<>(val);\n"); + builder.append(".clear();\n"); + builder.append("\t\t\tthis.") + .append(beanAttributeName) + .append(".putAll(val);\n"); } else if (isSet) { - builder.append("Objects.isNull(val) ? null : new ").append(ctx.setType).append("<>(val);\n"); - } else if (method.typeName().array()) { - builder.append("Objects.isNull(val) ? null : val.clone();\n"); + builder.append(".clear();\n"); + builder.append("\t\t\tthis.") + .append(beanAttributeName) + .append(".addAll(val);\n"); + } else if (typeName.array()) { + builder.append(" = val.clone();\n"); } else { - builder.append("val;\n"); + builder.append(" = val;\n"); } builder.append("\t\t\treturn identity();\n"); builder.append("\t\t}\n\n"); - TypeName typeName = method.typeName(); if (typeName.fqName().equals("char[]")) { - builder.append("\t\t/**\n"); - builder.append("\t\t * Setter for '").append(beanAttributeName).append("'.\n"); - builder.append("\t\t *\n"); - builder.append("\t\t * @param val the new value\n"); - builder.append("\t\t * @return this fluent builder\n"); - builder.append("\t\t * @see #").append(method.elementName()).append("()\n"); - builder.append("\t\t */\n"); - builder.append("\t\tpublic ").append(ctx.genericBuilderAliasDecl).append(" ").append(methodName) - .append("(String val) {\n"); - builder.append("\t\t\tthis.").append(beanAttributeName) - .append(" = Objects.isNull(val) ? null : val.toCharArray();\n"); - builder.append("\t\t\treturn identity();\n"); - builder.append("\t\t}\n\n"); + GenerateMethod.stringToCharSetter(builder, ctx, beanAttributeName, method, methodName); } mainBuilder.append(builder); - TypeName type = method.typeName(); - if (type.name().equals(Optional.class.getName()) && !type.typeArguments().isEmpty()) { - TypeName genericType = type.typeArguments().get(0); - appendDirectNonOptionalSetter(mainBuilder, beanAttributeName, method, genericType); + if (typeName.isOptional() && !typeName.typeArguments().isEmpty()) { + TypeName genericType = typeName.typeArguments().get(0); + appendDirectNonOptionalSetter(mainBuilder, ctx, beanAttributeName, method, methodName, genericType); } } @@ -1731,26 +745,19 @@ protected void appendSetter(StringBuilder mainBuilder, * Append the setters for the given bean attribute name. * * @param builder the builder + * @param ctx the body context * @param beanAttributeName the bean attribute name * @param method the method + * @param methodName the method name * @param genericType the generic return type name of the method */ protected void appendDirectNonOptionalSetter(StringBuilder builder, + BodyContext ctx, String beanAttributeName, TypedElementName method, + String methodName, TypeName genericType) { - builder.append("\t\t/**\n"); - builder.append("\t\t * Setter for '").append(beanAttributeName).append("'.\n"); - builder.append("\t\t *\n"); - builder.append("\t\t * @param val the new value\n"); - builder.append("\t\t * @return this fluent builder\n"); - builder.append("\t\t * @see #").append(method.elementName()).append("()\n"); - builder.append("\t\t */\n"); - builder.append("\t\tpublic B ").append(beanAttributeName).append("(") - .append(genericType.fqName()).append(" val) {\n"); - builder.append("\t\t\treturn ").append(beanAttributeName).append("(").append(Optional.class.getName()); - builder.append(".ofNullable(val));\n"); - builder.append("\t\t}\n\n"); + GenerateMethod.nonOptionalSetter(builder, ctx, beanAttributeName, method, methodName, genericType); } /** @@ -1777,66 +784,6 @@ protected void appendAnnotations(StringBuilder builder, } } - /** - * Returns true if the provided method involved {@link java.util.List}. - * - * @param method the method - * @return true if list is part of the type - */ - protected static boolean isList(TypedElementName method) { - return isList(method.typeName()); - } - - /** - * Returns true if the provided method involved {@link java.util.List}. - * - * @param typeName the type name - * @return true if list is part of the type - */ - protected static boolean isList(TypeName typeName) { - return (typeName.name().equals(List.class.getName())); - } - - /** - * Returns true if the provided method involved {@link java.util.Map}. - * - * @param method the method - * @return true if map is part of the type - */ - protected static boolean isMap(TypedElementName method) { - return isMap(method.typeName()); - } - - /** - * Returns true if the provided method involved {@link java.util.Map}. - * - * @param typeName the type name - * @return true if map is part of the type - */ - protected static boolean isMap(TypeName typeName) { - return (typeName.name().equals(Map.class.getName())); - } - - /** - * Returns true if the provided method involved {@link java.util.Set}. - * - * @param method the method - * @return true if set is part of the type - */ - protected static boolean isSet(TypedElementName method) { - return isSet(method.typeName()); - } - - /** - * Returns true if the provided method involved {@link java.util.Set}. - * - * @param typeName the type name - * @return true if set is part of the type - */ - protected static boolean isSet(TypeName typeName) { - return (typeName.name().equals(Set.class.getName())); - } - /** * Produces the generic descriptor decl for the method. * @@ -1861,42 +808,6 @@ protected static String toGenerics(TypeName typeName, return toGenerics(typeName, upLevelToCollection, 0); } - private static String toGenerics(TypeName typeName, - boolean upLevelToCollection, - int depth) { - if (typeName.typeArguments().isEmpty()) { - return (typeName.array() || Optional.class.getName().equals(typeName.name())) - ? typeName.fqName() : typeName.name(); - } - - if (upLevelToCollection) { - List upLevelInner = typeName.typeArguments().stream() - .map(it -> toGenerics(it, upLevelToCollection && 0 == depth, depth + 1)) - .collect(Collectors.toList()); - if (isList(typeName) || isSet(typeName)) { - return Collection.class.getName() + "<" + toString(upLevelInner) + ">"; - } else if (isMap(typeName)) { - return Map.class.getName() + "<" + toString(upLevelInner) + ">"; - } - } - - return typeName.fqName(); - } - - private static String toGenericsDecl(TypedElementName method) { - List compTypeNames = method.typeName().typeArguments(); - if (1 == compTypeNames.size()) { - return avoidWildcard(compTypeNames.get(0)) + " val"; - } else if (2 == compTypeNames.size()) { - return avoidWildcard(compTypeNames.get(0)) + " key, " + avoidWildcard(compTypeNames.get(1)) + " val"; - } - return "Object val"; - } - - private static String avoidWildcard(TypeName typeName) { - return typeName.wildcard() ? typeName.name() : typeName.fqName(); - } - /** * Walk the collection to build a separator-delimited string value. * @@ -1910,18 +821,18 @@ protected static String toString(Collection coll) { /** * Walk the collection to build a separator-delimited string value. * - * @param coll the collection - * @param optFnc the optional function to apply, defaulting to {@link String#valueOf(java.lang.Object)} - * @param optSeparator the optional separator, defaulting to ", " - * @param the types held by the collection + * @param coll the collection + * @param optFnc the optional function to apply, defaulting to {@link String#valueOf(java.lang.Object)} + * @param optSeparator the optional separator, defaulting to ", " + * @param the types held by the collection * @return the string representation */ protected static String toString(Collection coll, Optional> optFnc, Optional optSeparator) { - Function fn = optFnc.isEmpty() ? String::valueOf : optFnc.get(); - String separator = optSeparator.isEmpty() ? ", " : optSeparator.get(); - return coll.stream().map(fn::apply).collect(Collectors.joining(separator)); + Function fn = optFnc.orElse(String::valueOf); + String separator = optSeparator.orElse(", "); + return coll.stream().map(fn).collect(Collectors.joining(separator)); } /** @@ -1933,8 +844,8 @@ protected static String toString(Collection coll, * @return the default value, or empty if there is no default value applicable for the given arguments */ protected static Optional toConfiguredOptionValue(TypedElementName method, - boolean wantTypeElementDefaults, - boolean avoidBlanks) { + boolean wantTypeElementDefaults, + boolean avoidBlanks) { String val = toValue(ConfiguredOption.class, method, wantTypeElementDefaults, avoidBlanks).orElse(null); return ConfiguredOption.UNCONFIGURED.equals(val) ? Optional.empty() : Optional.ofNullable(val); } @@ -1949,9 +860,9 @@ protected static Optional toConfiguredOptionValue(TypedElementName metho * @return the default value, or empty if there is no default value applicable for the given arguments */ protected static Optional toValue(Class annoType, - TypedElementName method, - boolean wantTypeElementDefaults, - boolean avoidBlanks) { + TypedElementName method, + boolean wantTypeElementDefaults, + boolean avoidBlanks) { if (wantTypeElementDefaults && method.defaultValue().isPresent()) { if (!avoidBlanks || BuilderTypeTools.hasNonBlankValue(method.defaultValue().orElse(null))) { return method.defaultValue(); @@ -1972,20 +883,684 @@ protected static Optional toValue(Class annoType, return Optional.empty(); } - private static char[] reverseBeanName(String beanName) { - char[] c = beanName.toCharArray(); - c[0] = Character.toUpperCase(c[0]); - return c; - } - - private static String toBeanAttributeName(TypedElementName method, - boolean isBeanStyleRequired) { - AtomicReference>> refAttrNames = new AtomicReference<>(); - BeanUtils.validateAndParseMethodName(method.elementName(), method.typeName().name(), isBeanStyleRequired, refAttrNames); - List attrNames = (refAttrNames.get().isEmpty()) ? Collections.emptyList() : refAttrNames.get().get(); - if (!isBeanStyleRequired) { - return (!attrNames.isEmpty()) ? attrNames.get(0) : method.elementName(); - } - return Objects.requireNonNull(attrNames.get(0)); + private static String toGenerics(TypeName typeName, + boolean upLevelToCollection, + int depth) { + if (typeName.typeArguments().isEmpty()) { + return (typeName.array() || Optional.class.getName().equals(typeName.name())) + ? typeName.fqName() : typeName.name(); + } + + if (upLevelToCollection) { + List upLevelInner = typeName.typeArguments().stream() + .map(it -> toGenerics(it, upLevelToCollection && 0 == depth, depth + 1)) + .collect(Collectors.toList()); + if (typeName.isList() || typeName.isSet()) { + return Collection.class.getName() + "<" + toString(upLevelInner) + ">"; + } else if (typeName.isMap()) { + return Map.class.getName() + "<" + toString(upLevelInner) + ">"; + } + } + + return typeName.fqName(); + } + + private static String toGenericsDecl(TypedElementName method) { + List compTypeNames = method.typeName().typeArguments(); + if (1 == compTypeNames.size()) { + return avoidWildcard(compTypeNames.get(0)) + " val"; + } else if (2 == compTypeNames.size()) { + return avoidWildcard(compTypeNames.get(0)) + " key, " + avoidWildcard(compTypeNames.get(1)) + " val"; + } + return "Object val"; + } + + private static String avoidWildcard(TypeName typeName) { + return typeName.wildcard() ? typeName.name() : typeName.fqName(); + } + + private static char[] reverseBeanName(String beanName) { + char[] c = beanName.toCharArray(); + c[0] = Character.toUpperCase(c[0]); + return c; + } + + /** + * In support of {@link io.helidon.pico.builder.Builder#packageName()}. + */ + private String toPackageName(String packageName, + AnnotationAndValue builderAnnotation) { + String packageNameFromAnno = builderAnnotation.value("packageName").orElse(null); + if (packageNameFromAnno == null || packageNameFromAnno.isBlank()) { + return packageName; + } else if (packageNameFromAnno.startsWith(".")) { + return packageName + packageNameFromAnno; + } else { + return packageNameFromAnno; + } + } + + /** + * In support of {@link io.helidon.pico.builder.Builder#abstractImplPrefix()}. + */ + private String toAbstractImplTypePrefix(AnnotationAndValue builderAnnotation) { + return builderAnnotation.value("abstractImplPrefix").orElse(DEFAULT_ABSTRACT_IMPL_PREFIX); + } + + /** + * In support of {@link io.helidon.pico.builder.Builder#implPrefix()}. + */ + private String toImplTypePrefix(AnnotationAndValue builderAnnotation) { + return builderAnnotation.value("implPrefix").orElse(DEFAULT_IMPL_PREFIX); + } + + /** + * In support of {@link io.helidon.pico.builder.Builder#implSuffix()}. + */ + private String toImplTypeSuffix(AnnotationAndValue builderAnnotation) { + return builderAnnotation.value("implSuffix").orElse(DEFAULT_SUFFIX); + } + + private void appendBuilder(StringBuilder builder, + BodyContext ctx) { + appendBuilderHeader(builder, ctx); + appendExtraBuilderFields(builder, ctx.genericBuilderClassDecl(), ctx.builderAnnotation(), + ctx.typeInfo(), ctx.parentTypeName().get(), ctx.allAttributeNames(), ctx.allTypeInfos()); + appendBuilderBody(builder, ctx); + + appendExtraBuilderMethods(builder, ctx); + + if (ctx.doingConcreteType()) { + if (ctx.hasParent()) { + builder.append("\t\t@Override\n"); + } else { + 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}\n"); + } else { + int i = 0; + for (String beanAttributeName : ctx.allAttributeNames()) { + TypedElementName method = ctx.allTypeInfos().get(i); + TypeName typeName = method.typeName(); + boolean isList = typeName.isList(); + boolean isMap = !isList && typeName.isMap(); + boolean isSet = !isMap && typeName.isSet(); + boolean ignoredUpLevel = isSet || isList; + appendSetter(builder, beanAttributeName, beanAttributeName, method, ctx); + if (!isList && !isMap && !isSet) { + boolean isBoolean = BeanUtils.isBooleanType(typeName.name()); + if (isBoolean && beanAttributeName.startsWith("is")) { + // possibly overload setter to strip the "is"... + String basicAttributeName = "" + + Character.toLowerCase(beanAttributeName.charAt(2)) + + beanAttributeName.substring(3); + if (!ctx.allAttributeNames().contains(basicAttributeName)) { + appendSetter(builder, beanAttributeName, basicAttributeName, method, ctx); + } + } + } + + maybeAppendSingularSetter(builder, method, beanAttributeName, isList, isMap, isSet, ctx); + i++; + } + + if (!ctx.hasParent() && !ctx.requireLibraryDependencies()) { + GenerateJavadoc.buildMethod(builder); + builder.append("\t\tpublic abstract ") + .append(ctx.genericBuilderAcceptAliasDecl()) + .append(" build();\n\n"); + + if (ctx.hasStreamSupportOnBuilder()) { + GenerateJavadoc.updateConsumer(builder); + builder.append("\t\tpublic B update(Consumer<") + .append(ctx.genericBuilderAcceptAliasDecl()) + .append("> consumer) {\n" + + "\t\t\tconsumer.accept(get());\n" + + "\t\t\treturn identity();\n" + + "\t\t}\n\n"); + } + + if (!ctx.requireLibraryDependencies()) { + GenerateJavadoc.identity(builder); + builder.append("\t\t@SuppressWarnings(\"unchecked\")\n"); + builder.append("\t\tprotected ").append(ctx.genericBuilderAliasDecl()).append(" identity() {\n" + + "\t\t\treturn (") + .append(ctx.genericBuilderAliasDecl()).append(") this;\n" + + "\t\t}\n\n" + + "\t\t@Override\n" + + "\t\tpublic ") + .append(ctx.genericBuilderAcceptAliasDecl()).append(" get() {\n" + + "\t\t\treturn (") + .append(ctx.genericBuilderAcceptAliasDecl()).append(") build();\n" + + "\t\t}\n\n"); + } + } + + GenerateJavadoc.accept(builder); + builder.append("\t\tpublic ") + .append(ctx.genericBuilderAliasDecl()) + .append(" accept(").append(ctx.genericBuilderAcceptAliasDecl()).append(" val) {\n"); + builder.append("\t\t\tObjects.requireNonNull(val);\n"); + if (ctx.hasParent()) { + builder.append("\t\t\tsuper.accept(val);\n"); + } + builder.append("\t\t\tacceptThis(val);\n"); + builder.append("\t\t\treturn identity();\n"); + builder.append("\t\t}\n\n"); + + builder.append("\t\tprivate void acceptThis(").append(ctx.genericBuilderAcceptAliasDecl()).append(" val) {\n"); + + i = 0; + for (String beanAttributeName : ctx.allAttributeNames()) { + TypedElementName method = ctx.allTypeInfos().get(i++); + TypeName typeName = method.typeName(); + String getterName = method.elementName(); + builder.append("\t\t\t").append(beanAttributeName).append("("); + boolean isList = typeName.isList(); + boolean isMap = !isList && typeName.isMap(); + boolean isSet = !isMap && typeName.isSet(); + if (isList || isSet) { + builder.append("(java.util.Collection) "); + } else if (isMap) { + builder.append("(java.util.Map) "); + } + builder.append("val.").append(getterName).append("());\n"); + } + builder.append("\t\t}\n"); + } + + // end of the generated builder inner class here + builder.append("\t}\n"); + } + + private void appendBuilderBody(StringBuilder builder, BodyContext ctx) { + if (!ctx.doingConcreteType()) { + // prepare builder fields, starting with final (list, map, set) + boolean hasFinal = false; + for (int i = 0; i < ctx.allAttributeNames().size(); i++) { + String beanAttributeName = ctx.allAttributeNames().get(i); + TypedElementName method = ctx.allTypeInfos().get(i); + TypeName typeName = method.typeName(); + if (typeName.isList() || typeName.isMap() || typeName.isSet()) { + hasFinal = true; + addCollectionField(builder, ctx, method, typeName, beanAttributeName); + } + } + if (hasFinal) { + // eol to separate final from mutable fields + builder.append("\n"); + } + // then any other field + for (int i = 0; i < ctx.allAttributeNames().size(); i++) { + String beanAttributeName = ctx.allAttributeNames().get(i); + TypedElementName method = ctx.allTypeInfos().get(i); + TypeName typeName = method.typeName(); + if (typeName.isList() || typeName.isMap() || typeName.isSet()) { + continue; + } + addField(builder, method, typeName, beanAttributeName); + } + builder.append("\n"); + } + + GenerateJavadoc.builderConstructor(builder); + if (ctx.doingConcreteType()) { + builder.append("\t\tprotected ").append(ctx.genericBuilderClassDecl()).append("() {\n"); + builder.append("\t\t\tsuper();\n"); + } else { + builder.append("\t\tprotected ").append(ctx.genericBuilderClassDecl()).append("() {\n"); + if (ctx.hasParent()) { + builder.append("\t\t\tsuper();\n"); + } + appendOverridesOfDefaultValues(builder, ctx); + } + builder.append("\t\t}\n\n"); + } + + private void addField(StringBuilder builder, + TypedElementName method, + TypeName type, + String beanAttributeName) { + + GenerateJavadoc.builderField(builder, method); + builder.append("\t\tprotected ").append(type.array() ? type.fqName() : type.name()).append(" ") + .append(beanAttributeName); + Optional defaultVal = toConfiguredOptionValue(method, true, true); + if (defaultVal.isPresent()) { + builder.append(" = "); + appendDefaultValueAssignment(builder, method, defaultVal.get()); + } else { + if (type.isOptional()) { + builder.append(" = java.util.Optional.empty()"); + } + } + builder.append(";\n"); + } + + private void addCollectionField(StringBuilder builder, + BodyContext ctx, + TypedElementName method, + TypeName typeName, + String beanAttributeName) { + GenerateJavadoc.builderField(builder, method); + + builder.append("\t\tprotected final ") + .append(typeName.name()) + .append(" ") + .append(beanAttributeName) + .append(" = new ") + .append(collectionType(ctx, typeName)) + .append("<>();\n"); + } + + private String collectionType(BodyContext ctx, TypeName type) { + if (type.isList()) { + return ctx.listType(); + } + if (type.isMap()) { + return ctx.mapType(); + } + if (type.isSet()) { + return ctx.setType(); + } + throw new IllegalStateException("Type is not a known collection: " + type); + } + + private void appendBuilderHeader(StringBuilder builder, + BodyContext ctx) { + GenerateJavadoc.builderClass(builder, ctx); + builder.append("\tpublic "); + if (!ctx.doingConcreteType()) { + builder.append("abstract "); + } + builder.append("static class ").append(ctx.genericBuilderClassDecl()); + + if (ctx.doingConcreteType()) { + builder.append(" extends "); + builder.append(toAbstractImplTypeName(ctx.typeInfo().typeName(), ctx.builderAnnotation()).get()); + builder.append(".").append(ctx.genericBuilderClassDecl()); + builder.append("<").append(ctx.genericBuilderClassDecl()).append(", ").append(ctx.ctorBuilderAcceptTypeName()) + .append("> {\n"); + } else { + builder.append("<").append(ctx.genericBuilderAliasDecl()).append(" extends ").append(ctx.genericBuilderClassDecl()); + builder.append("<").append(ctx.genericBuilderAliasDecl()).append(", "); + builder.append(ctx.genericBuilderAcceptAliasDecl()).append(">, ").append(ctx.genericBuilderAcceptAliasDecl()) + .append(" extends "); + builder.append(ctx.ctorBuilderAcceptTypeName()).append("> "); + if (ctx.hasParent()) { + builder.append("extends ") + .append(toAbstractImplTypeName(ctx.parentTypeName().get(), ctx.builderAnnotation()).get()) + .append(".").append(ctx.genericBuilderClassDecl()); + builder.append("<").append(ctx.genericBuilderAliasDecl()) + .append(", ").append(ctx.genericBuilderAcceptAliasDecl()); + builder.append(">"); + } else if (ctx.hasStreamSupportOnBuilder()) { + builder.append("implements Supplier<").append(ctx.genericBuilderAcceptAliasDecl()).append(">"); + } + if (!ctx.hasParent()) { + 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("> */"); + } + } + + builder.append(" {\n"); + } + } + + private void appendToBuilderMethods(StringBuilder builder, + BodyContext ctx) { + if (!ctx.doingConcreteType()) { + return; + } + + GenerateMethod.builderMethods(builder, ctx); + + String decl = "public static Builder toBuilder({args}) {"; + appendExtraToBuilderBuilderFunctions(builder, decl, ctx); + } + + private void appendInterfaceBasedGetters(StringBuilder builder, + BodyContext ctx) { + if (ctx.doingConcreteType()) { + return; + } + + 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()) + .append("() {\n"); + builder.append("\t\treturn ").append(beanAttributeName).append(";\n"); + builder.append("\t}\n\n"); + i++; + } + } + + private void appendCtor(StringBuilder builder, + BodyContext ctx) { + + GenerateJavadoc.typeConstructorWithBuilder(builder); + builder.append("\tprotected ").append(ctx.implTypeName().className()); + builder.append("("); + builder.append(ctx.genericBuilderClassDecl()); + if (ctx.doingConcreteType()) { + builder.append(" b) {\n"); + builder.append("\t\tsuper(b);\n"); + } else { + if (!ctx.doingConcreteType()) { + builder.append(""); + } + builder.append(" b) {\n"); + appendExtraCtorCode(builder, ctx.hasParent(), "b", ctx.typeInfo()); + appendCtorCode(builder, "b", ctx); + } + + builder.append("\t}\n\n"); } + + private void appendHashCodeAndEquals(StringBuilder builder, + BodyContext ctx) { + if (ctx.doingConcreteType()) { + 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() + "()"); + } + 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"); + } + builder.append("\t\treturn equals;\n"); + builder.append("\t}\n\n"); + } + + private void appendInnerToStringMethod(StringBuilder builder, + BodyContext ctx) { + if (ctx.doingConcreteType()) { + return; + } + + GenerateJavadoc.innerToString(builder); + if (ctx.hasParent()) { + builder.append("\t@Override\n"); + } + builder.append("\tprotected String toStringInner() {\n"); + if (ctx.hasParent()) { + builder.append("\t\tString result = super.toStringInner();\n"); + if (!ctx.allAttributeNames().isEmpty()) { + builder.append("\t\tif (!result.isEmpty() && !result.endsWith(\", \")) {\n"); + builder.append("\t\t\tresult += \", \";\n"); + builder.append("\t\t}\n"); + } + } else { + builder.append("\t\tString result = \"\";\n"); + } + + int i = 0; + for (String beanAttributeName : ctx.allAttributeNames()) { + TypedElementName method = ctx.allTypeInfos().get(i++); + TypeName typeName = method.typeName(); + + builder.append("\t\tresult += \"").append(beanAttributeName).append("=\" + "); + + boolean handled = false; + + if (typeName.isOptional()) { + if (!typeName.typeArguments().isEmpty()) { + TypeName innerType = typeName.typeArguments().get(0); + if (innerType.array() && innerType.primitive()) { + // primitive types only if present or not + builder.append("(") + .append(method.elementName()) + .append("().isEmpty() ? \"Optional.empty\" : \"not-empty\")"); + handled = true; + } + } + } + + if (!handled) { + if (typeName.array()) { + builder.append("(").append(beanAttributeName).append(" == null ? \"null\" : "); + if (typeName.primitive()) { + builder.append("\"not-null\""); + } else { + builder.append("java.util.Arrays.asList("); + builder.append(method.elementName()).append("())"); + } + builder.append(")"); + } else { + builder.append(method.elementName()).append("()"); + } + } + if (i < ctx.allAttributeNames().size()) { + builder.append(" + \", \""); + } + builder.append(";\n"); + } + builder.append("\t\treturn result;\n"); + builder.append("\t}\n\n"); + } + + private void appendDefaultValueAssignment(StringBuilder builder, + TypedElementName method, + String defaultVal) { + TypeName type = method.typeName(); + boolean isOptional = type.isOptional(); + if (isOptional) { + builder.append(Optional.class.getName()).append(".of("); + if (!type.typeArguments().isEmpty()) { + type = type.typeArguments().get(0); + } + } + + boolean isString = type.name().equals(String.class.getName()) && !type.array(); + boolean isCharArr = type.fqName().equals("char[]"); + if ((isString || isCharArr) && !defaultVal.startsWith("\"")) { + builder.append("\""); + } + + builder.append(defaultVal); + + if ((isString || isCharArr) && !defaultVal.endsWith("\"")) { + builder.append("\""); + if (isCharArr) { + builder.append(".toCharArray()"); + } + } + + if (isOptional) { + builder.append(")"); + } + } + + private void appendCtorCode(StringBuilder builder, + String ignoredBuilderTag, + BodyContext ctx) { + if (ctx.hasParent()) { + builder.append("\t\tsuper(b);\n"); + } + int i = 0; + for (String beanAttributeName : ctx.allAttributeNames()) { + TypedElementName method = ctx.allTypeInfos().get(i++); + builder.append("\t\tthis.").append(beanAttributeName).append(" = "); + + if (method.typeName().isList()) { + builder.append("Collections.unmodifiableList(new ") + .append(ctx.listType()).append("<>(b.").append(beanAttributeName).append("));\n"); + } else if (method.typeName().isMap()) { + builder.append("Collections.unmodifiableMap(new ") + .append(ctx.mapType()).append("<>(b.").append(beanAttributeName).append("));\n"); + } else if (method.typeName().isSet()) { + builder.append("Collections.unmodifiableSet(new ") + .append(ctx.setType()).append("<>(b.").append(beanAttributeName).append("));\n"); + } else { + builder.append("b.").append(beanAttributeName).append(";\n"); + } + } + } + + private void appendOverridesOfDefaultValues(StringBuilder builder, + BodyContext ctx) { + boolean first = true; + for (TypedElementName method : ctx.typeInfo().elementInfo()) { + String beanAttributeName = toBeanAttributeName(method, ctx.isBeanStyleRequired()); + if (!ctx.allAttributeNames().contains(beanAttributeName)) { + // candidate for override... + String thisDefault = toConfiguredOptionValue(method, true, true).orElse(null); + String superDefault = superValue(ctx.typeInfo().superTypeInfo(), beanAttributeName, ctx.isBeanStyleRequired()); + if (BuilderTypeTools.hasNonBlankValue(thisDefault) && !Objects.equals(thisDefault, superDefault)) { + appendDefaultOverride(builder, beanAttributeName, method, thisDefault); + } + } + } + } + + private String superValue(Optional optSuperTypeInfo, + String elemName, + boolean isBeanStyleRequired) { + if (optSuperTypeInfo.isEmpty()) { + return null; + } + TypeInfo superTypeInfo = optSuperTypeInfo.get(); + Optional method = superTypeInfo.elementInfo().stream() + .filter(it -> toBeanAttributeName(it, isBeanStyleRequired).equals(elemName)) + .findFirst(); + if (method.isPresent()) { + Optional defaultValue = toConfiguredOptionValue(method.get(), true, true); + if (defaultValue.isPresent() && BuilderTypeTools.hasNonBlankValue(defaultValue.get())) { + return defaultValue.orElse(null); + } + } else { + return superValue(superTypeInfo.superTypeInfo(), elemName, isBeanStyleRequired); + } + + return null; + } + + private void appendDefaultOverride(StringBuilder builder, + String attrName, + TypedElementName method, + String override) { + builder.append("\t\t\t").append(attrName).append("("); + appendDefaultValueAssignment(builder, method, override); + builder.append(");\n"); + } + + private void appendCustomMapOf(StringBuilder builder) { + builder.append("\tprivate static Map __mapOf(Object... args) {\n" + + "\t\tMap result = new java.util.LinkedHashMap<>(args.length / 2);\n" + + "\t\tint i = 0;\n" + + "\t\twhile (i < args.length) {\n" + + "\t\t\tresult.put((String) args[i], args[i + 1]);\n" + + "\t\t\ti += 2;\n" + + "\t\t}\n" + + "\t\treturn result;\n" + + "\t}\n\n"); + } + + private String mapOf(String attrName, + TypedElementName method, + AtomicBoolean needsCustomMapOf) { + final Optional configuredOptions = DefaultAnnotationAndValue + .findFirst(ConfiguredOption.class.getName(), method.annotations()); + + TypeName typeName = method.typeName(); + String typeDecl = "\"type\", " + typeName.name() + ".class"; + if (!typeName.typeArguments().isEmpty()) { + int pos = typeName.typeArguments().size() - 1; + typeDecl += ", \"componentType\", " + typeName.typeArguments().get(pos).name() + ".class"; + } + + String key = (configuredOptions.isEmpty()) + ? null : configuredOptions.get().value("key").orElse(null); + key = normalizeConfiguredOptionKey(key, attrName, method); + if (BuilderTypeTools.hasNonBlankValue(key)) { + typeDecl += ", " + quotedTupleOf("key", key); + } + String defaultValue = method.defaultValue().orElse(null); + + if (configuredOptions.isEmpty() && !BuilderTypeTools.hasNonBlankValue(defaultValue)) { + return "Map.of(" + typeDecl + ")"; + } + + needsCustomMapOf.set(true); + StringBuilder result = new StringBuilder(); + result.append("__mapOf(").append(typeDecl); + + if (configuredOptions.isEmpty()) { + if (defaultValue.startsWith("{")) { + defaultValue = "new String[] " + defaultValue; + } + result.append(", "); + result.append(quotedValueOf("value")).append(", ").append(defaultValue); + } else { + configuredOptions.get().values().entrySet().stream() + .filter(e -> BuilderTypeTools.hasNonBlankValue(e.getValue())) + .filter(e -> !e.getKey().equals("key")) + .forEach((e) -> { + result.append(", "); + result.append(quotedTupleOf(e.getKey(), e.getValue())); + }); + } + result.append(")"); + + return result.toString(); + } + + private String quotedTupleOf(String key, + String val) { + assert (Objects.nonNull(key)); + assert (BuilderTypeTools.hasNonBlankValue(val)) : key; + if (key.equals("value") && ConfiguredOption.UNCONFIGURED.equals(val)) { + val = ConfiguredOption.class.getName() + ".UNCONFIGURED"; + } else { + val = quotedValueOf(val); + } + return quotedValueOf(key) + ", " + val; + } + + private String quotedValueOf(String val) { + if (val.startsWith("\"") && val.endsWith("\"")) { + return val; + } + + return "\"" + val + "\""; + } + } diff --git a/pico/builder/processor-tools/src/main/java/io/helidon/pico/builder/processor/tools/GenerateJavadoc.java b/pico/builder/processor-tools/src/main/java/io/helidon/pico/builder/processor/tools/GenerateJavadoc.java new file mode 100644 index 00000000000..0b7fc795fc9 --- /dev/null +++ b/pico/builder/processor-tools/src/main/java/io/helidon/pico/builder/processor/tools/GenerateJavadoc.java @@ -0,0 +1,174 @@ +/* + * 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.pico.builder.processor.tools; + +import io.helidon.pico.types.TypedElementName; + +final class GenerateJavadoc { + private GenerateJavadoc() { + } + + static void typeConstructorWithBuilder(StringBuilder builder) { + builder.append("\n\t/**\n" + + "\t * Constructor using the builder argument.\n" + + "\t *\n" + + "\t * @param b\tthe builder\n" + + "\t */\n"); + } + + static void builderClass(StringBuilder builder, + BodyContext ctx) { + builder.append("\n\t/**\n\t * Fluent API builder for {@code ") + .append(ctx.genericBuilderAcceptAliasDecl()) + .append("}.\n\t *\n"); + if (!ctx.doingConcreteType()) { + builder.append("\t * @param <").append(ctx.genericBuilderAliasDecl()).append(">\tthe type of the builder\n"); + builder.append("\t * @param <").append(ctx.genericBuilderAcceptAliasDecl()) + .append(">\tthe type of the built instance\n"); + } + builder.append("\t */\n"); + } + + static void builderMethod(StringBuilder builder, + BodyContext ctx) { + builder.append("\t/**\n" + + "\t * Creates a builder for this type.\n" + + "\t *\n"); + builder.append("\t * @return A builder for {@link "); + builder.append(ctx.typeInfo().typeName()); + builder.append("}\n\t */\n"); + } + + static void toBuilderMethod(StringBuilder builder, + BodyContext ctx) { + builder.append("\t/**\n" + + "\t * Creates a builder for this type, initialized with the attributes from the values passed" + + ".\n\n"); + builder.append("\t * @param val the value to copy to initialize the builder attributes\n"); + builder.append("\t * @return A builder for {@link ").append(ctx.typeInfo().typeName()); + builder.append("}\n\t */\n"); + } + + static void builderConstructor(StringBuilder builder) { + builder.append("\t\t/**\n" + + "\t\t * Fluent API builder constructor.\n" + + "\t\t */\n"); + } + + static void identity(StringBuilder builder) { + builder.append("\t\t/**\n" + + "\t\t * Instance of this builder as the correct type.\n" + + "\t\t *\n" + + "\t\t * @return this instance typed to correct type\n" + + "\t\t */\n"); + } + + static void accept(StringBuilder builder) { + builder.append("\t\t/**\n" + + "\t\t * Accept and update from the provided value object.\n" + + "\t\t *\n" + + "\t\t * @param val the value object to copy from\n" + + "\t\t * @return this instance typed to correct type\n" + + "\t\t */\n"); + } + + static void buildMethod(StringBuilder builder) { + builder.append("\t\t/**\n" + + "\t\t * Builds the instance.\n" + + "\t\t *\n" + + "\t\t * @return the built instance\n" + + "\t\t * @throws IllegalArgumentException if any required attributes are missing\n" + + "\t\t */\n"); + } + + static void updateConsumer(StringBuilder builder) { + builder.append("\t\t/**\n" + + "\t\t * Update the builder in a fluent API way.\n" + + "\t\t *\n" + + "\t\t * @param consumer consumer of the builder instance\n" + + "\t\t * @return updated builder instance\n" + + "\t\t */\n"); + } + + static void builderField(StringBuilder builder, + TypedElementName method) { + builder.append("\t\t/**\n" + "\t\t * Field value for {@code ") + .append(method) + .append("()}.\n\t\t */\n"); + } + + static void innerToString(StringBuilder builder) { + builder.append("\t/**\n" + + "\t * Produces the inner portion of the toString() output (i.e., what is between the parens).\n" + + "\t *\n" + + "\t * @return portion of the toString output\n" + + "\t */\n"); + } + + static void setter(StringBuilder builder, + String beanAttributeName, + TypedElementName method) { + builder.append("\t\t/**\n"); + builder.append("\t\t * Setter for '").append(beanAttributeName).append("'.\n"); + builder.append("\t\t *\n"); + builder.append("\t\t * @param val the new value\n"); + builder.append("\t\t * @return this fluent builder\n"); + builder.append("\t\t * @see #").append(method.elementName()).append("()\n"); + builder.append("\t\t */\n"); + } + + static void singularSetter(StringBuilder builder, + TypedElementName method, + String beanAttributeName) { + builder.append("\t\t/**\n"); + builder.append("\t\t * Singular setter for '").append(beanAttributeName).append("'.\n"); + builder.append("\t\t *\n"); + if (method.typeName().isMap()) { + builder.append("\t\t * @param key the key\n"); + } + builder.append("\t\t * @param val the new value\n"); + builder.append("\t\t * @return this fluent builder\n"); + builder.append("\t\t * @see #").append(method.elementName()).append("()\n"); + builder.append("\t\t */\n"); + } + + static void internalMetaAttributes(StringBuilder builder) { + builder.append("\t/**\n" + + "\t * The map of meta attributes describing each element of this type.\n" + + "\t *\n" + + "\t * @return the map of meta attributes using the key being the attribute name\n" + + "\t */\n"); + } + + static void internalMetaPropsField(StringBuilder builder) { + builder.append("\t/**\n" + + "\t * Meta properties, statically cached.\n" + + "\t */\n"); + } + + static void visitAttributes(StringBuilder builder, + BodyContext ctx, + String extraTabs) { + builder.append(extraTabs).append("\t/**\n"); + builder.append(extraTabs).append("\t * Visits all attributes of " + ctx.typeInfo().typeName() + ", calling the {@link " + + "AttributeVisitor} for each.\n"); + builder.append(extraTabs).append("\t *\n"); + builder.append(extraTabs).append("\t * @param visitor\t\t\tthe visitor called for each attribute\n"); + builder.append(extraTabs).append("\t * @param userDefinedCtx\tany object you wish to pass to each visit call\n"); + builder.append(extraTabs).append("\t */\n"); + } +} diff --git a/pico/builder/processor-tools/src/main/java/io/helidon/pico/builder/processor/tools/GenerateMethod.java b/pico/builder/processor-tools/src/main/java/io/helidon/pico/builder/processor/tools/GenerateMethod.java new file mode 100644 index 00000000000..516b5356b31 --- /dev/null +++ b/pico/builder/processor-tools/src/main/java/io/helidon/pico/builder/processor/tools/GenerateMethod.java @@ -0,0 +1,139 @@ +/* + * 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.pico.builder.processor.tools; + +import java.util.List; +import java.util.Optional; + +import io.helidon.pico.types.TypeName; +import io.helidon.pico.types.TypedElementName; + +final class GenerateMethod { + private GenerateMethod() { + } + + static void builderMethods(StringBuilder builder, + BodyContext ctx) { + GenerateJavadoc.builderMethod(builder, ctx); + builder.append("\tpublic static Builder"); + builder.append(" builder() {\n"); + builder.append("\t\treturn new Builder();\n"); + builder.append("\t}\n\n"); + + GenerateJavadoc.toBuilderMethod(builder, ctx); + builder.append("\tpublic static Builder"); + builder.append(" toBuilder(").append(ctx.ctorBuilderAcceptTypeName()).append(" val) {\n"); + builder.append("\t\tObjects.requireNonNull(val);\n"); + builder.append("\t\treturn builder().accept(val);\n"); + builder.append("\t}\n\n"); + } + + static void stringToCharSetter(StringBuilder builder, + BodyContext ctx, + String beanAttributeName, + TypedElementName method, + String methodName) { + GenerateJavadoc.setter(builder, beanAttributeName, method); + builder.append("\t\tpublic ").append(ctx.genericBuilderAliasDecl()).append(" ").append(methodName) + .append("(String val) {\n"); + builder.append("\t\t\tObjects.requireNonNull(val);\n"); + builder.append("\t\t\treturn this.") + .append(methodName) + .append("(val.toCharArray());\n"); + builder.append("\t\t}\n\n"); + } + + static void internalMetaAttributes(StringBuilder builder) { + GenerateJavadoc.internalMetaAttributes(builder); + builder.append("\tpublic static Map> __metaAttributes() {\n" + + "\t\treturn META_PROPS;\n" + + "\t}\n\n"); + } + + static void nonOptionalSetter(StringBuilder builder, + BodyContext ctx, + String beanAttributeName, + TypedElementName method, + String methodName, + TypeName genericType) { + GenerateJavadoc.setter(builder, beanAttributeName, method); + builder.append("\t\tpublic ") + .append(ctx.genericBuilderAliasDecl()) + .append(" ") + .append(methodName) + .append("(") + .append(genericType.fqName()) + .append(" val) {\n"); + builder.append("\t\t\tObjects.requireNonNull(val);\n"); + builder.append("\t\t\treturn ") + .append(beanAttributeName) + .append("(") + .append(Optional.class.getName()) + .append(".of(val));\n"); + builder.append("\t\t}\n\n"); + + if ("char[]".equals(genericType.fqName())) { + stringToCharSetter(builder, ctx, beanAttributeName, method, methodName); + } + } + + static void singularSetter(StringBuilder builder, + BodyContext ctx, + TypedElementName method, + String beanAttributeName, + char[] methodName) { + GenerateJavadoc.singularSetter(builder, method, beanAttributeName); + + // builder method declaration for "addSomething()" + builder.append("\t\tpublic ") + .append(ctx.genericBuilderAliasDecl()) + .append(" add") + .append(methodName) + .append("(") + .append(toGenericsDecl(method)) + .append(") {\n"); + // body of the method + TypeName typeName = method.typeName(); + if (typeName.isMap()) { + builder.append("\t\t\tObjects.requireNonNull(key);\n"); + } + builder.append("\t\t\tObjects.requireNonNull(val);\n"); + + builder.append("\t\t\tthis.").append(beanAttributeName); + if (typeName.isList() || typeName.isSet()) { + builder.append(".add(val);\n"); + } else { // isMap + builder.append(".put(key, val);\n"); + } + builder.append("\t\t\treturn identity();\n"); + builder.append("\t\t}\n\n"); + } + + private static String toGenericsDecl(TypedElementName method) { + List compTypeNames = method.typeName().typeArguments(); + if (1 == compTypeNames.size()) { + return avoidWildcard(compTypeNames.get(0)) + " val"; + } else if (2 == compTypeNames.size()) { + return avoidWildcard(compTypeNames.get(0)) + " key, " + avoidWildcard(compTypeNames.get(1)) + " val"; + } + return "Object val"; + } + + private static String avoidWildcard(TypeName typeName) { + return typeName.wildcard() ? typeName.name() : typeName.fqName(); + } +} diff --git a/pico/builder/processor-tools/src/main/java/io/helidon/pico/builder/processor/tools/GenerateVisitor.java b/pico/builder/processor-tools/src/main/java/io/helidon/pico/builder/processor/tools/GenerateVisitor.java new file mode 100644 index 00000000000..9494351394d --- /dev/null +++ b/pico/builder/processor-tools/src/main/java/io/helidon/pico/builder/processor/tools/GenerateVisitor.java @@ -0,0 +1,97 @@ +/* + * 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.pico.builder.processor.tools; + +final class GenerateVisitor { + private GenerateVisitor() { + } + + static void appendAttributeVisitors(StringBuilder builder, + BodyContext ctx) { + if (ctx.doingConcreteType()) { + return; + } + + if (!ctx.hasParent() + && ctx.includeMetaAttributes() + && !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"); + builder.append("\t@FunctionalInterface\n" + + "\tpublic static interface AttributeVisitor {\n" + + "\t\t/**\n" + + "\t\t * Visits the attribute named 'attrName'.\n" + + "\t\t *\n" + + "\t\t * @param attrName\t\tthe attribute name\n" + + "\t\t * @param valueSupplier\tthe attribute value supplier\n" + + "\t\t * @param meta\t\t\tthe meta information for the attribute\n" + + "\t\t * @param userDefinedCtx a user defined context that can be used for holding an " + + "object of your choosing\n" + + "\t\t * @param type\t\t\tthe type of the attribute\n" + + "\t\t * @param typeArgument\tthe type arguments (if type is a parameterized / generic " + + "type)\n" + + "\t\t */\n" + + "\t\tvoid visit(String attrName, Supplier valueSupplier, " + + "Map meta, Object userDefinedCtx, Class " + + "type, Class... typeArgument);\n" + + "\t}"); + + builder.append("\n\n\t/**\n" + + "\t * An implementation of {@link AttributeVisitor} that will validate each attribute to " + + "enforce not-null. The source\n" + + "\t * must be annotated with {@code ConfiguredOption(required=true)} for this to be " + + "enforced.\n" + + "\t */\n"); + builder.append("\tprotected static class RequiredAttributeVisitor implements AttributeVisitor {\n" + + "\t\tprivate final List errors = new java.util.ArrayList();\n" + + "\n" + + "\t\t/**\n" + + "\t\t * Default Constructor.\n" + + "\t\t */\n" + + "\t\tprotected RequiredAttributeVisitor() {\n" + + "\t\t}\n\n"); + builder.append("\t\t@Override\n" + + "\t\tpublic void visit(String attrName,\n" + + "\t\t\t\t\t\t Supplier valueSupplier,\n" + + "\t\t\t\t\t\t Map meta,\n" + + "\t\t\t\t\t\t Object userDefinedCtx,\n" + + "\t\t\t\t\t\t Class type,\n" + + "\t\t\t\t\t\t Class... typeArgument) {\n" + + "\t\t\tboolean required = Boolean.valueOf((String) meta.get(\"required\"));\n" + + "\t\t\tif (!required) {\n" + + "\t\t\t\treturn;\n" + + "\t\t\t}\n" + + "\t\t\t\n" + + "\t\t\tObject val = valueSupplier.get();\n" + + "\t\t\tif (Objects.nonNull(val)) {\n" + + "\t\t\t\treturn;\n" + + "\t\t\t}\n" + + "\t\t\t\n" + + "\t\t\terrors.add(\"'\" + attrName + \"' is a required attribute and should not be null\")" + + ";\n" + + "\t\t}\n" + + "\n" + + "\t\tvoid validate() {\n" + + "\t\t\tif (!errors.isEmpty()) {\n" + + "\t\t\t\tthrow new java.lang.IllegalStateException(String.join(\", \", errors));\n" + + "\t\t\t}\n" + + "\t\t}\n" + + "\t}\n"); + } + } +} diff --git a/pico/builder/processor-tools/src/main/java/module-info.java b/pico/builder/processor-tools/src/main/java/module-info.java index 96284d61493..b8c9a9a1f67 100644 --- a/pico/builder/processor-tools/src/main/java/module-info.java +++ b/pico/builder/processor-tools/src/main/java/module-info.java @@ -15,7 +15,7 @@ */ /** - * The Pico Builder tools module. + * The Pico Builder Processor Tools module. */ module io.helidon.pico.builder.processor.tools { requires java.compiler; diff --git a/pico/builder/processor/src/main/java/io/helidon/pico/builder/processor/BuilderProcessor.java b/pico/builder/processor/src/main/java/io/helidon/pico/builder/processor/BuilderProcessor.java index ff372c2c5da..8b4010360f4 100644 --- a/pico/builder/processor/src/main/java/io/helidon/pico/builder/processor/BuilderProcessor.java +++ b/pico/builder/processor/src/main/java/io/helidon/pico/builder/processor/BuilderProcessor.java @@ -19,12 +19,10 @@ import java.io.IOException; import java.io.Writer; import java.lang.annotation.Annotation; -import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.ServiceLoader; import java.util.Set; @@ -37,6 +35,7 @@ import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; import javax.lang.model.element.TypeElement; +import javax.lang.model.util.Elements; import javax.tools.Diagnostic; import javax.tools.JavaFileObject; @@ -55,16 +54,16 @@ * The processor for handling any annotation having a {@link io.helidon.pico.builder.BuilderTrigger}. */ public class BuilderProcessor extends AbstractProcessor { - private static final System.Logger LOGGER = System.getLogger(BuilderProcessor.class.getName()); - - private TypeInfoCreator tools; - private final LinkedHashSet elementsProcessed = new LinkedHashSet<>(); - private static final Map> PRODUCERS_BY_ANNOTATION = new LinkedHashMap<>(); private static final Set> ALL_ANNO_TYPES_HANDLED = new LinkedHashSet<>(); private static final List PRODUCERS = initialize(); + private final LinkedHashSet elementsProcessed = new LinkedHashSet<>(); + + private TypeInfoCreator tools; + private Elements elementUtils; + /** * Default constructor. */ @@ -72,41 +71,29 @@ public class BuilderProcessor extends AbstractProcessor { public BuilderProcessor() { } - private static List initialize() { - try { - // note: it is important to use this class' CL since maven will not give us the "right" one. - List producers = HelidonServiceLoader - .create(ServiceLoader.load(BuilderCreator.class, BuilderCreator.class.getClassLoader())) - .asList(); - producers.forEach(producer -> { - producer.supportedAnnotationTypes().forEach(annoType -> { - PRODUCERS_BY_ANNOTATION.compute(DefaultTypeName.create(annoType), (k, v) -> { - if (Objects.isNull(v)) { - v = new LinkedHashSet<>(); - } - v.add(producer); - return v; - }); - }); - }); - producers.sort(Weights.weightComparator()); - producers.forEach(p -> ALL_ANNO_TYPES_HANDLED.addAll(p.supportedAnnotationTypes())); - return producers; - } catch (Throwable t) { - RuntimeException e = new RuntimeException("Failed to initialize", t); - LOGGER.log(System.Logger.Level.ERROR, e.getMessage(), e); - throw e; - } + @Override + public Set getSupportedAnnotationTypes() { + return ALL_ANNO_TYPES_HANDLED.stream().map(Class::getName).collect(Collectors.toSet()); + } + + @Override + public SourceVersion getSupportedSourceVersion() { + return SourceVersion.latestSupported(); } @Override public void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); - tools = HelidonServiceLoader.create( + this.elementUtils = processingEnv.getElementUtils(); + this.tools = HelidonServiceLoader.create( ServiceLoader.load(TypeInfoCreator.class, TypeInfoCreator.class.getClassLoader())) - .asList().stream().findFirst().orElse(null); - if (Objects.isNull(tools)) { + .asList() + .stream() + .findFirst() + .orElse(null); + + if (tools == null) { String msg = "no available " + TypeInfoCreator.class.getSimpleName() + " instances found"; processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, msg); throw new IllegalStateException(msg); @@ -121,24 +108,9 @@ public void init(ProcessingEnvironment processingEnv) { LOGGER.log(System.Logger.Level.DEBUG, BuilderCreator.class.getSimpleName() + "s: " + PRODUCERS); } - Set getProducersForType(TypeName annoTypeName) { - Set set = PRODUCERS_BY_ANNOTATION.get(annoTypeName); - return Objects.isNull(set) ? null : Collections.unmodifiableSet(set); - } - - @Override - public SourceVersion getSupportedSourceVersion() { - return SourceVersion.latestSupported(); - } - - @Override - public Set getSupportedAnnotationTypes() { - return ALL_ANNO_TYPES_HANDLED.stream().map(Class::getName).collect(Collectors.toSet()); - } - @Override public boolean process(Set annotations, RoundEnvironment roundEnv) { - if (roundEnv.processingOver() || Objects.isNull(tools) || PRODUCERS.isEmpty()) { + if (roundEnv.processingOver() || tools == null || PRODUCERS.isEmpty()) { elementsProcessed.clear(); return false; } @@ -170,18 +142,21 @@ public boolean process(Set annotations, RoundEnvironment /** * Process the annotation for the given element. * - * @param annoType the annotation that triggered processing - * @param element the element being processed + * @param annoType the annotation that triggered processing + * @param element the element being processed * @throws IOException if unable to write the generated class */ protected void process(Class annoType, Element element) throws IOException { AnnotationMirror am = BuilderTypeTools.findAnnotationMirror(annoType.getName(), - element.getAnnotationMirrors()).orElse(null); + element.getAnnotationMirrors()) + .orElseThrow(() -> new IllegalArgumentException("Cannot find annotation mirror for " + annoType + + " on " + element)); + AnnotationAndValue builderAnnotation = BuilderTypeTools - .createAnnotationAndValueFromMirror(am, processingEnv.getElementUtils()).get(); + .createAnnotationAndValueFromMirror(am, elementUtils).get(); TypeName typeName = BuilderTypeTools.createTypeNameFromElement(element).orElse(null); - TypeInfo typeInfo = tools.createTypeInfo(builderAnnotation, typeName, (TypeElement) element, processingEnv).orElse(null); - if (Objects.isNull(typeInfo)) { + Optional typeInfo = tools.createTypeInfo(builderAnnotation, typeName, (TypeElement) element, processingEnv); + if (typeInfo.isEmpty()) { String msg = "Nothing to process, skipping: " + element; LOGGER.log(System.Logger.Level.WARNING, msg); processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, msg); @@ -190,7 +165,7 @@ protected void process(Class annoType, Element element) th Set creators = getProducersForType(DefaultTypeName.create(annoType)); Optional> result = creators.stream() - .map(it -> it.create(typeInfo, builderAnnotation)) + .map(it -> it.create(typeInfo.get(), builderAnnotation)) .filter(it -> !it.isEmpty()) .findFirst(); if (result.isEmpty()) { @@ -205,7 +180,7 @@ protected void process(Class annoType, Element element) th /** * Performs the actual code generation of the given type and body model object. * - * @param codegens the model objects to be code generated + * @param codegens the model objects to be code generated * @throws IOException if unable to write the generated class */ protected void codegen(List codegens) throws IOException { @@ -217,4 +192,31 @@ protected void codegen(List codegens) throws IOException { } } + private static List initialize() { + try { + // note: it is important to use this class' CL since maven will not give us the "right" one. + List producers = HelidonServiceLoader + .create(ServiceLoader.load(BuilderCreator.class, BuilderCreator.class.getClassLoader())) + .asList(); + producers.forEach(producer -> { + producer.supportedAnnotationTypes().forEach(annoType -> { + PRODUCERS_BY_ANNOTATION.computeIfAbsent(DefaultTypeName.create(annoType), it -> new LinkedHashSet<>()) + .add(producer); + }); + }); + producers.sort(Weights.weightComparator()); + producers.forEach(p -> ALL_ANNO_TYPES_HANDLED.addAll(p.supportedAnnotationTypes())); + return producers; + } catch (Throwable t) { + RuntimeException e = new RuntimeException("Failed to initialize", t); + LOGGER.log(System.Logger.Level.ERROR, e.getMessage(), e); + throw e; + } + } + + private Set getProducersForType(TypeName annoTypeName) { + Set set = PRODUCERS_BY_ANNOTATION.get(annoTypeName); + return set == null ? Set.of() : Set.copyOf(set); + } + } diff --git a/pico/builder/processor/src/main/java/module-info.java b/pico/builder/processor/src/main/java/module-info.java index 478fc5e711c..7bf5c9db3ae 100644 --- a/pico/builder/processor/src/main/java/module-info.java +++ b/pico/builder/processor/src/main/java/module-info.java @@ -28,4 +28,7 @@ exports io.helidon.pico.builder.processor; provides javax.annotation.processing.Processor with io.helidon.pico.builder.processor.BuilderProcessor; + + uses io.helidon.pico.builder.processor.spi.BuilderCreator; + uses io.helidon.pico.builder.processor.spi.TypeInfoCreator; } diff --git a/pico/builder/tests/builder/pom.xml b/pico/builder/tests/builder/pom.xml index aee9c1cb826..4efc5f7ad57 100644 --- a/pico/builder/tests/builder/pom.xml +++ b/pico/builder/tests/builder/pom.xml @@ -79,6 +79,11 @@ junit-jupiter-api test + + io.helidon.common.testing + helidon-common-testing-junit5 + test + diff --git a/pico/builder/tests/builder/src/main/java/io/helidon/pico/builder/test/testsubjects/ChildInterfaceIsABuilder.java b/pico/builder/tests/builder/src/main/java/io/helidon/pico/builder/test/testsubjects/ChildInterfaceIsABuilder.java index 391cad89a97..7191b9ec576 100644 --- a/pico/builder/tests/builder/src/main/java/io/helidon/pico/builder/test/testsubjects/ChildInterfaceIsABuilder.java +++ b/pico/builder/tests/builder/src/main/java/io/helidon/pico/builder/test/testsubjects/ChildInterfaceIsABuilder.java @@ -16,6 +16,8 @@ package io.helidon.pico.builder.test.testsubjects; +import java.util.Optional; + import io.helidon.config.metadata.ConfiguredOption; import io.helidon.pico.builder.Builder; @@ -49,6 +51,15 @@ public interface ChildInterfaceIsABuilder extends ParentInterfaceNotABuilder { */ @Override @ConfiguredOption("override") + Optional maybeOverrideMe(); + + /** + * Used for testing {@link io.helidon.config.metadata.ConfiguredOption} default values. + * + * @return ignored, here for testing purposes only + */ + @Override + @ConfiguredOption("override2") char[] overrideMe(); } diff --git a/pico/builder/tests/builder/src/main/java/io/helidon/pico/builder/test/testsubjects/Level1.java b/pico/builder/tests/builder/src/main/java/io/helidon/pico/builder/test/testsubjects/Level1.java index 93f32ad4c2f..c4c3518d990 100644 --- a/pico/builder/tests/builder/src/main/java/io/helidon/pico/builder/test/testsubjects/Level1.java +++ b/pico/builder/tests/builder/src/main/java/io/helidon/pico/builder/test/testsubjects/Level1.java @@ -16,6 +16,8 @@ package io.helidon.pico.builder.test.testsubjects; +import java.util.Optional; + import io.helidon.config.metadata.ConfiguredOption; import io.helidon.pico.builder.Builder; @@ -65,6 +67,6 @@ public interface Level1 extends Level0 { * * @return ignored, here for testing purposes only */ - Boolean getLevel1BooleanAttribute(); + Optional getLevel1BooleanAttribute(); } diff --git a/pico/builder/tests/builder/src/main/java/io/helidon/pico/builder/test/testsubjects/ParentInterfaceNotABuilder.java b/pico/builder/tests/builder/src/main/java/io/helidon/pico/builder/test/testsubjects/ParentInterfaceNotABuilder.java index bb23617d13b..fed89f82d90 100644 --- a/pico/builder/tests/builder/src/main/java/io/helidon/pico/builder/test/testsubjects/ParentInterfaceNotABuilder.java +++ b/pico/builder/tests/builder/src/main/java/io/helidon/pico/builder/test/testsubjects/ParentInterfaceNotABuilder.java @@ -16,6 +16,8 @@ package io.helidon.pico.builder.test.testsubjects; +import java.util.Optional; + /** * Used for demonstrating (and testing) multi-inheritance of interfaces and the builders that are produced. * @@ -29,13 +31,22 @@ public interface ParentInterfaceNotABuilder extends ParentOfParentInterfaceIsABu default void ignoreMe() { } + /** + * The Pico Builder will ignore {@code default} and {@code static} functions. + * + * @return ignored, here for testing purposes only + */ + default Optional maybeOverrideMe() { + return Optional.empty(); + } + /** * The Pico Builder will ignore {@code default} and {@code static} functions. * * @return ignored, here for testing purposes only */ default char[] overrideMe() { - return new char[] {}; + return "default".toCharArray(); } /** diff --git a/pico/builder/tests/builder/src/test/java/io/helidon/pico/builder/api/test/ComplexCaseTest.java b/pico/builder/tests/builder/src/test/java/io/helidon/pico/builder/api/test/ComplexCaseTest.java index a6044c2d10b..1d2470e037f 100644 --- a/pico/builder/tests/builder/src/test/java/io/helidon/pico/builder/api/test/ComplexCaseTest.java +++ b/pico/builder/tests/builder/src/test/java/io/helidon/pico/builder/api/test/ComplexCaseTest.java @@ -17,8 +17,12 @@ package io.helidon.pico.builder.api.test; import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import io.helidon.pico.builder.test.testsubjects.ComplexCaseImpl; +import io.helidon.pico.builder.test.testsubjects.MyConfigBean; import org.junit.jupiter.api.Test; @@ -29,15 +33,17 @@ class ComplexCaseTest { @Test void testIt() { + Map> mapWithNull = new HashMap<>(); + mapWithNull.put("key", null); + ComplexCaseImpl val = ComplexCaseImpl.builder() .name("name") - .addConfigBean(null) - .addKeyToConfigBean("key", null) + .mapOfKeyToConfigBeans(mapWithNull) .setOfLists(Collections.singleton(Collections.singletonList(null))) .build(); assertThat(val.toString(), equalTo("ComplexCase(name=name, enabled=false, port=8080, mapOfKeyToConfigBeans={key=null}, " - + "listOfConfigBeans=[null], setOfLists=[[null]])")); + + "listOfConfigBeans=[], setOfLists=[[null]])")); } } diff --git a/pico/builder/tests/builder/src/test/java/io/helidon/pico/builder/api/test/EdgeCasesTest.java b/pico/builder/tests/builder/src/test/java/io/helidon/pico/builder/api/test/EdgeCasesTest.java index bbe3a759187..de91e7d2031 100644 --- a/pico/builder/tests/builder/src/test/java/io/helidon/pico/builder/api/test/EdgeCasesTest.java +++ b/pico/builder/tests/builder/src/test/java/io/helidon/pico/builder/api/test/EdgeCasesTest.java @@ -28,7 +28,7 @@ class EdgeCasesTest { @Test void testIt() { - DefaultEdgeCases val = DefaultEdgeCases.toBuilder(null).build(); + DefaultEdgeCases val = DefaultEdgeCases.builder().build(); assertThat(val.optionalIntegerWithDefault().get(), is(-1)); assertThat(val.optionalStringWithDefault().get(), equalTo("test")); diff --git a/pico/builder/tests/builder/src/test/java/io/helidon/pico/builder/api/test/LevelTest.java b/pico/builder/tests/builder/src/test/java/io/helidon/pico/builder/api/test/LevelTest.java index f03dcee70cf..e876a848dc6 100644 --- a/pico/builder/tests/builder/src/test/java/io/helidon/pico/builder/api/test/LevelTest.java +++ b/pico/builder/tests/builder/src/test/java/io/helidon/pico/builder/api/test/LevelTest.java @@ -50,12 +50,12 @@ void manualGeneric() { .build(); assertThat(val.toString(), equalTo("Level2ManualImpl(level0StringAttribute=a, level1intAttribute=1, level1IntegerAttribute=null, " - + "level1booleanAttribute=false, level1BooleanAttribute=null, " + + "level1booleanAttribute=false, level1BooleanAttribute=Optional.empty, " + "level2Level0Info=[Level0ManualImpl(level0StringAttribute=1)], " + "level2ListOfLevel0s=[Level0ManualImpl(level0StringAttribute=1)], " + "level2MapOfStringToLevel1s={key=Level1ManualImpl(level0StringAttribute=1, " + "level1intAttribute=1, level1IntegerAttribute=1, level1booleanAttribute=true, " - + "level1BooleanAttribute=null)})")); + + "level1BooleanAttribute=Optional.empty)})")); Level2 val2 = Level2ManualImpl.builder() .level0StringAttribute("a") @@ -84,12 +84,12 @@ void manualNonGeneric() { .addStringToLevel1("key", Level1ManualImpl.builder().build()) .build(); assertThat(val.toString(), equalTo("Level2ManualImpl(level0StringAttribute=a, level1intAttribute=1, level1IntegerAttribute=null, " - + "level1booleanAttribute=false, level1BooleanAttribute=null, " + + "level1booleanAttribute=false, level1BooleanAttribute=Optional.empty, " + "level2Level0Info=[Level0ManualImpl(level0StringAttribute=1)], " + "level2ListOfLevel0s=[Level0ManualImpl(level0StringAttribute=1)], " + "level2MapOfStringToLevel1s={key=Level1ManualImpl(level0StringAttribute=1, " + "level1intAttribute=1, level1IntegerAttribute=1, level1booleanAttribute=true, " - + "level1BooleanAttribute=null)})")); + + "level1BooleanAttribute=Optional.empty)})")); Level2 val2 = Level2ManualImpl.toBldr(val).build(); assertThat(val, equalTo(val2)); @@ -100,26 +100,22 @@ void codeGen() { Level2 val = Level2Impl.builder() .level0StringAttribute("a") .level1booleanAttribute(false) - .level1IntegerAttribute(null) - .level2Level0Info(null) .level2Level0Info(Collections.singleton(Level0Impl.builder().build())) .addLevel0(Level0Impl.builder().build()) .addStringToLevel1("key", Level1Impl.builder().build()) .build(); assertThat(val.toString(), - equalTo("Level2(level0StringAttribute=a, level1intAttribute=1, level1IntegerAttribute=null, " - + "level1booleanAttribute=false, level1BooleanAttribute=null, " + equalTo("Level2(level0StringAttribute=a, level1intAttribute=1, level1IntegerAttribute=1, " + + "level1booleanAttribute=false, level1BooleanAttribute=Optional.empty, " + "level2Level0Info=[Level0(level0StringAttribute=1)], " + "level2ListOfLevel0s=[Level0(level0StringAttribute=1)], " + "level2MapOfStringToLevel1s={key=Level1(level0StringAttribute=1, " + "level1intAttribute=1, level1IntegerAttribute=1, level1booleanAttribute=true, " - + "level1BooleanAttribute=null)})")); + + "level1BooleanAttribute=Optional.empty)})")); Level2 val2 = Level2Impl.builder() .level0StringAttribute("a") .level1booleanAttribute(false) - .level1IntegerAttribute(null) - .level2Level0Info(null) .level2Level0Info(Collections.singleton(Level0Impl.builder().build())) .addLevel0(Level0Impl.builder().build()) .addStringToLevel1("key", Level1Impl.builder().build()) @@ -133,8 +129,6 @@ void toBuilderAndEquals() { Level2 val = Level2Impl.builder() .level0StringAttribute("a") .level1booleanAttribute(false) - .level1IntegerAttribute(null) - .level2Level0Info(null) .level2Level0Info(Collections.singleton(Level0Impl.builder().build())) .addLevel0(Level0Impl.builder().build()) .addStringToLevel1("key", Level1Impl.builder().build()) @@ -150,7 +144,7 @@ void streams() { m2.accept(m1.get()); assertThat(m2.build().toString(), equalTo("Level1(level0StringAttribute=hello, level1intAttribute=1, level1IntegerAttribute=1, " - + "level1booleanAttribute=true, level1BooleanAttribute=null)")); + + "level1booleanAttribute=true, level1BooleanAttribute=Optional.empty)")); } @Test @@ -158,13 +152,13 @@ void levelDefaults() { Level2 val2 = Level2Impl.builder().build(); assertThat(val2.toString(), equalTo("Level2(level0StringAttribute=2, level1intAttribute=1, level1IntegerAttribute=1, " - + "level1booleanAttribute=true, level1BooleanAttribute=null, level2Level0Info=[], " + + "level1booleanAttribute=true, level1BooleanAttribute=Optional.empty, level2Level0Info=[], " + "level2ListOfLevel0s=[], level2MapOfStringToLevel1s={})")); Level1 val1 = Level1Impl.builder().build(); assertThat(val1.toString(), equalTo("Level1(level0StringAttribute=1, level1intAttribute=1, level1IntegerAttribute=1, " - + "level1booleanAttribute=true, level1BooleanAttribute=null)")); + + "level1booleanAttribute=true, level1BooleanAttribute=Optional.empty)")); Level0 val0 = Level0Impl.builder().build(); assertThat(val0.toString(), diff --git a/pico/builder/tests/builder/src/test/java/io/helidon/pico/builder/api/test/MyConfigBeanTest.java b/pico/builder/tests/builder/src/test/java/io/helidon/pico/builder/api/test/MyConfigBeanTest.java index 0505dafeb89..9906b54e3f9 100644 --- a/pico/builder/tests/builder/src/test/java/io/helidon/pico/builder/api/test/MyConfigBeanTest.java +++ b/pico/builder/tests/builder/src/test/java/io/helidon/pico/builder/api/test/MyConfigBeanTest.java @@ -60,7 +60,7 @@ void codeGen() { @Test void mixed() { - MyConfigBean val1 = MyConfigBeanManualImpl.builder().build(); + MyConfigBean val1 = MyConfigBeanManualImpl.builder().name("initial").build(); val1 = MyConfigBeanImpl.toBuilder(val1) .name("test") .enabled(true) diff --git a/pico/builder/tests/builder/src/test/java/io/helidon/pico/builder/api/test/ParentParentChildTest.java b/pico/builder/tests/builder/src/test/java/io/helidon/pico/builder/api/test/ParentParentChildTest.java index 1ecf306f2da..d21e019efea 100644 --- a/pico/builder/tests/builder/src/test/java/io/helidon/pico/builder/api/test/ParentParentChildTest.java +++ b/pico/builder/tests/builder/src/test/java/io/helidon/pico/builder/api/test/ParentParentChildTest.java @@ -17,12 +17,14 @@ package io.helidon.pico.builder.api.test; import java.net.URI; +import java.util.Optional; import io.helidon.pico.builder.test.testsubjects.ChildInterfaceIsABuilder; import io.helidon.pico.builder.test.testsubjects.ChildInterfaceIsABuilderImpl; import org.junit.jupiter.api.Test; +import static io.helidon.common.testing.junit5.OptionalMatcher.optionalEmpty; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; @@ -35,9 +37,9 @@ void collapsedMiddleType() { .childLevel(100) .parentLevel(99) .uri(URI.create("http://localhost")) - .empty((String) null) + .empty(Optional.empty()) .build(); - assertThat(new String(child.overrideMe()), equalTo("override")); + assertThat(new String(child.maybeOverrideMe().get()), equalTo("override")); assertThat(child.uri().get().toString(), equalTo("http://localhost")); assertThat(child.empty().isEmpty(), is(true)); assertThat(child.childLevel(), is(100L)); @@ -51,16 +53,22 @@ void collapsedMiddleType() { @Test void ensureCharArraysAreHiddenFromToStringOutput() { ChildInterfaceIsABuilderImpl val = ChildInterfaceIsABuilderImpl.builder() - .overrideMe("password") .build(); assertThat(val.toString(), - equalTo("ChildInterfaceIsABuilder(uri=null, empty=null, parentLevel=0, childLevel=0, isChildLevel=true, " - + "overrideMe=not-null)")); + equalTo("ChildInterfaceIsABuilder(uri=Optional.empty, empty=Optional.empty, parentLevel=0, childLevel=0, " + + "isChildLevel=true, maybeOverrideMe=not-empty, overrideMe=not-null)")); + assertThat(val.overrideMe(), equalTo("override2".toCharArray())); + assertThat(val.maybeOverrideMe().orElseThrow(), equalTo("override".toCharArray())); - val = ChildInterfaceIsABuilderImpl.toBuilder(val).overrideMe((char[]) null).build(); + val = ChildInterfaceIsABuilderImpl.toBuilder(val) + .maybeOverrideMe(Optional.empty()) + .overrideMe("pwd") + .build(); assertThat(val.toString(), - equalTo("ChildInterfaceIsABuilder(uri=null, empty=null, parentLevel=0, childLevel=0, isChildLevel=true, " - + "overrideMe=null)")); + equalTo("ChildInterfaceIsABuilder(uri=Optional.empty, empty=Optional.empty, parentLevel=0, childLevel=0, " + + "isChildLevel=true, maybeOverrideMe=Optional.empty, overrideMe=not-null)")); + assertThat(val.overrideMe(), equalTo("pwd".toCharArray())); + assertThat(val.maybeOverrideMe(), optionalEmpty()); } } diff --git a/pico/builder/tests/builder/src/test/java/io/helidon/pico/builder/api/test/PickleBarrelTest.java b/pico/builder/tests/builder/src/test/java/io/helidon/pico/builder/api/test/PickleBarrelTest.java index f62cbca0b28..96efe668b59 100644 --- a/pico/builder/tests/builder/src/test/java/io/helidon/pico/builder/api/test/PickleBarrelTest.java +++ b/pico/builder/tests/builder/src/test/java/io/helidon/pico/builder/api/test/PickleBarrelTest.java @@ -34,7 +34,7 @@ class PickleBarrelTest { @Test void testIt() { DefaultPickle.Builder pickleBuilder = DefaultPickle.builder().size(Optional.of(Pickle.Size.MEDIUM)); - AssertionError e = assertThrows(AssertionError.class, pickleBuilder::build); + Exception e = assertThrows(IllegalStateException.class, pickleBuilder::build); assertThat(e.getMessage(), equalTo("'type' is a required attribute and should not be null")); @@ -44,12 +44,12 @@ void testIt() { equalTo("Pickle(type=DILL, size=Optional[MEDIUM])")); DefaultPickleBarrel.Builder pickleBarrelBuilder = DefaultPickleBarrel.builder(); - e = assertThrows(AssertionError.class, pickleBarrelBuilder::build); + e = assertThrows(IllegalStateException.class, pickleBarrelBuilder::build); assertThat(e.getMessage(), equalTo("'id' is a required attribute and should not be null")); PickleBarrel pickleBarrel = pickleBarrelBuilder.addPickle(pickle).id("123").build(); assertThat(pickleBarrel.toString(), - equalTo("PickleBarrel(id=123, type=null, pickles=[Pickle(type=DILL, size=Optional[MEDIUM])])")); + equalTo("PickleBarrel(id=123, type=Optional.empty, pickles=[Pickle(type=DILL, size=Optional[MEDIUM])])")); } } diff --git a/pico/builder/tests/builder/src/test/java/io/helidon/pico/builder/api/test/testsubjects/Level1ManualImpl.java b/pico/builder/tests/builder/src/test/java/io/helidon/pico/builder/api/test/testsubjects/Level1ManualImpl.java index 8225083b2b0..0bdd3c11b3e 100644 --- a/pico/builder/tests/builder/src/test/java/io/helidon/pico/builder/api/test/testsubjects/Level1ManualImpl.java +++ b/pico/builder/tests/builder/src/test/java/io/helidon/pico/builder/api/test/testsubjects/Level1ManualImpl.java @@ -17,6 +17,7 @@ package io.helidon.pico.builder.api.test.testsubjects; import java.util.Objects; +import java.util.Optional; import io.helidon.pico.builder.test.testsubjects.Level1; @@ -119,8 +120,8 @@ public boolean getLevel1booleanAttribute() { * @return ignored, here for testing only */ @Override - public Boolean getLevel1BooleanAttribute() { - return level1BooleanAttribute; + public Optional getLevel1BooleanAttribute() { + return Optional.ofNullable(level1BooleanAttribute); } /** @@ -176,7 +177,7 @@ private void acceptThis(T val) { this.level1intAttribute = val.getLevel1intAttribute(); this.level1IntegerAttribute = val.getLevel1IntegerAttribute(); this.level1booleanAttribute = val.getLevel1booleanAttribute(); - this.level1BooleanAttribute = val.getLevel1BooleanAttribute(); + val.getLevel1BooleanAttribute().ifPresent(this::level1BooleanAttribute); } /** diff --git a/pico/builder/tests/builder/src/test/java/io/helidon/pico/builder/api/test/testsubjects/MyConfigBeanManualImpl.java b/pico/builder/tests/builder/src/test/java/io/helidon/pico/builder/api/test/testsubjects/MyConfigBeanManualImpl.java index c2584d59cb9..36762d30918 100644 --- a/pico/builder/tests/builder/src/test/java/io/helidon/pico/builder/api/test/testsubjects/MyConfigBeanManualImpl.java +++ b/pico/builder/tests/builder/src/test/java/io/helidon/pico/builder/api/test/testsubjects/MyConfigBeanManualImpl.java @@ -60,10 +60,8 @@ public String toString() { @Override public int hashCode() { - int hashCode = 0; // super.hashCode(); - hashCode ^= Objects.hashCode(getName()); - hashCode ^= Objects.hashCode(isEnabled()); - hashCode ^= Objects.hashCode(getPort()); + int hashCode = 1; // super.hashCode(); + hashCode = 31 * hashCode + Objects.hash(getName(), isEnabled(), getPort()); return hashCode; } diff --git a/pico/pico/README.md b/pico/pico/README.md new file mode 100644 index 00000000000..c9d39785dca --- /dev/null +++ b/pico/pico/README.md @@ -0,0 +1,16 @@ +This module contains all the API and SPI types that are applicable to a Helidon Pico based application. + +The API can logically be broken up into two categories - declarative types and imperative/programmatic types. The declarative form is the most common approach for using Pico. + +The declarative API is small and based upon annotations. This is because most of the supporting annotation types actually come directly from both of the standard javax/jakarta inject and javax/jakarta annotation modules. These standard annotations are supplemented with these proprietary annotation types offered here from Pico: + +* [@Contract](src/main/java/io/helidon/pico/Contract.java) +* [@ExteralContract](src/main/java/io/helidon/pico/ExternalContract.java) +* [@RunLevel](src/main/java/io/helidon/pico/RunLevel.java) + +The programmatic API is typically used to manually lookup and activate services (those that are typically annotated with @jakarta.inject.Singleton for example) directly. The main entry points for programmatic access can start from one of these two types: + +* [PicoServices](src/main/java/io/helidon/pico/PicoServices.java) +* [Services](src/main/java/io/helidon/pico/Services.java) + +Note that this module only contains the common types for a Helidon Pico services provider. See the pico-services module for the default reference implementation for this API / SPI. diff --git a/pico/pico/pom.xml b/pico/pico/pom.xml new file mode 100644 index 00000000000..9e4ea08b8ad --- /dev/null +++ b/pico/pico/pom.xml @@ -0,0 +1,114 @@ + + + + + + io.helidon.pico + helidon-pico-project + 4.0.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + helidon-pico + Helidon Pico API / SPI + + + + 11 + + + + + io.helidon.pico + helidon-pico-types + + + io.helidon.common + helidon-common + + + io.helidon.common + helidon-common-config + + + jakarta.inject + jakarta.inject-api + compile + + + io.helidon.config + helidon-config-metadata + provided + true + + + io.helidon.pico.builder + helidon-pico-builder + provided + + + io.helidon.pico.builder + helidon-pico-builder-processor + provided + + + jakarta.annotation + jakarta.annotation-api + provided + + + org.hamcrest + hamcrest-all + test + + + org.junit.jupiter + junit-jupiter-api + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.helidon.pico.builder + helidon-pico-builder-processor + ${helidon.version} + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + true + + + + + diff --git a/pico/pico/src/main/java/io/helidon/pico/ActivationLog.java b/pico/pico/src/main/java/io/helidon/pico/ActivationLog.java new file mode 100644 index 00000000000..0ae772183a5 --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/ActivationLog.java @@ -0,0 +1,48 @@ +/* + * 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.pico; + +import java.util.Optional; + +/** + * Tracks the transformations of {@link ServiceProvider}'s {@link ActivationStatus} in lifecycle activity (i.e., activation + * startup and deactivation shutdown). + * + * @see Activator + * @see DeActivator + */ +public interface ActivationLog { + + /** + * Expected to be called during service creation and activation to capture the activation log transcripts. + * + * @param entry the log entry to record + * @return the (perhaps decorated) activation log entry + */ + ActivationLogEntry recordActivationEvent(ActivationLogEntry entry); + + /** + * Optionally provide a means to query the activation log, if query is possible. If query is not possible then an empty + * will be returned. + * + * @return the optional query API of log activation records + */ + default Optional toQuery() { + return Optional.empty(); + } + +} diff --git a/pico/pico/src/main/java/io/helidon/pico/ActivationLogEntry.java b/pico/pico/src/main/java/io/helidon/pico/ActivationLogEntry.java new file mode 100644 index 00000000000..3daacb8b4da --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/ActivationLogEntry.java @@ -0,0 +1,113 @@ +/* + * 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.pico; + +import java.time.Instant; +import java.util.Optional; + +import io.helidon.pico.builder.Builder; + +/** + * Log entry for lifecycle related events (i.e., activation startup and deactivation shutdown). + * + * @see ActivationLog + * @see Activator + * @see DeActivator + * @param the service type + */ +@Builder +public interface ActivationLogEntry { + + /** + * The activation event. + */ + enum Event { + /** + * Starting. + */ + STARTING, + + /** + * Finished. + */ + FINISHED + } + + /** + * The managing service provider. + * + * @return the managing service provider + */ + ServiceProvider serviceProvider(); + + /** + * The event. + * + * @return the event + */ + Event event(); + + /** + * The starting activation phase. + * + * @return the starting activation phase + */ + ActivationPhase startingActivationPhase(); + + /** + * The eventual/desired/target activation phase. + * + * @return the eventual/desired/target activation phase + */ + ActivationPhase targetActivationPhase(); + + /** + * The finishing phase at the time of this event's log entry. + * + * @return the actual finishing phase + */ + ActivationPhase finishingActivationPhase(); + + /** + * The finishing activation status at the time of this event's log entry. + * + * @return the activation status + */ + ActivationStatus finishingStatus(); + + /** + * The time this event was generated. + * + * @return the time of the event + */ + Instant time(); + + /** + * Any observed error during activation. + * + * @return any observed error + */ + Optional error(); + + /** + * The thread id that the event occurred on. + * + * @return the thread id + */ + long threadId(); + +} diff --git a/pico/pico/src/main/java/io/helidon/pico/ActivationLogQuery.java b/pico/pico/src/main/java/io/helidon/pico/ActivationLogQuery.java new file mode 100644 index 00000000000..bf43e8aad96 --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/ActivationLogQuery.java @@ -0,0 +1,64 @@ +/* + * 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.pico; + +import java.util.List; + +/** + * Provide a means to query the activation log. + * + * @see ActivationLog + */ +public interface ActivationLogQuery { + + /** + * Clears the activation log. + */ + void clear(); + + /** + * The full transcript of all services phase transitions being managed. + * + * @return the activation log if log capture is enabled + */ + List> fullActivationLog(); + + /** + * A filtered list only including service providers. + * + * @param serviceProviders the filter + * @return the filtered activation log if log capture is enabled + */ + List> serviceProviderActivationLog(ServiceProvider... serviceProviders); + + /** + * A filtered list only including service providers. + * + * @param serviceTypeNames the filter + * @return the filtered activation log if log capture is enabled + */ + List> serviceProviderActivationLog(String... serviceTypeNames); + + /** + * A filtered list only including service providers. + * + * @param instances the filter + * @return the filtered activation log if log capture is enabled + */ + List> managedServiceActivationLog(Object... instances); + +} diff --git a/pico/pico/src/main/java/io/helidon/pico/ActivationPhase.java b/pico/pico/src/main/java/io/helidon/pico/ActivationPhase.java new file mode 100644 index 00000000000..325eef96edd --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/ActivationPhase.java @@ -0,0 +1,98 @@ +/* + * 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.pico; + +/** + * Forms a progression of full activation and deactivation. + */ +public enum ActivationPhase { + + /** + * Starting state before anything happens activation-wise. + */ + INIT(false), + + /** + * Planned to be activated. + */ + PENDING(true), + + /** + * Starting to be activated. + */ + ACTIVATION_STARTING(true), + + /** + * Gathering dependencies. + */ + GATHERING_DEPENDENCIES(true), + + /** + * Constructing. + */ + CONSTRUCTING(true), + + /** + * Injecting (fields then methods). + */ + INJECTING(true), + + /** + * Calling any post construct method. + */ + POST_CONSTRUCTING(true), + + /** + * Finishing post construct method. + */ + ACTIVATION_FINISHING(true), + + /** + * Service is active. + */ + ACTIVE(true), + + /** + * About to call pre-destroy. + */ + PRE_DESTROYING(false), + + /** + * Destroyed (after calling any pre-destroy). + */ + DESTROYED(false); + + /** + * True if this phase is eligible for deactivation/shutdown. + */ + private final boolean eligibleForDeactivation; + + /** + * Determines whether this phase passes the gate for whether deactivation (PreDestroy) can be called. + * + * @return true if this phase is eligible to be included in shutdown processing. + * + * @see PicoServices#shutdown() + */ + public boolean eligibleForDeactivation() { + return eligibleForDeactivation; + } + + ActivationPhase(boolean eligibleForDeactivation) { + this.eligibleForDeactivation = eligibleForDeactivation; + } +} diff --git a/pico/pico/src/main/java/io/helidon/pico/ActivationRequest.java b/pico/pico/src/main/java/io/helidon/pico/ActivationRequest.java new file mode 100644 index 00000000000..daecb5267cf --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/ActivationRequest.java @@ -0,0 +1,59 @@ +/* + * 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.pico; + +import java.util.Optional; + +import io.helidon.config.metadata.ConfiguredOption; +import io.helidon.pico.builder.Builder; + +/** + * Request to activate a service. + * + * @param service type + */ +@Builder +public interface ActivationRequest { + /** + * Target service provider. + * + * @return service provider + */ + ServiceProvider serviceProvider(); + + /** + * Injection point context information. + * + * @return injection point info + */ + Optional injectionPoint(); + + /** + * Ultimate target phase for activation. + * + * @return phase to target + */ + ActivationPhase targetPhase(); + + /** + * Whether to throw an exception on failure to activate, or return an error activation result on activation. + * + * @return whether to throw on failure + */ + @ConfiguredOption("true") + boolean throwOnFailure(); +} diff --git a/pico/pico/src/main/java/io/helidon/pico/ActivationResult.java b/pico/pico/src/main/java/io/helidon/pico/ActivationResult.java new file mode 100644 index 00000000000..bd0d600c1af --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/ActivationResult.java @@ -0,0 +1,133 @@ +/* + * 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.pico; + +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.Future; + +import io.helidon.pico.builder.Builder; + +/** + * Represents the result of a service activation or deactivation. + * + * @see Activator + * @see DeActivator + * + * @param The type of the associated activator + */ +@Builder +public interface ActivationResult { + + /** + * The service provider undergoing activation or deactivation. + * + * @return the service provider generating the result + */ + ServiceProvider serviceProvider(); + + /** + * Optionally, given by the implementation provider to indicate the future completion when the provider's + * {@link ActivationStatus} is {@link ActivationStatus#WARNING_SUCCESS_BUT_NOT_READY}. + * + * @return the future result, assuming how activation can be async in nature + */ + Optional>> finishedActivationResult(); + + /** + * The activation phase that was found at onset of the phase transition. + * + * @return the starting phase + */ + ActivationPhase startingActivationPhase(); + + /** + * The activation phase that was requested at the onset of the phase transition. + * + * @return the target, desired, ultimate phase requested + */ + ActivationPhase ultimateTargetActivationPhase(); + + /** + * The activation phase we finished successfully on. + * + * @return the actual finishing phase + */ + ActivationPhase finishingActivationPhase(); + + /** + * How did the activation finish. + * + * @return the finishing status + */ + ActivationStatus finishingStatus(); + + /** + * The containing activation log that tracked this result. + * + * @return the activation log + */ + Optional activationLog(); + + /** + * The services registry that was used. + * + * @return the services registry + */ + Optional services(); + + /** + * Any vendor/provider implementation specific codes. + * + * @return the status code, 0 being the normal/default value + */ + int statusCode(); + + /** + * Any vendor/provider implementation specific description. + * + * @return a developer friendly description (useful if an error occurs) + */ + Optional statusDescription(); + + /** + * Any throwable/exceptions that were observed during activation. + * + * @return the captured error + */ + Optional error(); + + /** + * Returns true if this result is finished. + * + * @return true if finished + */ + default boolean finished() { + Future> f = finishedActivationResult().orElse(null); + return (Objects.isNull(f) || f.isDone()); + } + + /** + * Returns true if this result is successful. + * + * @return true if successful + */ + default boolean success() { + return finishingStatus() != ActivationStatus.FAILURE; + } + +} diff --git a/pico/pico/src/main/java/io/helidon/pico/ActivationStatus.java b/pico/pico/src/main/java/io/helidon/pico/ActivationStatus.java new file mode 100644 index 00000000000..b1e12fb21b7 --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/ActivationStatus.java @@ -0,0 +1,47 @@ +/* + * 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.pico; + +/** + * The activation status. This status applies to the {@link ActivationLogEntry} record. + * + * @see Activator + */ +public enum ActivationStatus { + + /** + * The service has been activated and is fully ready to receive requests. + */ + SUCCESS, + + /** + * The service has been activated but is still being started asynchronously, and is not fully ready yet to receive requests. + * Important note: This is NOT health related - Health is orthogonal to service bindings/activation and readiness. + */ + WARNING_SUCCESS_BUT_NOT_READY, + + /** + * A general warning during lifecycle. + */ + WARNING_GENERAL, + + /** + * Failed to activate to the given phase. + */ + FAILURE + +} diff --git a/pico/pico/src/main/java/io/helidon/pico/Activator.java b/pico/pico/src/main/java/io/helidon/pico/Activator.java new file mode 100644 index 00000000000..4d2c5129641 --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/Activator.java @@ -0,0 +1,55 @@ +/* + * 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.pico; + +/** + * Activators are responsible for lifecycle creation and lazy activation of service providers. They are responsible for taking the + * {@link ServiceProvider}'s manage service instance from {@link ActivationPhase#PENDING} + * through {@link ActivationPhase#POST_CONSTRUCTING} (i.e., including any + * {@link PostConstructMethod} invocations, etc.), and finally into the + * {@link ActivationPhase#ACTIVE} phase. + *

+ * Assumption: + *

    + *
  1. Each {@link ServiceProvider} managing its backing service will have an activator strategy conforming to the DI + * specification.
  2. + *
  3. Each services activation is expected to be non-blocking, but may in fact require deferred blocking activities to become + * fully ready for runtime operation.
  4. + *
+ * Activation includes: + *
    + *
  1. Management of the service's {@link ActivationPhase}.
  2. + *
  3. Control over creation (i.e., invoke the constructor non-reflectively).
  4. + *
  5. Control over gathering the service requisite dependencies (ctor, field, setters) and optional activation of those.
  6. + *
  7. Invocation of any {@link PostConstructMethod}.
  8. + *
  9. Responsible to logging to the {@link ActivationLog} - see {@link PicoServices#activationLog()}.
  10. + *
+ * + * @param the managed service type being activated + * @see DeActivator + */ +@Contract +public interface Activator { + + /** + * Activate a managed service/provider. + * + * @param activationRequest activation request + * @return the result of the activation + */ + ActivationResult activate(ActivationRequest activationRequest); +} diff --git a/pico/pico/src/main/java/io/helidon/pico/Application.java b/pico/pico/src/main/java/io/helidon/pico/Application.java new file mode 100644 index 00000000000..ada9c3d3819 --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/Application.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.pico; + +/** + * An Application instance, if available at runtime, will be expected to provide a blueprint for all service provider's injection + * points. + *

+ * Implementations of this contract are normally code generated, although then can be programmatically written by the developer + * for special cases. + *

+ * Note: instances of this type are not eligible for injection. + * + * @see Module + */ +@Contract +public interface Application extends Named { + + /** + * Called by the provider implementation at bootstrapping time to bind all injection plans to each and every service provider. + * + * @param binder the binder used to register the service provider injection plans + */ + void configure(ServiceInjectionPlanBinder binder); + +} diff --git a/pico/pico/src/main/java/io/helidon/pico/ContextualServiceQuery.java b/pico/pico/src/main/java/io/helidon/pico/ContextualServiceQuery.java new file mode 100644 index 00000000000..40d35485ef8 --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/ContextualServiceQuery.java @@ -0,0 +1,51 @@ +/* + * 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.pico; + +import io.helidon.pico.builder.Builder; + +/** + * Combines the {@link io.helidon.pico.ServiceInfo} criteria along with the {@link io.helidon.pico.InjectionPointInfo} context + * that the query applies to. + * + * @see io.helidon.pico.InjectionPointProvider + */ +@Builder +public interface ContextualServiceQuery { + + /** + * The criteria to use for the lookup into {@link io.helidon.pico.Services}. + * + * @return the service info criteria + */ + ServiceInfoCriteria serviceInfo(); + + /** + * The injection point context this search applies to. + * + * @return the injection point context info + */ + InjectionPointInfo ipInfo(); + + /** + * Set to true if there is an expectation that there is at least one match result from the search. + * + * @return true if it is expected there is at least a single match result + */ + boolean expected(); + +} diff --git a/pico/pico/src/main/java/io/helidon/pico/Contract.java b/pico/pico/src/main/java/io/helidon/pico/Contract.java new file mode 100644 index 00000000000..70c10b499f2 --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/Contract.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.pico; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * The {@code Contract} annotation is used to relay significance to the type. While remaining optional in its use, it is typically + * placed on an interface definition to signify that the given type can be used for lookup in the {@link io.helidon.pico.Services} + * registry, and be eligible for injection via standard {@code @Inject}. While normally places on interface types, it can also be + * placed on other types (e.g., abstract class) as well. The main point is that a contract is the focal point for service lookup + * and injection. + *

+ * If the developer does not have access to the source to place this annotation on the interface definition directly then consider + * using {@link ExternalContracts} instead - this annotation can be placed on the implementation class implementing the given + * {@code Contract} interface(s). + * + * @see io.helidon.pico.ServiceInfo#contractsImplemented() + * @see io.helidon.pico.ServiceInfo#externalContractsImplemented() + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(java.lang.annotation.ElementType.TYPE) +public @interface Contract { + +} diff --git a/pico/pico/src/main/java/io/helidon/pico/DeActivationRequest.java b/pico/pico/src/main/java/io/helidon/pico/DeActivationRequest.java new file mode 100644 index 00000000000..e1e60e8926c --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/DeActivationRequest.java @@ -0,0 +1,55 @@ +/* + * 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.pico; + +import io.helidon.config.metadata.ConfiguredOption; +import io.helidon.pico.builder.Builder; + +/** + * Request to {@link io.helidon.pico.DeActivator#deactivate(DeActivationRequest)}. + * + * @param type to deactivate + */ +@Builder +public interface DeActivationRequest { + /** + * Create a request with defaults. + * + * @param provider service provider responsible for invoking deactivate + * @return a new request + * @param type to deactivate + */ + @SuppressWarnings("unchecked") + static DeActivationRequest create(ServiceProvider provider) { + return DefaultDeActivationRequest.builder().serviceProvider(provider).build(); + } + + /** + * Service provider responsible for invoking deactivate. + * + * @return service provider + */ + ServiceProvider serviceProvider(); + + /** + * Whether to throw an exception on failure, or return it as part of the result. + * + * @return throw on failure + */ + @ConfiguredOption("true") + boolean throwOnFailure(); +} diff --git a/pico/pico/src/main/java/io/helidon/pico/DeActivator.java b/pico/pico/src/main/java/io/helidon/pico/DeActivator.java new file mode 100644 index 00000000000..6978ef3ccae --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/DeActivator.java @@ -0,0 +1,39 @@ +/* + * 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.pico; + +/** + * DeActivators are responsible for lifecycle, transitioning a {@link ServiceProvider} through its + * {@link io.helidon.pico.ActivationPhase}'s, notably including any + * {@link jakarta.annotation.PreDestroy} method invocations, and finally into the terminal + * {@link ActivationPhase#DESTROYED} phase. These phase transitions are the inverse of {@link Activator}. + * + * @param the type to deactivate + * @see Activator + */ +@Contract +public interface DeActivator { + + /** + * Deactivate a managed service. This will trigger any {@link jakarta.annotation.PreDestroy} method on the + * underlying service type instance. + * + * @param request deactivation request + * @return the result + */ + ActivationResult deactivate(DeActivationRequest request); +} diff --git a/pico/pico/src/main/java/io/helidon/pico/DefaultQualifierAndValue.java b/pico/pico/src/main/java/io/helidon/pico/DefaultQualifierAndValue.java new file mode 100644 index 00000000000..b4c22725b25 --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/DefaultQualifierAndValue.java @@ -0,0 +1,171 @@ +/* + * 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.pico; + +import java.lang.annotation.Annotation; +import java.util.Objects; + +import io.helidon.pico.types.AnnotationAndValue; +import io.helidon.pico.types.DefaultAnnotationAndValue; +import io.helidon.pico.types.DefaultTypeName; +import io.helidon.pico.types.TypeName; + +/** + * Describes a {@link jakarta.inject.Qualifier} type annotation associated with a service being provided or dependant upon. + * In Pico these are generally determined at compile time to avoid any use of reflection at runtime. + */ +public class DefaultQualifierAndValue extends DefaultAnnotationAndValue + implements QualifierAndValue, Comparable { + + /** + * Represents a {@link jakarta.inject.Named} type name with no value. + */ + public static final TypeName NAMED = DefaultTypeName.create(Named.class); + + /** + * Represents a wildcard {@link #NAMED} qualifier. + */ + public static final QualifierAndValue WILDCARD_NAMED = DefaultQualifierAndValue.createNamed("*"); + + /** + * Constructor using the builder. + * + * @param b the builder + * @see #builder() + */ + protected DefaultQualifierAndValue(Builder b) { + super(b); + } + + /** + * Creates a {@link jakarta.inject.Named} qualifier. + * + * @param name the name + * @return named qualifier + */ + public static DefaultQualifierAndValue createNamed(String name) { + Objects.requireNonNull(name); + return (DefaultQualifierAndValue) builder().typeName(NAMED).value(name).build(); + } + + /** + * Creates a qualifier from an annotation. + * + * @param qualifierType the qualifier type + * @return qualifier + */ + public static DefaultQualifierAndValue create(Class qualifierType) { + Objects.requireNonNull(qualifierType); + return (DefaultQualifierAndValue) builder().typeName(DefaultTypeName.create(qualifierType)).build(); + } + + /** + * Creates a qualifier. + * + * @param qualifierType the qualifier type + * @param val the value + * @return qualifier + */ + public static DefaultQualifierAndValue create(Class qualifierType, String val) { + Objects.requireNonNull(qualifierType); + return (DefaultQualifierAndValue) builder().typeName(DefaultTypeName.create(qualifierType)).value(val).build(); + } + + /** + * Creates a qualifier. + * + * @param qualifierTypeName the qualifier + * @param val the value + * @return qualifier + */ + public static DefaultQualifierAndValue create(String qualifierTypeName, String val) { + return (DefaultQualifierAndValue) builder() + .typeName(DefaultTypeName.createFromTypeName(qualifierTypeName)) + .value(val) + .build(); + } + + /** + * Creates a qualifier. + * + * @param qualifierType the qualifier + * @param val the value + * @return qualifier + */ + public static DefaultQualifierAndValue create(TypeName qualifierType, String val) { + return (DefaultQualifierAndValue) builder() + .typeName(qualifierType) + .value(val) + .build(); + } + + /** + * Converts from an {@link io.helidon.pico.types.AnnotationAndValue} to a {@link QualifierAndValue}. + * + * @param annotationAndValue the annotation and value + * @return the qualifier and value equivalent + */ + public static QualifierAndValue convert(AnnotationAndValue annotationAndValue) { + if (annotationAndValue instanceof QualifierAndValue) { + return (QualifierAndValue) annotationAndValue; + } + + return (QualifierAndValue) builder() + .typeName(annotationAndValue.typeName()) + .values(annotationAndValue.values()) + .update(it -> annotationAndValue.value().ifPresent(it::value)) + .build(); + } + + @Override + public int compareTo(AnnotationAndValue other) { + return typeName().compareTo(other.typeName()); + } + + + /** + * Creates a builder for {@link QualifierAndValue}. + * + * @return a fluent builder + */ + public static Builder builder() { + return new Builder(); + } + + + /** + * The fluent builder. + */ + public static class Builder extends DefaultAnnotationAndValue.Builder { + /** + * Fluent builder constructor. + */ + protected Builder() { + } + + /** + * Build the instance. + * + * @return the built instance + */ + @Override + public DefaultQualifierAndValue build() { + return new DefaultQualifierAndValue(this); + } + } + +} diff --git a/pico/pico/src/main/java/io/helidon/pico/DefaultServiceInfo.java b/pico/pico/src/main/java/io/helidon/pico/DefaultServiceInfo.java new file mode 100644 index 00000000000..b1700e54ba4 --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/DefaultServiceInfo.java @@ -0,0 +1,554 @@ +/* + * 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.pico; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Collectors; + +import io.helidon.pico.types.AnnotationAndValue; + +/** + * The default/reference implementation for {@link ServiceInfo}. + */ +public class DefaultServiceInfo implements ServiceInfo { + + private final String serviceTypeName; + private final Set contractsImplemented; + private final Set externalContractsImplemented; + private final Set scopeTypeNames; + private final Set qualifiers; + private final String activatorTypeName; + private final int runLevel; + private final Double weight; + private final String moduleName; + + /** + * Copy constructor. + * + * @param src the source to copy + */ + protected DefaultServiceInfo(ServiceInfo src) { + this.serviceTypeName = src.serviceTypeName(); + this.contractsImplemented = new TreeSet<>(src.contractsImplemented()); + this.externalContractsImplemented = new LinkedHashSet<>(src.externalContractsImplemented()); + this.scopeTypeNames = new LinkedHashSet<>(src.scopeTypeNames()); + this.qualifiers = new LinkedHashSet<>(src.qualifiers()); + this.activatorTypeName = src.activatorTypeName(); + this.runLevel = src.runLevel(); + this.moduleName = src.moduleName().orElse(null); + this.weight = src.declaredWeight().orElse(null); + } + + /** + * Constructor using the builder result. + * + * @param b the builder + * @see #builder() + */ + @SuppressWarnings("unchecked") + protected DefaultServiceInfo(Builder b) { + this.serviceTypeName = b.serviceTypeName; + this.contractsImplemented = Collections.unmodifiableSet(new TreeSet(b.contractsImplemented)); + this.externalContractsImplemented = Collections.unmodifiableSet(b.externalContractsImplemented); + this.scopeTypeNames = Collections.unmodifiableSet(b.scopeTypeNames); + this.qualifiers = Collections.unmodifiableSet(b.qualifiers); + this.activatorTypeName = b.activatorTypeName; + this.runLevel = b.runLevel; + this.moduleName = b.moduleName; + this.weight = b.weight; + } + + /** + * Provides a facade over {@link java.util.Objects#equals(Object, Object)}. + * + * @param o1 an object + * @param o2 an object to compare with a1 for equality + * @return true if a1 equals a2 + */ + public static boolean equals(Object o1, Object o2) { + return Objects.equals(o1, o2); + } + + /** + * Creates a fluent builder for this type. + * + * @return A builder for {@link DefaultServiceInfo}. + */ + @SuppressWarnings("unchecked") + public static Builder> builder() { + return new Builder(); + } + + @Override + public Set externalContractsImplemented() { + return externalContractsImplemented; + } + + @Override + public String activatorTypeName() { + return activatorTypeName; + } + + @Override + public Optional moduleName() { + return Optional.ofNullable(moduleName); + } + + /** + * Matches is a looser form of equality check than {@link #equals(Object, Object)}. If a service matches criteria + * it is generally assumed to be viable for assignability. + * + * @param criteria the criteria to compare against + * @return true if the criteria provided matches this instance + * @see Services#lookup(ServiceInfo) + */ + @Override + public boolean matches(ServiceInfoCriteria criteria) { + if (criteria == PicoServices.EMPTY_CRITERIA) { + return true; + } + + boolean matches = matches(this.serviceTypeName(), criteria.serviceTypeName()); + if (matches && criteria.serviceTypeName().isEmpty()) { + matches = this.contractsImplemented().containsAll(criteria.contractsImplemented()) + || criteria.contractsImplemented().contains(this.serviceTypeName()); + } + return matches + && this.scopeTypeNames().containsAll(criteria.scopeTypeNames()) + && matchesQualifiers(this.qualifiers(), criteria.qualifiers()) + && matches(this.activatorTypeName(), criteria.activatorTypeName()) + && matches(this.runLevel(), criteria.runLevel()) + && matchesWeight(this, criteria) + && matches(this.moduleName(), criteria.moduleName()); + } + + @Override + public String serviceTypeName() { + return serviceTypeName; + } + + @Override + public Set scopeTypeNames() { + return scopeTypeNames; + } + + @Override + public Set qualifiers() { + return qualifiers; + } + + @Override + public Set contractsImplemented() { + return contractsImplemented; + } + + @Override + public int runLevel() { + return runLevel; + } + + @Override + public Optional declaredWeight() { + return Optional.ofNullable(weight); + } + + @Override + public int hashCode() { + return Objects.hash(serviceTypeName(), contractsImplemented()); + } + + @Override + public boolean equals(Object another) { + if (!(another instanceof ServiceInfo)) { + return false; + } + + return equals(serviceTypeName(), ((ServiceInfo) another).serviceTypeName()) + && equals(contractsImplemented(), ((ServiceInfo) another).contractsImplemented()) + && equals(qualifiers(), ((ServiceInfo) another).qualifiers()) + && equals(activatorTypeName(), ((ServiceInfo) another).activatorTypeName()) + && equals(runLevel(), ((ServiceInfo) another).runLevel()) + && equals(realizedWeight(), ((ServiceInfo) another).realizedWeight()) + && equals(moduleName(), ((ServiceInfo) another).moduleName()); + } + + /** + * Creates a fluent builder initialized with the current values of this instance. + * + * @return A builder initialized with the current attributes. + */ + @SuppressWarnings("unchecked") + public Builder> toBuilder() { + return new Builder(this); + } + + /** + * Weight matching is always less or equal to criteria specified. + * + * @param src the item being considered + * @param criteria the criteria + * @return true if there is a match + */ + protected static boolean matchesWeight(ServiceInfoBasics src, ServiceInfoCriteria criteria) { + if (criteria.weight().isEmpty()) { + return true; + } + + Double srcWeight = src.realizedWeight(); + return (srcWeight.compareTo(criteria.weight().get()) <= 0); + } + + /** + * Matches qualifier collections. + * + * @param src the target service info to evaluate + * @param criteria the criteria to compare against + * @return true if the criteria provided matches this instance + */ + private static boolean matchesQualifiers(Set src, Set criteria) { + if (criteria.isEmpty()) { + return true; + } + + if (src.isEmpty()) { + return false; + } + + if (src.contains(DefaultQualifierAndValue.WILDCARD_NAMED)) { + return true; + } + + for (QualifierAndValue criteriaQualifier : criteria) { + if (src.contains(criteriaQualifier)) { + // NOP; + continue; + } else if (criteriaQualifier.typeName().equals(DefaultQualifierAndValue.NAMED)) { + if (criteriaQualifier.equals(DefaultQualifierAndValue.WILDCARD_NAMED) + || criteriaQualifier.value().isEmpty()) { + // any Named qualifier will match ... + boolean hasSameTypeAsCriteria = src.stream() + .anyMatch(q -> q.typeName().equals(criteriaQualifier.typeName())); + if (hasSameTypeAsCriteria) { + continue; + } + } else if (src.contains(DefaultQualifierAndValue.WILDCARD_NAMED)) { + continue; + } + return false; + } else if (criteriaQualifier.value().isEmpty()) { + Set sameTypeAsCriteriaSet = src.stream() + .filter(q -> q.typeName().equals(criteriaQualifier.typeName())) + .collect(Collectors.toSet()); + if (sameTypeAsCriteriaSet.isEmpty()) { + return false; + } + } else { + return false; + } + } + + return true; + } + + private static boolean matches(Object src, Optional criteria) { + if (criteria.isEmpty()) { + return true; + } + + return equals(src, criteria.get()); + } + + /** + * The fluent builder for {@link ServiceInfo}. + * + * @param the builder type + * @param the concrete type being build + */ + public static class Builder> + implements io.helidon.common.Builder { + private final Set contractsImplemented = new LinkedHashSet<>(); + private final Set externalContractsImplemented = new LinkedHashSet<>(); + private final Set scopeTypeNames = new LinkedHashSet<>(); + private final Set qualifiers = new LinkedHashSet<>(); + + private String serviceTypeName; + private String activatorTypeName; + private Integer runLevel; + private String moduleName; + private Double weight; + + /** + * Builder Constructor. + * + * @see #builder() + */ + protected Builder() { + } + + /** + * Builder Copy Constructor. + * + * @param c the existing value object + * @see #toBuilder() + */ + protected Builder(C c) { + this.serviceTypeName = c.serviceTypeName(); + this.contractsImplemented.addAll(c.contractsImplemented()); + this.externalContractsImplemented.addAll(c.externalContractsImplemented()); + this.scopeTypeNames.addAll(c.scopeTypeNames()); + this.qualifiers.addAll(c.qualifiers()); + this.activatorTypeName = c.activatorTypeName(); + this.runLevel = c.runLevel(); + this.moduleName = c.moduleName().orElse(null); + this.weight = c.declaredWeight().orElse(null); + } + + /** + * Builds the {@link DefaultServiceInfo}. + * + * @return the fluent builder instance + */ + @SuppressWarnings("unchecked") + public C build() { + Objects.requireNonNull(serviceTypeName); + + return (C) new DefaultServiceInfo(this); + } + + /** + * Sets the mandatory serviceTypeName for this {@link ServiceInfo}. + * + * @param serviceTypeName the service type name + * @return this fluent builder + */ + public B serviceTypeName(String serviceTypeName) { + this.serviceTypeName = serviceTypeName; + return identity(); + } + + /** + * Sets the mandatory serviceTypeName for this {@link ServiceInfo}. + * + * @param serviceType the service type + * @return this fluent builder + */ + public B serviceType(Class serviceType) { + return serviceTypeName(serviceType.getName()); + } + + /** + * Sets the optional name for this {@link ServiceInfo}. + * + * @param name the name + * @return this fluent builder + */ + public B named(String name) { + return addQualifier(DefaultQualifierAndValue.createNamed(name)); + } + + /** + * Adds a singular qualifier for this {@link ServiceInfo}. + * + * @param qualifier the qualifier + * @return this fluent builder + */ + public B addQualifier(QualifierAndValue qualifier) { + Objects.requireNonNull(qualifier); + qualifiers.add(qualifier); + return identity(); + } + + /** + * Sets the collection of qualifiers for this {@link ServiceInfo}. + * + * @param qualifiers the qualifiers + * @return this fluent builder + */ + public B qualifiers(Collection qualifiers) { + Objects.requireNonNull(qualifiers); + qualifiers.clear(); + this.qualifiers.addAll(qualifiers); + return identity(); + } + + /** + * Adds a singular contract implemented for this {@link ServiceInfo}. + * + * @param contractImplemented the contract implemented + * @return this fluent builder + */ + public B contractImplemented(String contractImplemented) { + Objects.requireNonNull(contractImplemented); + contractsImplemented.add(contractImplemented); + return identity(); + } + + /** + * Adds a contract implemented. + * + * @param contract the contract type + * @return this fluent builder + */ + public B contractTypeImplemented(Class contract) { + return contractImplemented(contract.getName()); + } + + /** + * Sets the collection of contracts implemented for this {@link ServiceInfo}. + * + * @param contractsImplemented the contract names implemented + * @return this fluent builder + */ + public B contractsImplemented(Collection contractsImplemented) { + Objects.requireNonNull(contractsImplemented); + this.contractsImplemented.clear(); + this.contractsImplemented.addAll(contractsImplemented); + return identity(); + } + + /** + * Adds a singular external contract implemented for this {@link ServiceInfo}. + * + * @param contractImplemented the type name of the external contract implemented + * @return this fluent builder + */ + public B addExternalContractImplemented(String contractImplemented) { + Objects.requireNonNull(contractImplemented); + this.externalContractsImplemented.add(contractImplemented); + return contractImplemented(contractImplemented); + } + + /** + * Adds an external contract implemented. + * + * @param contract the external contract type + * @return this fluent builder + */ + public B externalContractTypeImplemented(Class contract) { + return addExternalContractImplemented(contract.getName()); + } + + /** + * Sets the collection of contracts implemented for this {@link ServiceInfo}. + * + * @param contractsImplemented the external contract names implemented + * @return this fluent builder + */ + public B externalContractsImplemented(Collection contractsImplemented) { + Objects.requireNonNull(contractsImplemented); + this.externalContractsImplemented.clear(); + this.externalContractsImplemented.addAll(contractsImplemented); + return identity(); + } + + /** + * Adds a singular scope type name for this {@link ServiceInfo}. + * + * @param scopeTypeName the scope type name + * @return this fluent builder + */ + public B addScopeTypeName(String scopeTypeName) { + Objects.requireNonNull(scopeTypeName); + this.scopeTypeNames.add(scopeTypeName); + return identity(); + } + + /** + * Sets the scope type. + * + * @param scopeType the scope type + * @return this fluent builder + */ + public B scopeType(Class scopeType) { + return addScopeTypeName(scopeType.getName()); + } + + /** + * sets the collection of scope type names declared for this {@link ServiceInfo}. + * + * @param scopeTypeNames the contract names implemented + * @return this fluent builder + */ + public B scopeTypeNames(Collection scopeTypeNames) { + Objects.requireNonNull(scopeTypeNames); + this.scopeTypeNames.clear(); + this.scopeTypeNames.addAll(scopeTypeNames); + return identity(); + } + + /** + * Sets the activator type name. + * + * @param activatorTypeName the activator type name + * @return this fluent builder + */ + public B activatorTypeName(String activatorTypeName) { + this.activatorTypeName = activatorTypeName; + return identity(); + } + + /** + * Sets the activator type. + * + * @param activatorType the activator type + * @return this fluent builder + */ + public B activatorType(Class activatorType) { + return activatorTypeName(activatorType.getName()); + } + + /** + * Sets the run level value. + * + * @param runLevel the run level + * @return this fluent builder + */ + public B runLevel(Integer runLevel) { + this.runLevel = runLevel; + return identity(); + } + + /** + * Sets the module name value. + * + * @param moduleName the module name + * @return this fluent builder + */ + public B moduleName(String moduleName) { + this.moduleName = moduleName; + return identity(); + } + + /** + * Sets the weight value. + * + * @param weight the weight (aka priority) + * @return this fluent builder + */ + public B weight(Double weight) { + this.weight = weight; + return identity(); + } + } + +} diff --git a/pico/pico/src/main/java/io/helidon/pico/DependenciesInfo.java b/pico/pico/src/main/java/io/helidon/pico/DependenciesInfo.java new file mode 100644 index 00000000000..88899a271f4 --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/DependenciesInfo.java @@ -0,0 +1,42 @@ +/* + * 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.pico; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Represents a per {@link ServiceInfo} mapping of {@link DependencyInfo}'s. + */ +public interface DependenciesInfo { + + /** + * Represents the set of dependencies for each {@link ServiceInfo}. + * + * @return map from the service info to its dependencies + */ + Map> serviceInfoDependencies(); + + /** + * Represents a flattened list of all dependencies. + * + * @return the flattened list of all dependencies + */ + List allDependencies(); + +} diff --git a/pico/pico/src/main/java/io/helidon/pico/DependencyInfo.java b/pico/pico/src/main/java/io/helidon/pico/DependencyInfo.java new file mode 100644 index 00000000000..593f273a326 --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/DependencyInfo.java @@ -0,0 +1,44 @@ +/* + * 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.pico; + +import java.util.Set; + +import io.helidon.pico.builder.Builder; + +/** + * Aggregates the set of {@link InjectionPointInfo}'s that are dependent upon a specific and common + * {@link ServiceInfo} definition. + */ +@Builder +public interface DependencyInfo { + + /** + * The service info describing what the injection point dependencies are dependent upon. + * + * @return the service info dependency + */ + ServiceInfo dependencyTo(); + + /** + * The set of injection points that depends upon {@link #dependencyTo()}. + * + * @return the set of dependencies + */ + Set injectionPointDependencies(); + +} diff --git a/pico/pico/src/main/java/io/helidon/pico/ElementInfo.java b/pico/pico/src/main/java/io/helidon/pico/ElementInfo.java new file mode 100644 index 00000000000..e17c2263c59 --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/ElementInfo.java @@ -0,0 +1,137 @@ +/* + * 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.pico; + +import java.util.Optional; +import java.util.Set; + +import io.helidon.pico.builder.Builder; +import io.helidon.pico.types.AnnotationAndValue; + +/** + * Abstractly describes method or field elements of a managed service type (i.e., fields, constructors, injectable methods, etc.). + */ +@Builder +public interface ElementInfo { + + /** + * The name assigned to constructors. + */ + String CONSTRUCTOR = ""; + + /** + * The kind of injection target. + */ + enum ElementKind { + /** + * The injectable constructor. Note that there can be at most 1 injectable constructor. + */ + CONSTRUCTOR, + + /** + * A field. + */ + FIELD, + + /** + * A method. + */ + METHOD + } + + /** + * The access describing the target injection point. + */ + enum Access { + /** + * public. + */ + PUBLIC, + + /** + * protected. + */ + PROTECTED, + + /** + * package private. + */ + PACKAGE_PRIVATE, + + /** + * private. + */ + PRIVATE + } + + /** + * The injection point/receiver kind. + * + * @return the kind + */ + ElementKind elementKind(); + + /** + * The access modifier on the injection point/receiver. + * + * @return the access + */ + Access access(); + + /** + * The element type name (e.g., method type or field type). + * + * @return the target receiver type name + */ + String elementTypeName(); + + /** + * The element name (e.g., method name or field name). + * + * @return the target receiver name + */ + String elementName(); + + /** + * If the element is a method or constructor then this is the ordinal argument position of that argument. + * + * @return the offset argument, 0 based, or empty if field type + */ + Optional elementOffset(); + + /** + * True if the injection point is static. + * + * @return true if static receiver + */ + boolean staticDeclaration(); + + /** + * The enclosing class name for the element. + * + * @return service type name + */ + String serviceTypeName(); + + /** + * The annotations on this element. + * + * @return the annotations on this element + */ + Set annotations(); + +} diff --git a/pico/pico/src/main/java/io/helidon/pico/EventReceiver.java b/pico/pico/src/main/java/io/helidon/pico/EventReceiver.java new file mode 100644 index 00000000000..4516ea1be83 --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/EventReceiver.java @@ -0,0 +1,58 @@ +/* + * 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.pico; + +/** + * A receiver of events from the {@link Services} registry. + *

+ * Note that only {@link ServiceProvider}'s implement this contract that are also bound to the global + * {@link io.helidon.pico.Services} registry are currently capable of receiving events. + * + * @see ServiceProviderBindable + */ +public interface EventReceiver { + + /** + * Events issued from the framework. + */ + enum Event { + + /** + * Called after all modules and services from those modules are initially loaded into the service registry. + */ + POST_BIND_ALL_MODULES, + + /** + * Called after {@link #POST_BIND_ALL_MODULES} to resolve any latent bindings, prior to {@link #SERVICES_READY}. + */ + FINAL_RESOLVE, + + /** + * The service registry is fully populated and ready. + */ + SERVICES_READY + + } + + /** + * Called at the end of module and service bindings, when all the services in the service registry have been populated. + * + * @param event the event + */ + void onEvent(Event event); + +} diff --git a/pico/api/src/main/java/io/helidon/pico/api/ExternalContracts.java b/pico/pico/src/main/java/io/helidon/pico/ExternalContracts.java similarity index 95% rename from pico/api/src/main/java/io/helidon/pico/api/ExternalContracts.java rename to pico/pico/src/main/java/io/helidon/pico/ExternalContracts.java index 9e6a9945c9f..9001c25243a 100644 --- a/pico/api/src/main/java/io/helidon/pico/api/ExternalContracts.java +++ b/pico/pico/src/main/java/io/helidon/pico/ExternalContracts.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.helidon.pico.api; +package io.helidon.pico; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -25,7 +25,7 @@ * Placed on the implementation of a service as an alternative to using a {@link Contract}. *

* Use this annotation when it is impossible to place an annotation on the interface itself - for instance of the interface comes - * from a 3rd party library/provider. + * from a 3rd party library provider. */ @Documented @Retention(RetentionPolicy.RUNTIME) diff --git a/pico/pico/src/main/java/io/helidon/pico/InjectionException.java b/pico/pico/src/main/java/io/helidon/pico/InjectionException.java new file mode 100644 index 00000000000..21c07c1f206 --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/InjectionException.java @@ -0,0 +1,81 @@ +/* + * 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.pico; + +import java.util.Optional; + +/** + * Represents an injection exception. These might be thrown either at compile time or at runtime depending upon how the + * application is built. + */ +public class InjectionException extends PicoServiceProviderException { + + /** + * The optional activation log (configure to enabled). + * + * @see io.helidon.pico.PicoServicesConfig + */ + private final ActivationLog activationLog; + + /** + * Injection, or a required service lookup related exception. + * + * @param msg the message + */ + public InjectionException(String msg) { + super(msg); + this.activationLog = null; + } + + /** + * Injection, or a required service lookup related exception. + * + * @param msg the message + * @param cause the root cause + * @param serviceProvider the service provider + */ + public InjectionException(String msg, Throwable cause, ServiceProvider serviceProvider) { + super(msg, cause, serviceProvider); + this.activationLog = null; + } + + /** + * Injection, or a required service lookup related exception. + * + * @param msg the message + * @param cause the root cause + * @param serviceProvider the service provider + * @param log the optional activity log + */ + public InjectionException(String msg, + Throwable cause, + ServiceProvider serviceProvider, + ActivationLog log) { + super(msg, cause, serviceProvider); + this.activationLog = log; + } + + /** + * Returns the activation log if available. + * + * @return the optional activation log + */ + public Optional activationLog() { + return Optional.ofNullable(activationLog); + } + +} diff --git a/pico/pico/src/main/java/io/helidon/pico/InjectionPointInfo.java b/pico/pico/src/main/java/io/helidon/pico/InjectionPointInfo.java new file mode 100644 index 00000000000..82fddc705e7 --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/InjectionPointInfo.java @@ -0,0 +1,81 @@ +/* + * 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.pico; + +import java.util.Set; + +import io.helidon.pico.builder.Builder; + +/** + * Describes a receiver for injection - identifies who/what is requesting an injection that needs to be satisfied. + */ +@Builder +public interface InjectionPointInfo extends ElementInfo { + + /** + * The identifying name for this injection point. The identity should be unique for the service type it is contained within. + *

+ * This method will return the {@link #baseIdentity()} when {@link #elementOffset()} is null. If not null + * then the elemOffset is part of the returned identity. + * + * @return the unique identity + */ + String identity(); + + /** + * The base identifying name for this injection point. If the element represents a function, then the function arguments + * are encoded in its base identity. + * + * @return the base identity of the element + */ + String baseIdentity(); + + /** + * The qualifiers on this element. + * + * @return The qualifiers on this element. + */ + Set qualifiers(); + + /** + * True if the injection point is of type {@link java.util.List}. + * + * @return true if list based receiver + */ + boolean listWrapped(); + + /** + * True if the injection point is of type {@link java.util.Optional}. + * + * @return true if optional based receiver + */ + boolean optionalWrapped(); + + /** + * True if the injection point is of type Provider (or Supplier). + * + * @return true if provider based receiver + */ + boolean providerWrapped(); + + /** + * The dependency this is dependent upon. + * + * @return The service info we are dependent upon. + */ + ServiceInfo dependencyToServiceInfo(); +} diff --git a/pico/pico/src/main/java/io/helidon/pico/InjectionPointProvider.java b/pico/pico/src/main/java/io/helidon/pico/InjectionPointProvider.java new file mode 100644 index 00000000000..09b2945dd30 --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/InjectionPointProvider.java @@ -0,0 +1,69 @@ +/* + * 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.pico; + +import java.util.List; +import java.util.Optional; + +import jakarta.inject.Provider; + +/** + * Provides ability to contextualize the injected service by the target receiver of the injection point dynamically + * at runtime. This API will provide service instances of type {@code T}. These services may be singleton, or created based upon + * scoping cardinality that is defined by the provider implementation of the given type. This is why the javadoc reads "get (or + * create)". + *

+ * The ordering of services, and the preferred service itself, is determined by the same as documented for + * {@link io.helidon.pico.Services}. + * + * @param the type that the provider produces + */ +public interface InjectionPointProvider extends Provider { + /** + * Get (or create) an instance of this service type using default/empty criteria and context. + * + * @return the best service provider matching the criteria + * @throws io.helidon.pico.PicoException if resolution fails to resolve a match + */ + @Override + default T get() { + return first(PicoServices.SERVICE_QUERY_REQUIRED) + .orElseThrow(() -> new PicoException("Could not resolve a match for " + this)); + } + + /** + * Get (or create) an instance of this service type for the given injection point context. This is logically the same + * as using the first element of the result from calling {@link #list(ContextualServiceQuery)}. + * + * @param query the service query + * @return the best service provider matching the criteria + * @throws io.helidon.pico.PicoException if expected=true and resolution fails to resolve a match + */ + Optional first(ContextualServiceQuery query); + + /** + * Get (or create) a list of instances matching the criteria for the given injection point context. + * + * @param query the service query + * @return the resolved services matching criteria for the injection point in order of weight, or null if the context is not + * supported + */ + default List list(ContextualServiceQuery query) { + return first(query).map(List::of).orElseGet(List::of); + } + +} diff --git a/pico/pico/src/main/java/io/helidon/pico/Injector.java b/pico/pico/src/main/java/io/helidon/pico/Injector.java new file mode 100644 index 00000000000..e6636d6b1a8 --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/Injector.java @@ -0,0 +1,93 @@ +/* + * 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.pico; + +/** + * Used to perform programmatic activation and injection. + *

+ * Note that the reference implementation of Pico only performs non-reflective, compile-time generation of service activators + * for services that it manages. This Injector contract is mainly provided in order to allow other library extension + * implementations to extend the model to perform other types of injection point resolution. + */ +public interface Injector { + /** + * Empty options is the same as passing no options, taking all the default values. + */ + InjectorOptions EMPTY_OPTIONS = DefaultInjectorOptions.builder().build(); + + /** + * The strategy the injector should attempt to apply. The reference implementation for Pico provider only handles + * {@link Injector.Strategy#ACTIVATOR} type. + */ + enum Strategy { + + /** + * Activator based implies compile-time injection strategy. This is the preferred / default strategy. + */ + ACTIVATOR, + + /** + * Reflection based implies runtime injection strategy. Note: This is available for other 3rd parties of Pico that choose + * to use reflection as a strategy. + */ + REFLECTION, + + /** + * Any. Defers the strategy to the provider implementation's capabilities and configuration. + */ + ANY + + } + + /** + * Called to activate and inject a manage service instance or service provider, putting it into + * {@link ActivationPhase#ACTIVE}. + *

+ * Note that if a {@link ServiceProvider} is passed in then the {@link Activator} + * will be used instead. In this case, then any {@link InjectorOptions#startAtPhase()} and + * {@link InjectorOptions#finishAtPhase()} arguments will be ignored. + * + * @param serviceOrServiceProvider the target instance or service provider being activated and injected + * @param opts the injector options, or use {@link #EMPTY_OPTIONS} + * @param the managed service instance type + * @return the result of the activation + * @throws io.helidon.pico.PicoServiceProviderException if an injection or activation problem occurs + * @see Activator + */ + ActivationResult activateInject(T serviceOrServiceProvider, + InjectorOptions opts) throws PicoServiceProviderException; + + /** + * Called to deactivate a managed service or service provider, putting it into {@link ActivationPhase#DESTROYED}. + * If a managed service has a {@link jakarta.annotation.PreDestroy} annotated method then it will be called during + * this lifecycle event. + *

+ * Note that if a {@link ServiceProvider} is passed in then the {@link DeActivator} + * will be used instead. In this case, then any {@link InjectorOptions#startAtPhase()} and + * {@link InjectorOptions#finishAtPhase()} arguments will be ignored. + * + * @param serviceOrServiceProvider the service provider or instance registered and being managed + * @param opts the injector options, or use {@link #EMPTY_OPTIONS} + * @param the managed service instance type + * @return the result of the deactivation + * @throws io.helidon.pico.PicoServiceProviderException if a problem occurs + * @see DeActivator + */ + ActivationResult deactivate(T serviceOrServiceProvider, + InjectorOptions opts) throws PicoServiceProviderException; + +} diff --git a/pico/pico/src/main/java/io/helidon/pico/InjectorOptions.java b/pico/pico/src/main/java/io/helidon/pico/InjectorOptions.java new file mode 100644 index 00000000000..ac35dfb938d --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/InjectorOptions.java @@ -0,0 +1,75 @@ +/* + * 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.pico; + +import java.util.Optional; + +import io.helidon.pico.builder.Builder; + +/** + * Provides optional, contextual tunings to the {@link Injector}. + * + * @see Injector + */ +@Builder +public interface InjectorOptions { + + /** + * The optional starting phase for the {@link Activator} behind the {@link Injector}. + * The default is the current phase that the managed {@link ServiceProvider} is currently in. + * + * @return the optional target finish phase + */ + Optional startAtPhase(); + + /** + * The optional target finishing phase for the {@link Activator} behind the {@link Injector}. + * The default is {@link ActivationPhase#ACTIVE}. + * + * @return the optional target finish phase + */ + Optional finishAtPhase(); + + /** + * The optional recipient target, describing who and what is being injected. + * + * @return the optional target injection point info + */ + Optional ipInfo(); + + /** + * The optional services registry to use, defaulting to {@link PicoServices#services()}. + * + * @return the optional services registry to use + */ + Optional services(); + + /** + * The optional activation log that the injection should record its activity on. + * + * @return the optional activation log to use + */ + Optional log(); + + /** + * The optional injection strategy the injector should apply. The default is {@link Injector.Strategy#ANY}. + * + * @return the optional injector strategy to use + */ + Optional strategy(); + +} diff --git a/pico/api/src/main/java/io/helidon/pico/api/Contract.java b/pico/pico/src/main/java/io/helidon/pico/Intercepted.java similarity index 53% rename from pico/api/src/main/java/io/helidon/pico/api/Contract.java rename to pico/pico/src/main/java/io/helidon/pico/Intercepted.java index ad0027b02d9..eb984cf7b39 100644 --- a/pico/api/src/main/java/io/helidon/pico/api/Contract.java +++ b/pico/pico/src/main/java/io/helidon/pico/Intercepted.java @@ -14,26 +14,33 @@ * limitations under the License. */ -package io.helidon.pico.api; +package io.helidon.pico; import java.lang.annotation.Documented; +import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import jakarta.inject.Qualifier; + /** - * The Contract annotation is used to relay significance to the type. While remaining optional in its use, it is typically placed - * on an interface definition to signify that the given type can be used for lookup in the service registry, or otherwise be - * eligible for injection via standard @Inject. It could also be used on other types (e.g., abstract class) as well. - *

+ * Indicates that type identified by {@link #value()} is being intercepted. * - * If the developer does not have access to the source to place this annotation on the interface definition then consider using - * {@link ExternalContracts} instead - this annotation can be placed on the implementation class implementing the given interface. - * See io.helidon.pico.spi.ServiceInfo#getContractsImplemented() + * @see io.helidon.pico.Interceptor */ @Documented @Retention(RetentionPolicy.RUNTIME) +@Inherited +@Qualifier @Target(java.lang.annotation.ElementType.TYPE) -public @interface Contract { +public @interface Intercepted { + + /** + * The target being intercepted. + * + * @return the target class being intercepted + */ + Class value(); } diff --git a/pico/pico/src/main/java/io/helidon/pico/InterceptedTrigger.java b/pico/pico/src/main/java/io/helidon/pico/InterceptedTrigger.java new file mode 100644 index 00000000000..60fa62a87dc --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/InterceptedTrigger.java @@ -0,0 +1,33 @@ +/* + * 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.pico; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Meta-annotation for an annotation that will trigger services annotated with it to become intercepted. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.ANNOTATION_TYPE) +public @interface InterceptedTrigger { + +} diff --git a/pico/pico/src/main/java/io/helidon/pico/Interceptor.java b/pico/pico/src/main/java/io/helidon/pico/Interceptor.java new file mode 100644 index 00000000000..103f78ac081 --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/Interceptor.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.pico; + +/** + * Implementors of this contract must be {@link jakarta.inject.Named} according to the {@link Intercepted} + * annotation they support. + */ +@Contract +public interface Interceptor { + + /** + * Called during interception of the target V. The implementation typically should finish with the call to + * {@link Interceptor.Chain#proceed()}. + * + * @param ctx the invocation context + * @param chain the chain to call proceed on + * @param the return value type (or {@link Void} for void method elements) + * @return the return value to the caller + */ + V proceed(InvocationContext ctx, Chain chain); + + + /** + * Represents the next in line for interception, terminating with a call to the wrapped service provider. + * + * @param the return value + */ + interface Chain { + /** + * Call the next interceptor in line, or finishing with the call to the service provider. + * + * @return the result of the call. + */ + V proceed(); + } + +} diff --git a/pico/pico/src/main/java/io/helidon/pico/InvocationContext.java b/pico/pico/src/main/java/io/helidon/pico/InvocationContext.java new file mode 100644 index 00000000000..c037f9dc6d4 --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/InvocationContext.java @@ -0,0 +1,80 @@ +/* + * 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.pico; + +import java.util.List; +import java.util.Map; + +import io.helidon.pico.types.AnnotationAndValue; +import io.helidon.pico.types.TypeName; +import io.helidon.pico.types.TypedElementName; + +/** + * Used by {@link Interceptor}. + */ +public interface InvocationContext { + + /** + * The root service provider being intercepted. + * + * @return the root service provider being intercepted + */ + ServiceProvider rootServiceProvider(); + + /** + * The service type name for the root service provider. + * + * @return the service type name for the root service provider + */ + TypeName serviceTypeName(); + + /** + * The annotations on the enclosing type. + * + * @return the annotations on the enclosing type + */ + List classAnnotations(); + + /** + * The element info represents the method (or the constructor) being invoked. + * + * @return the element info represents the method (or the constructor) being invoked + */ + TypedElementName elementInfo(); + + /** + * The method/element argument info. + * + * @return the method/element argument info. + */ + TypedElementName[] elementArgInfo(); + + /** + * The arguments to the method. + * + * @return the read/write method/element arguments + */ + Object[] elementArgs(); + + /** + * The contextual info that can be shared between interceptors. + * + * @return the read/write contextual data that is passed between each chained interceptor + */ + Map contextData(); + +} diff --git a/pico/pico/src/main/java/io/helidon/pico/InvocationException.java b/pico/pico/src/main/java/io/helidon/pico/InvocationException.java new file mode 100644 index 00000000000..99a6538852a --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/InvocationException.java @@ -0,0 +1,35 @@ +/* + * 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.pico; + +/** + * Wraps any checked exceptions that are thrown during the {@link Interceptor} invocations. + */ +public class InvocationException extends PicoServiceProviderException { + + /** + * Constructor. + * + * @param msg the message + * @param cause the root cause + * @param serviceProvider the service provider + */ + public InvocationException(String msg, Throwable cause, ServiceProvider serviceProvider) { + super(msg, cause, serviceProvider); + } + +} diff --git a/pico/pico/src/main/java/io/helidon/pico/Module.java b/pico/pico/src/main/java/io/helidon/pico/Module.java new file mode 100644 index 00000000000..906efb40574 --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/Module.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.pico; + +/** + * Provides aggregation of services to the "containing" (jar) module. + *

+ * Implementations of this contract are normally code generated, although then can be programmatically written by the developer + * for special cases. + *

+ * Note: instances of this type are not eligible for injection. + * + * @see Application + */ +@Contract +public interface Module extends Named { + + /** + * Called by the provider implementation at bootstrapping time to bind all services / service providers to the + * service registry. + * + * @param binder the binder used to register the services to the registry + */ + void configure(ServiceBinder binder); + +} diff --git a/pico/pico/src/main/java/io/helidon/pico/Named.java b/pico/pico/src/main/java/io/helidon/pico/Named.java new file mode 100644 index 00000000000..2e50c17a44f --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/Named.java @@ -0,0 +1,37 @@ +/* + * 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.pico; + +import java.util.Optional; + +/** + * Provides a means to identify if the instance is named. + * + * @see jakarta.inject.Named + */ +public interface Named { + + /** + * The optional name for this instance. + * + * @return the name associated with this instance or empty if not available or known. + */ + default Optional name() { + return Optional.empty(); + } + +} diff --git a/pico/pico/src/main/java/io/helidon/pico/PicoException.java b/pico/pico/src/main/java/io/helidon/pico/PicoException.java new file mode 100644 index 00000000000..6e5d44f3df4 --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/PicoException.java @@ -0,0 +1,48 @@ +/* + * 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.pico; + +/** + * A general purpose exception. + * + * @see PicoServiceProviderException + * @see InjectionException + * @see InvocationException + */ +public class PicoException extends RuntimeException { + + /** + * A general purpose exception from Pico. + * + * @param msg the message + */ + public PicoException(String msg) { + super(msg); + } + + /** + * A general purpose exception from Pico. + * + * @param msg the message + * @param cause the root cause + */ + public PicoException(String msg, + Throwable cause) { + super(msg, cause); + } + +} diff --git a/pico/pico/src/main/java/io/helidon/pico/PicoServiceProviderException.java b/pico/pico/src/main/java/io/helidon/pico/PicoServiceProviderException.java new file mode 100644 index 00000000000..2f66fb07f8d --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/PicoServiceProviderException.java @@ -0,0 +1,90 @@ +/* + * 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.pico; + +import java.util.Objects; +import java.util.Optional; + +/** + * A general purpose exception from Pico. + */ +public class PicoServiceProviderException extends PicoException { + + /** + * The service provider this exception pertains. + */ + private final ServiceProvider serviceProvider; + + /** + * A general purpose exception from Pico. + * + * @param msg the message + */ + public PicoServiceProviderException(String msg) { + super(msg); + this.serviceProvider = null; + } + + /** + * A general purpose exception from Pico. + * + * @param msg the message + * @param cause the root cause + */ + public PicoServiceProviderException(String msg, + Throwable cause) { + super(msg, cause); + + if (cause instanceof PicoServiceProviderException) { + this.serviceProvider = ((PicoServiceProviderException) cause).serviceProvider().orElse(null); + } else { + this.serviceProvider = null; + } + } + + /** + * A general purpose exception from Pico. + * + * @param msg the message + * @param cause the root cause + * @param serviceProvider the service provider + */ + public PicoServiceProviderException(String msg, + Throwable cause, + ServiceProvider serviceProvider) { + super(msg, cause); + Objects.requireNonNull(serviceProvider); + this.serviceProvider = serviceProvider; + } + + /** + * The service provider that this exception pertains to, or empty if not related to any particular provider. + * + * @return the optional / contextual service provider + */ + public Optional> serviceProvider() { + return Optional.ofNullable(serviceProvider); + } + + @Override + public String getMessage() { + return super.getMessage() + + (Objects.isNull(serviceProvider) + ? "" : (": service provider: " + serviceProvider)); + } + +} diff --git a/pico/pico/src/main/java/io/helidon/pico/PicoServices.java b/pico/pico/src/main/java/io/helidon/pico/PicoServices.java new file mode 100644 index 00000000000..1e37b059d30 --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/PicoServices.java @@ -0,0 +1,119 @@ +/* + * 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.pico; + +import java.util.Map; +import java.util.Optional; + +/** + * Abstract factory for all services provided by a single Helidon Pico provider implementation. + * An implementation of this interface must minimally supply a "services registry" - see {@link #services()}. + */ +public interface PicoServices { + /** + * Empty criteria will match anything and everything. + */ + ServiceInfoCriteria EMPTY_CRITERIA = DefaultServiceInfoCriteria.builder().build(); + + /** + * Denotes a match to any (default) service, but required to be matched to at least one. + */ + ContextualServiceQuery SERVICE_QUERY_REQUIRED = DefaultContextualServiceQuery.builder() + .serviceInfo(EMPTY_CRITERIA) + .expected(true) + .build(); + + /** + * Get {@link PicoServices} instance if available. The highest {@link io.helidon.common.Weighted} service will be loaded + * and returned. + * + * @return the Pico services instance + */ + static Optional picoServices() { + return PicoServicesHolder.picoServices(); + } + + /** + * The service registry. + * + * @return the services registry + */ + Services services(); + + /** + * Creates a service binder instance for a specified module. + * + * @param module the module to offer binding to dynamically, and typically only at early startup initialization + * + * @return the service binder capable of binding, or empty if not permitted/available + */ + default Optional createServiceBinder(Module module) { + return Optional.empty(); + } + + /** + * Optionally, the injector. + * + * @return the injector, or empty if not available + */ + default Optional injector() { + return Optional.empty(); + } + + /** + * Optionally, the service providers' configuration. + * + * @return the config, or empty if not available + */ + default Optional config() { + return Optional.empty(); + } + + /** + * Attempts to perform a graceful {@link Injector#deactivate(Object, InjectorOptions)} on all managed + * service instances in the {@link Services} registry. + * Deactivation is handled within the current thread. + *

+ * If the service provider does not support shutdown an empty is returned. + *

+ * The default reference implementation for Pico will return a map of all service types that were deactivated to any + * throwable that was observed during that services shutdown sequence. + *

+ * The order in which services are deactivated is dependent upon whether the {@link #activationLog()} is available. + * If the activation log is available, then services will be shutdown in reverse chronological order as how they + * were started. If the activation log is not enabled or found to be empty then the deactivation will be in reverse + * order of {@link RunLevel} from the highest value down to the lowest value. If two services share + * the same {@link RunLevel} value then the ordering will be based upon the implementation's comparator. + *

+ * When shutdown returns, it is guaranteed that all services were shutdown, or failed to shutdown. + * + * @return a map of all managed service types deactivated to results of deactivation + */ + default Optional>> shutdown() { + return Optional.empty(); + } + + /** + * Optionally, the service provider activation log. + * + * @return the injector, or empty if not available + */ + default Optional activationLog() { + return Optional.empty(); + } + +} diff --git a/pico/pico/src/main/java/io/helidon/pico/PicoServicesConfig.java b/pico/pico/src/main/java/io/helidon/pico/PicoServicesConfig.java new file mode 100644 index 00000000000..fa0e59dfd9e --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/PicoServicesConfig.java @@ -0,0 +1,168 @@ +/* + * 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.pico; + +import java.util.function.Supplier; + +import io.helidon.common.config.Config; + +/** + * Provides optional config by the provider implementation. + */ +@Contract +public interface PicoServicesConfig extends Config { + + /** + * The short name for pico. + */ + String NAME = "pico"; + + /** + * The fully qualified name for pico (used for system properties, etc). + */ + String FQN = "io.helidon." + NAME; + + /** + * The key association with the name of the provider implementation. + */ + String KEY_PROVIDER = FQN + ".provider"; + /** + * The key association with the version of the provider implementation. + */ + String KEY_VERSION = FQN + ".version"; + + /** + * Applicable during activation, this is the key that controls the timeout before deadlock detection errors being thrown. + */ + String KEY_DEADLOCK_TIMEOUT_IN_MILLIS = FQN + ".deadlock.timeout.millis"; + /** + * The default deadlock detection timeout in millis. + */ + long DEFAULT_DEADLOCK_TIMEOUT_IN_MILLIS = 10000L; + + /** + * Applicable for capturing activation logs. + */ + String KEY_ACTIVATION_LOGS_ENABLED = FQN + ".activation.logs.enabled"; + /** + * The default value for this is false, meaning that the activation logs will not be recorded or logged. + */ + boolean DEFAULT_ACTIVATION_LOGS_ENABLED = false; + + /** + * The key that models the services registry, and whether the registry can expand dynamically after program startup. + */ + String KEY_SUPPORTS_DYNAMIC = FQN + ".supports.dynamic"; + /** + * The default value for this is false, meaning that the services registry cannot be changed during runtime. + */ + boolean DEFAULT_SUPPORTS_DYNAMIC = false; + + /** + * The key that represents whether the provider support reflection, and reflection based activation/injection. + */ + String KEY_SUPPORTS_REFLECTION = FQN + ".supports.reflection"; + /** + * The default value for this is false, meaning no reflection is available or provided in the implementation. + */ + boolean DEFAULT_SUPPORTS_REFLECTION = false; + + /** + * Can the provider support compile-time activation/injection (i.e., {@link Activator}'s)? + */ + String KEY_SUPPORTS_COMPILE_TIME = FQN + ".supports.compiletime"; + /** + * The default value is true, meaning injection points are evaluated at compile-time. + */ + boolean DEFAULT_SUPPORTS_COMPILE_TIME = true; + + /** + * Can the services registry activate services in a thread-safe manner? + */ + String KEY_SUPPORTS_THREAD_SAFE_ACTIVATION = FQN + ".supports.threadsafe.activation"; + /** + * The default is true, meaning the implementation is (or should be) thread safe. + */ + boolean DEFAULT_SUPPORTS_THREAD_SAFE_ACTIVATION = true; + + /** + * The key to represent whether the provider support and is compliant w/ Jsr-330. + */ + String KEY_SUPPORTS_JSR330 = FQN + ".supports.jsr330"; + /** + * The default value is true. + */ + boolean DEFAULT_SUPPORTS_JSR330 = true; + + /** + * Can the injector / activator support static injection? Note: this is optional in Jsr-330 + */ + String KEY_SUPPORTS_JSR330_STATIC = FQN + ".supports.jsr330.static"; + /** + * The default value is false. + */ + boolean DEFAULT_SUPPORTS_STATIC = false; + /** + * Can the injector / activator support private injection? Note: this is optional in Jsr-330 + */ + String KEY_SUPPORTS_JSR330_PRIVATE = FQN + ".supports.jsr330.private"; + /** + * The default value is false. + */ + boolean DEFAULT_SUPPORTS_PRIVATE = false; + + /** + * Indicates whether the {@link Module}(s) should be read at startup. The default value is true. + */ + String KEY_BIND_MODULES = FQN + ".bind.modules"; + /** + * The default value is true. + */ + boolean DEFAULT_BIND_MODULES = true; + + /** + * Indicates whether the {@link Application}(s) should be used as an optimization at startup to + * avoid lookups. The default value is true. + */ + String KEY_BIND_APPLICATION = FQN + ".bind.application"; + /** + * The default value is true. + */ + boolean DEFAULT_BIND_APPLICATION = true; + + /** + * Shortcut method to obtain a String with a default value supplier. + * + * @param key configuration key + * @param defaultValueSupplier supplier of default value + * @return value + */ + default String asString(String key, Supplier defaultValueSupplier) { + return get(key).asString().orElseGet(defaultValueSupplier); + } + + /** + * Shortcut method to obtain a String with a default value supplier. + * + * @param key configuration key + * @param defaultValue default value + * @return value + */ + default String asString(String key, String defaultValue) { + return get(key).asString().orElse(defaultValue); + } +} diff --git a/pico/pico/src/main/java/io/helidon/pico/PicoServicesHolder.java b/pico/pico/src/main/java/io/helidon/pico/PicoServicesHolder.java new file mode 100644 index 00000000000..2d638f9def8 --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/PicoServicesHolder.java @@ -0,0 +1,46 @@ +/* + * 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.pico; + +import java.util.Optional; +import java.util.ServiceLoader; + +import io.helidon.common.HelidonServiceLoader; +import io.helidon.common.LazyValue; +import io.helidon.pico.spi.PicoServicesProvider; + +/** + * The holder for the active {@link PicoServices} instance. + */ +class PicoServicesHolder { + private static final LazyValue> INSTANCE = LazyValue.create(PicoServicesHolder::load); + + private PicoServicesHolder() { + } + + static Optional picoServices() { + return INSTANCE.get(); + } + + private static Optional load() { + return HelidonServiceLoader.create(ServiceLoader.load(PicoServicesProvider.class)) + .asList() + .stream() + .findFirst() + .map(PicoServicesProvider::services); + } +} diff --git a/pico/pico/src/main/java/io/helidon/pico/PostConstructMethod.java b/pico/pico/src/main/java/io/helidon/pico/PostConstructMethod.java new file mode 100644 index 00000000000..16f4bfea2df --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/PostConstructMethod.java @@ -0,0 +1,32 @@ +/* + * 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.pico; + +/** + * Represents the {@link jakarta.annotation.PostConstruct} method. + * + * @see Activator + */ +@FunctionalInterface +public interface PostConstructMethod { + + /** + * Represents the post-construct method. + */ + void postConstruct(); + +} diff --git a/pico/pico/src/main/java/io/helidon/pico/PreDestroyMethod.java b/pico/pico/src/main/java/io/helidon/pico/PreDestroyMethod.java new file mode 100644 index 00000000000..a8af213ef75 --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/PreDestroyMethod.java @@ -0,0 +1,32 @@ +/* + * 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.pico; + +/** + * Represents the {@link jakarta.annotation.PreDestroy} method. + * + * @see DeActivator + */ +@FunctionalInterface +public interface PreDestroyMethod { + + /** + * Represents the pre destroy method. + */ + void preDestroy(); + +} diff --git a/pico/pico/src/main/java/io/helidon/pico/QualifierAndValue.java b/pico/pico/src/main/java/io/helidon/pico/QualifierAndValue.java new file mode 100644 index 00000000000..d9b4d4f48e3 --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/QualifierAndValue.java @@ -0,0 +1,37 @@ +/* + * 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.pico; + +import io.helidon.pico.types.AnnotationAndValue; + +/** + * Represents a tuple of the Qualifier and optionally any value. + * + * @see jakarta.inject.Qualifier + */ +public interface QualifierAndValue extends AnnotationAndValue { + + /** + * The qualifier annotation type name. + * + * @return the qualifier/annotation type name + */ + default String qualifierTypeName() { + return typeName().name(); + } + +} diff --git a/pico/api/src/main/java/io/helidon/pico/api/RunLevel.java b/pico/pico/src/main/java/io/helidon/pico/RunLevel.java similarity index 98% rename from pico/api/src/main/java/io/helidon/pico/api/RunLevel.java rename to pico/pico/src/main/java/io/helidon/pico/RunLevel.java index a653e483454..40f60af0418 100644 --- a/pico/api/src/main/java/io/helidon/pico/api/RunLevel.java +++ b/pico/pico/src/main/java/io/helidon/pico/RunLevel.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.helidon.pico.api; +package io.helidon.pico; import java.lang.annotation.Documented; import java.lang.annotation.Inherited; diff --git a/pico/pico/src/main/java/io/helidon/pico/ServiceBinder.java b/pico/pico/src/main/java/io/helidon/pico/ServiceBinder.java new file mode 100644 index 00000000000..11982299c99 --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/ServiceBinder.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.pico; + +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * Responsible for binding service providers to the service registry. + */ +public interface ServiceBinder { + + /** + * Bind a service provider instance into the backing {@link Services} service registry. + * + * @param serviceProvider the service provider to bind into the service registry + */ + void bind(ServiceProvider serviceProvider); + + /** + * Converts the array of contract types to their respective contract names. + * + * @param contractTypes the class types to convert + * + * @return the set of contract names + */ + default Set toContractNames(Class... contractTypes) { + Set result = new LinkedHashSet<>(); + for (Class clazz : contractTypes) { + result.add(clazz.getName()); + } + return result; + } + +} diff --git a/pico/pico/src/main/java/io/helidon/pico/ServiceInfo.java b/pico/pico/src/main/java/io/helidon/pico/ServiceInfo.java new file mode 100644 index 00000000000..ee3f6cccaab --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/ServiceInfo.java @@ -0,0 +1,59 @@ +/* + * 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.pico; + +import java.util.Optional; +import java.util.Set; + +/** + * Describes a managed service or injection point. + */ +public interface ServiceInfo extends ServiceInfoBasics { + + /** + * The managed services external contracts / interfaces. These should also be contained within + * {@link #contractsImplemented()}. External contracts are from other modules other than the module containing + * the implementation typically. + * + * @see io.helidon.pico.ExternalContracts + * @return the service external contracts implemented + */ + Set externalContractsImplemented(); + + /** + * The management agent (i.e., the activator) that is responsible for creating and activating - typically build-time created. + * + * @return the activator type name + */ + String activatorTypeName(); + + /** + * The name of the ascribed module, if known. + * + * @return the module name + */ + Optional moduleName(); + + /** + * Determines whether this service info matches the criteria for injection. + * + * @param criteria the criteria to compare against + * @return true if matches + */ + boolean matches(ServiceInfoCriteria criteria); + +} diff --git a/pico/pico/src/main/java/io/helidon/pico/ServiceInfoBasics.java b/pico/pico/src/main/java/io/helidon/pico/ServiceInfoBasics.java new file mode 100644 index 00000000000..8a20304ed11 --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/ServiceInfoBasics.java @@ -0,0 +1,105 @@ +/* + * 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.pico; + +import java.util.Collections; +import java.util.Optional; +import java.util.Set; + +import io.helidon.common.Weighted; + +import jakarta.inject.Singleton; + +/** + * Basic service info that describes a service provider type. + */ +public interface ServiceInfoBasics extends Weighted { + + /** + * The managed service implementation {@link Class}. + * + * @return the service type name + */ + String serviceTypeName(); + + /** + * The managed service assigned Scope's. + * + * @return the service scope type name + */ + default Set scopeTypeNames() { + return Collections.singleton(Singleton.class.getName()); + } + + /** + * The managed service assigned Qualifier's. + * + * @return the service qualifiers + */ + default Set qualifiers() { + return Set.of(); + } + + /** + * The managed services advertised types (i.e., typically its interfaces). + * + * @see io.helidon.pico.ExternalContracts + * @return the service contracts implemented + */ + default Set contractsImplemented() { + return Set.of(); + } + + /** + * The optional {@link RunLevel} ascribed to the service. + * + * @return the service's run level + */ + default int runLevel() { + return RunLevel.NORMAL; + } + + /** + * Weight that was declared on the type itself. + * + * @return the declared weight + * @see #realizedWeight() + */ + default Optional declaredWeight() { + return Optional.of(weight()); + } + + /** + * The realized weight will use the default weight if no weight was specified directly. + * + * @return the realized weight + * @see #weight() + */ + default double realizedWeight() { + return declaredWeight().orElse(weight()); + } + + /** + * Determines whether this matches the given contract. + * + * @param contract the contract + * @return true if the service type name or the set of contracts implemented equals the provided contract + */ + default boolean matchesContract(String contract) { + return contract.equals(serviceTypeName()) || contractsImplemented().contains(contract); + } +} diff --git a/pico/pico/src/main/java/io/helidon/pico/ServiceInfoCriteria.java b/pico/pico/src/main/java/io/helidon/pico/ServiceInfoCriteria.java new file mode 100644 index 00000000000..1fa862b3b49 --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/ServiceInfoCriteria.java @@ -0,0 +1,100 @@ +/* + * 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.pico; + +import java.util.Optional; +import java.util.Set; + +import io.helidon.pico.builder.Builder; +import io.helidon.pico.builder.Singular; + +/** + * A criteria to discover service. + */ +@Builder +public interface ServiceInfoCriteria { + /** + * The managed service implementation {@link Class}. + * + * @return the service type name + */ + Optional serviceTypeName(); + + /** + * The managed service assigned Scope's. + * + * @return the service scope type name + */ + @Singular + Set scopeTypeNames(); + + /** + * The managed service assigned Qualifier's. + * + * @return the service qualifiers + */ + @Singular + Set qualifiers(); + + /** + * The managed services advertised types (i.e., typically its interfaces). + * + * @see io.helidon.pico.ExternalContracts + * @return the service contracts implemented + */ + @Singular + Set contractsImplemented(); + + /** + * The optional {@link RunLevel} ascribed to the service. + * + * @return the service's run level + */ + Optional runLevel(); + + /** + * Weight that was declared on the type itself. + * + * @return the declared weight + */ + Optional weight(); + + /** + * The managed services external contracts / interfaces. These should also be contained within + * {@link #contractsImplemented()}. External contracts are from other modules other than the module containing + * the implementation typically. + * + * @see io.helidon.pico.ExternalContracts + * @return the service external contracts implemented + */ + @Singular + Set externalContractsImplemented(); + + /** + * The management agent (i.e., the activator) that is responsible for creating and activating - typically build-time created. + * + * @return the activator type name + */ + Optional activatorTypeName(); + + /** + * The name of the ascribed module, if known. + * + * @return the module name + */ + Optional moduleName(); +} diff --git a/pico/pico/src/main/java/io/helidon/pico/ServiceInjectionPlanBinder.java b/pico/pico/src/main/java/io/helidon/pico/ServiceInjectionPlanBinder.java new file mode 100644 index 00000000000..75ecfefaca6 --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/ServiceInjectionPlanBinder.java @@ -0,0 +1,88 @@ +/* + * 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.pico; + +/** + * Responsible for registering the injection plan to the services in the service registry. + */ +public interface ServiceInjectionPlanBinder { + + /** + * Bind an injection plan to a service provider instance. + * + * @param serviceProvider the service provider to receive the injection plan. + * @return the binder to use for binding the injection plan to the service provider + */ + Binder bindTo(ServiceProvider serviceProvider); + + + /** + * The binder builder for the service plan. + * + * @see InjectionPointInfo + */ + interface Binder { + + /** + * Binds a single service provider to the injection point identified by {@link InjectionPointInfo#identity()}. + * It is assumed that the caller of this is aware of the proper cardinality for each injection point. + * + * @param ipIdentity the injection point identity + * @param serviceProvider the service provider to bind to this identity. + * @param the service type + * @return the binder builder + */ + Binder bind(String ipIdentity, ServiceProvider serviceProvider); + + /** + * Binds a list of service providers to the injection point identified by {@link InjectionPointInfo#identity()}. + * It is assumed that the caller of this is aware of the proper cardinality for each injection point. + * + * @param ipIdentity the injection point identity + * @param serviceProviders the list of service providers to bind to this identity. + * @return the binder builder + */ + Binder bindMany(String ipIdentity, + ServiceProvider... serviceProviders); + + /** + * Represents a void / null bind, only applicable for an Optional injection point. + * + * @param ipIdentity the injection point identity + * @return the binder builder + */ + Binder bindVoid(String ipIdentity); + + /** + * Represents injection points that cannot be bound at startup, and instead must rely on a + * deferred resolver based binding. Typically, this represents some form of dynamic or configurable instance. + * + * @param ipIdentity the injection point identity + * @param serviceType the service type needing to be resolved + * @return the binder builder + */ + Binder resolvedBind(String ipIdentity, + Class serviceType); + + /** + * Commits the bindings for this service provider. + */ + void commit(); + + } + +} diff --git a/pico/pico/src/main/java/io/helidon/pico/ServiceProvider.java b/pico/pico/src/main/java/io/helidon/pico/ServiceProvider.java new file mode 100644 index 00000000000..f9938ab9a25 --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/ServiceProvider.java @@ -0,0 +1,133 @@ +/* + * 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.pico; + +import java.util.Optional; + +import io.helidon.common.Weighted; + +import jakarta.inject.Singleton; + +/** + * Provides management lifecycle around services. + * + * @param the type that this service provider manages + */ +public interface ServiceProvider extends InjectionPointProvider, Weighted { + + /** + * Describe the service provider physically and (globally) uniquely. + * + * @return the unique identity description + */ + String identity(); + + /** + * Describe the service provider conceptually. + * + * @return the logical description + */ + String description(); + + /** + * Is the service annotated by @Singleton. + * This is a Helper only, one can alternatively check {@link ServiceInfo#scopeTypeNames()}. + * + * @return true if the service is a singleton + */ + default boolean isSingletonScope() { + return serviceInfo().scopeTypeNames().contains(Singleton.class.getName()); + } + + /** + * Does the service provide singletons, does it always produce the same result for every call to {@link #get()}. + * I.e., if the managed service implements Provider or + * {@link InjectionPointProvider} then this typically is considered not a singleton provider. + * I.e., If the managed services is NOT {@link Singleton}, then it will be treated as per request / dependent + * scope. + * Note that this is similar in nature to RequestScope, except the "official" request scope is bound to the + * web request. Here, we are speaking about contextually any caller asking for a new instance of the service in + * question. The requester in question will ideally be able to identify itself to this provider via + * {@link InjectionPointProvider#first(ContextualServiceQuery)} so that this provider can properly + * service the "provide" request. + * + * @return true if the service provider provides per-request instances for each caller. + */ + boolean isProvider(); + + /** + * The meta information that describes the service. Must remain immutable for the lifetime of the JVM post + * binding - ie., after {@link ServiceBinder#bind(ServiceProvider)} is called. + * + * @return the meta information describing the service + */ + ServiceInfo serviceInfo(); + + /** + * Provides the dependencies for this service provider if known, or null if not known or not available. + * + * @return the dependencies this service provider has or null if unknown or unavailable + */ + DependenciesInfo dependencies(); + + /** + * The current activation phase for this service provider. + * + * @return the activation phase + */ + ActivationPhase currentActivationPhase(); + + /** + * The agent responsible for activation - this will be non-null for build-time activators. If not present then + * an {@link Injector} must be used to reflectively activate. + * + * @return the activator + */ + Activator activator(); + + /** + * The agent responsible for deactivation - this will be non-null for build-time activators. If not present then + * an {@link Injector} must be used to reflectively deactivate. + * + * @return the deactivator to use or null if the service is not interested in deactivation + */ + DeActivator deActivator(); + + /** + * The optional method handling PreDestroy. + * + * @return the post-construct method or empty if there is none + */ + Optional postConstructMethod(); + + /** + * The optional method handling PostConstruct. + * + * @return the pre-destroy method or empty if there is none + */ + Optional preDestroyMethod(); + + /** + * The agent/instance to be used for binding this service provider to the pico application that is class code generated. + * + * @return the service provider that should be used for binding + * @see Module + * @see ServiceBinder + * @see ServiceProviderBindable + */ + ServiceProvider serviceProviderBindable(); +} diff --git a/pico/pico/src/main/java/io/helidon/pico/ServiceProviderBindable.java b/pico/pico/src/main/java/io/helidon/pico/ServiceProviderBindable.java new file mode 100644 index 00000000000..ff27012f9a6 --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/ServiceProviderBindable.java @@ -0,0 +1,121 @@ +/* + * 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.pico; + +import java.util.Optional; + +/** + * An extension to {@link ServiceProvider} that allows for startup binding from a picoApplication, + * and thereby works in conjunction with the {@link ServiceBinder} during pico service registry + * initialization. + *

+ * The only guarantee the provider implementation has is ensuring that {@link Module} instances + * are bound to the pico services instances, as well as informed on the module name. + *

+ * Generally this class should be called internally by the framework, and typically occurs only during initialization sequences. + * + * @param the type that this service provider manages + * @see Application + * @see ServiceProvider#serviceProviderBindable() + */ +public interface ServiceProviderBindable extends ServiceProvider { + + /** + * Called to inform a service provider the module name it is bound to. Will only be called when there is a non-null + * module name associated for the given {@link Module}. A service provider can be associated with + * 0..1 modules. + * + * @param moduleName the non-null module name + */ + void moduleName(String moduleName); + + /** + * Returns {@code true} if this service provider is intercepted. + * + * @return flag indicating whether this service provider is intercepted + */ + default boolean isIntercepted() { + return interceptor().isPresent(); + } + + /** + * Returns the service provider that intercepts this provider. + * + * @return the service provider that intercepts this provider + */ + Optional> interceptor(); + + /** + * Sets the interceptor for this service provider. + * + * @param interceptor the interceptor for this provider + */ + default void interceptor(ServiceProvider interceptor) { + // NOP; intended to be overridden if applicable + throw new UnsupportedOperationException(); + } + + /** + * Gets the root/parent provider for this service. A root/parent provider is intended to manage it's underlying + * providers. Note that "root" and "parent" are interchangeable here since there is at most one level of depth that occurs + * when {@link ServiceProvider}'s are wrapped by other providers. + * + * @return the root/parent provider or empty if this instance is the root provider + */ + default Optional> rootProvider() { + return Optional.empty(); + } + + /** + * Returns true if this provider is the root provider. + * + * @return indicates whether this provider is a root provider - the default is true. + */ + default boolean isRootProvider() { + return rootProvider().isEmpty(); + } + + /** + * Sets the root/parent provider for this instance. + * + * @param rootProvider sets the root provider + */ + default void rootProvider(ServiceProvider rootProvider) { + // NOP; intended to be overridden if applicable + throw new UnsupportedOperationException(); + } + + /** + * The instance of services this provider is bound to. A service provider can be associated with 0..1 services instance. + * If not set, the service provider should use {@link PicoServices#picoServices()} to ascertain the instance. + * + * @param picoServices the pico services instance + */ + default void picoServices(PicoServices picoServices) { + // NOP; intended to be overridden if applicable + } + + /** + * The binder can be provided by the service provider to deterministically set the injection plan at compile-time, and + * subsequently loaded at early startup initialization. + * + * @return binder used for this service provider, or empty if not capable or ineligible of being bound + */ + default Optional injectionPlanBinder() { + return Optional.empty(); + } +} diff --git a/pico/pico/src/main/java/io/helidon/pico/ServiceProviderProvider.java b/pico/pico/src/main/java/io/helidon/pico/ServiceProviderProvider.java new file mode 100644 index 00000000000..f961836797f --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/ServiceProviderProvider.java @@ -0,0 +1,47 @@ +/* + * 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.pico; + +import java.util.List; +import java.util.Map; + +/** + * Instances of these provide lists and maps of {@link ServiceProvider}s. + */ +public interface ServiceProviderProvider { + + /** + * Returns a list of all matching service providers, potentially including itself in the result. + * + * @param criteria the injection point criteria that must match + * @param wantThis if this instance matches criteria, do we want to return this instance as part of the result + * @param thisAlreadyMatches an optimization that signals to the implementation that this instance has already + * matched using the standard service info matching checks + * @return the list of service providers matching + */ + List> serviceProviders(ServiceInfo criteria, boolean wantThis, boolean thisAlreadyMatches); + + /** + * This method will only apply to the managed/slave instances being provided, not to itself as in the case for + * {@link #serviceProviders(ServiceInfo, boolean, boolean)}. + * + * @param criteria the injection point criteria that must match + * @return the map of managed service providers matching the criteria, identified by its key/context + */ + Map> managedServiceProviders(ServiceInfo criteria); + +} diff --git a/pico/pico/src/main/java/io/helidon/pico/Services.java b/pico/pico/src/main/java/io/helidon/pico/Services.java new file mode 100644 index 00000000000..68e8e3f5fed --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/Services.java @@ -0,0 +1,168 @@ +/* + * 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.pico; + +import java.util.List; +import java.util.Optional; + +/** + * The service registry. The service registry generally has knowledge about all the services that are available within your + * application, along with the contracts (i.e., interfaces) they advertise, the qualifiers that optionally describe them, and oll + * of each services' dependencies on other service contracts, etc. + * + * Collectively these service instances are considered "the managed service instances" under Pico. A {@link ServiceProvider} wrapper + * provides lifecycle management on the underlying service instances that each provider "manages" in terms of activation, scoping, + * etc. The service providers are typically created during compile-time processing when the Pico APT processor is applied to your + * module (i.e., any service annotated using {@link jakarta.inject.Singleton}, + * {@link Contract}, {@link jakarta.inject.Inject}, etc.) during compile time. Additionally, they can be built + * using the Pico maven-plugin. Note also that the maven-plugin can be used to "compute" your applications entire DI model + * at compile time, generating an {@link Application} class that will be used at startup when found by the + * Pico framework. + *

+ * This Services interface exposes a read-only set of methods providing access to these "managed service" providers, and available + * via one of the lookup methods provided. Once you resolve the service provider(s), the service provider can be activated by + * calling one of its get() methods. This is equivalent to the declarative form just using {@link jakarta.inject.Inject} instead. + * Note that activation of a service might result in activation chaining. For example, service A injects service B, etc. When + * service A is activated then service A's dependencies (i.e., injection points) need to be activated as well. To avoid long + * activation chaining, it is recommended to that users strive to use {@link jakarta.inject.Provider} injection whenever possible. + * Provider injection (a) breaks long activation chains from occurring by deferring activation until when those services are really + * needed, and (b) breaks circular references that lead to {@link InjectionException} during activation (i.e., + * service A injects B, and service B injects A). + *

+ * The services are ranked according to the provider's comparator. Pico will rank according to a strategy that first looks for + * {@link io.helidon.common.Weighted}, then {@link jakarta.annotation.Priority}, and finally by the alphabetic ordering according + * to the type name (package and class canonical name). + */ +public interface Services { + + /** + * Retrieve the "first" service that implements a given contract type with the expectation that there is a match available. + * + * @param type the type to find + * @param the type of the service + * @return the best service provider matching the criteria + * @throws io.helidon.pico.PicoException if resolution fails to resolve a match + */ + default ServiceProvider lookup(Class type) { + return lookupFirst(type, true).get(); + } + + /** + * Retrieve the "first" named service that implements a given contract type with the expectation that there is a match + * available. + * + * @param type the type to find + * @param name the name for the service + * @param the type of the service + * @return the best service provider matching the criteria + * @throws io.helidon.pico.PicoException if resolution fails to resolve a match + */ + default ServiceProvider lookup(Class type, String name) { + return lookupFirst(type, name, true).get(); + } + + /** + * Retrieve the "first" service that implements a given contract type with no expectation that there is a match available + * unless {@code expected = true}. + * + * @param type the criteria to find + * @param expected indicates whether the provider should throw if a match is not found + * @param the type of the service + * @return the best service provider matching the criteria, or {@code empty} if (@code expected = false) and no match found + * @throws io.helidon.pico.PicoException if expected=true and resolution fails to resolve a match + */ + default Optional> lookupFirst(Class type, boolean expected) { + return lookupFirst(type, null, expected); + } + + /** + * Retrieve the "first" service that implements a given contract type with no expectation that there is a match available + * unless {@code expected = true}. + * + * @param type the criteria to find + * @param name the name for the service + * @param expected indicates whether the provider should throw if a match is not found + * @param the type of the service + * @return the best service provider matching the criteria, or {@code empty} if (@code expected = false) and no match found + * @throws io.helidon.pico.PicoException if expected=true and resolution fails to resolve a match + */ + Optional> lookupFirst(Class type, String name, boolean expected); + + /** + * Retrieves the first match based upon the passed service info criteria. + * + * @param serviceInfo the criteria to find + * @param the type of the service + * @return the best service provider + * @throws io.helidon.pico.PicoException if resolution fails to resolve a match + */ + @SuppressWarnings("unchecked") + default ServiceProvider lookup(ServiceInfo serviceInfo) { + return (ServiceProvider) lookupFirst(serviceInfo, true).get(); + } + + /** + * Retrieves the first match based upon the passed service info criteria. + * + * @param criteria the criteria to find + * @param expected indicates whether the provider should throw if a match is not found + * @param the type of the service + * @return the best service provider matching the criteria, or {@code empty} if (@code expected = false) and no match found + * @throws io.helidon.pico.PicoException if expected=true and resolution fails to resolve a match + */ + Optional> lookupFirst(ServiceInfo criteria, boolean expected); + + /** + * Retrieve all services that implement a given contract type. + * + * @param type the criteria to find + * @param the type of the service + * @return the list of service providers matching criteria + */ + List> lookupAll(Class type); + + /** + * Retrieve all services that match the criteria. + * + * @param criteria the criteria to find + * @param the type of the service + * @return the list of service providers matching criteria + */ + List> lookupAll(ServiceInfo criteria); + + /** + * Retrieve all services that match the criteria. + * + * @param criteria the criteria to find + * @param the type of the service + * @param expected indicates whether the provider should throw if a match is not found + * @return the list of service providers matching criteria + */ + List> lookupAll(ServiceInfo criteria, boolean expected); + + /** + * Implementors can provide a means to use a "special" services registry that better applies to the target injection + * point context to apply for sub-lookup* operations. + * + * @param ctx the injection point context to use to filter the services to what qualifies for this injection point + * @return the qualifying services relative to the given context + */ + default Services contextualServices(InjectionPointInfo ctx) { + return this; + } + +} diff --git a/pico/pico/src/main/java/io/helidon/pico/package-info.java b/pico/pico/src/main/java/io/helidon/pico/package-info.java new file mode 100644 index 00000000000..d19da58e74d --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/package-info.java @@ -0,0 +1,36 @@ +/* + * 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. + */ + +/** + * The Pico API provide these annotation types that are typically used at compile time + * to assign special meaning to the type. It is used in conjunction with Helidon tooling (see the {@code pico-processor} and + * {@code pico-maven-plugin} modules) to create and validate the DI module at compile time. + *

    + *
  • {@link io.helidon.pico.Contract} - signifies that the type can be used for lookup in the service registry.
  • + *
  • {@link io.helidon.pico.ExternalContracts} - same as Contract, but applied to the implementation class instead.
  • + *
  • {@link io.helidon.pico.RunLevel} - ascribes meaning for when the service should start.
  • + *
+ * Also note that the set of annotations from both the {@code jakarta.inject} and {@code jakarta.annotation} modules are the + * primary way to annotate your DI model types. + *

+ * Other types from the API are less commonly used, but are still made available for situations where programmatic access + * is required or desirable in some way. The two most common types for entry into this part of the API are shown below. + *

    + *
  • {@link io.helidon.pico.PicoServices} - suite of services that are typically delivered by the Pico provider.
  • + *
  • {@link io.helidon.pico.Services} - the services registry, which is one such service from this suite.
  • + *
+ */ +package io.helidon.pico; diff --git a/pico/pico/src/main/java/io/helidon/pico/spi/PicoServicesProvider.java b/pico/pico/src/main/java/io/helidon/pico/spi/PicoServicesProvider.java new file mode 100644 index 00000000000..eb4b39aac12 --- /dev/null +++ b/pico/pico/src/main/java/io/helidon/pico/spi/PicoServicesProvider.java @@ -0,0 +1,32 @@ +/* + * 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.pico.spi; + +import io.helidon.pico.PicoServices; + +/** + * Java {@link java.util.ServiceLoader} provider interface to find implementation of {@link io.helidon.pico.PicoServices}. + */ +public interface PicoServicesProvider { + + /** + * Provide services implementation. + * + * @return Pico services + */ + PicoServices services(); +} diff --git a/pico/api/src/main/java/module-info.java b/pico/pico/src/main/java/io/helidon/pico/spi/package-info.java similarity index 88% rename from pico/api/src/main/java/module-info.java rename to pico/pico/src/main/java/io/helidon/pico/spi/package-info.java index bf2711730a4..7af31ae5716 100644 --- a/pico/api/src/main/java/module-info.java +++ b/pico/pico/src/main/java/io/helidon/pico/spi/package-info.java @@ -15,8 +15,6 @@ */ /** - * Pico API module. + * SPI for Pico, to load the implementation. */ -module io.helidon.pico.api { - exports io.helidon.pico.api; -} +package io.helidon.pico.spi; diff --git a/pico/pico/src/main/java/module-info.java b/pico/pico/src/main/java/module-info.java new file mode 100644 index 00000000000..cb89b9efc52 --- /dev/null +++ b/pico/pico/src/main/java/module-info.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. + */ + +/** + * Pico API module. + */ +module io.helidon.pico { + requires io.helidon.common; + requires io.helidon.common.config; + requires io.helidon.pico.types; + requires static io.helidon.pico.builder; + requires static io.helidon.config.metadata; + + requires static jakarta.annotation; + requires jakarta.inject; + + exports io.helidon.pico; + exports io.helidon.pico.spi; + + uses io.helidon.pico.spi.PicoServicesProvider; +} diff --git a/pico/pico/src/test/java/io/helidon/pico/test/PriorityAndServiceTypeComparatorTest.java b/pico/pico/src/test/java/io/helidon/pico/test/PriorityAndServiceTypeComparatorTest.java new file mode 100644 index 00000000000..711d0d82ac1 --- /dev/null +++ b/pico/pico/src/test/java/io/helidon/pico/test/PriorityAndServiceTypeComparatorTest.java @@ -0,0 +1,111 @@ +/* + * 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.pico.test; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import io.helidon.common.Weighted; +import io.helidon.common.Weights; +import io.helidon.pico.test.testsubjects.PicoServices1Provider; +import io.helidon.pico.test.testsubjects.PicoServices2Provider; +import io.helidon.pico.test.testsubjects.PicoServices3Provider; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.lessThan; + +/** + * Ensure the weights comparator from common works the way we expect it to. Sanity test for Pico... + */ +class PriorityAndServiceTypeComparatorTest { + + static final Comparator comparator = Weights.weightComparator(); + + @Test + void ordering() { + assertThat(comparator.compare(null, null), is(0)); + assertThat(comparator.compare(null, null), is(0)); + assertThat(comparator.compare("a", "a"), is(0)); + assertThat(comparator.compare("a", "b"), is(lessThan(0))); + assertThat(comparator.compare("b", "a"), is(greaterThan(0))); + assertThat(comparator.compare(1, 2), is(lessThan(0))); + + assertThat(comparator.compare(new HighWeight(), new HighWeight()), is(0)); + assertThat(comparator.compare(new LowWeight(), new HighWeight()), is(greaterThan(0))); + assertThat(comparator.compare(new LowWeight(), new DefaultWeight()), is(greaterThan(0))); + assertThat(comparator.compare(new DefaultWeight(), new LowWeight()), is(lessThan(0))); + assertThat(comparator.compare(new DefaultWeight(), null), is(lessThan(0))); + assertThat(comparator.compare(null, new DefaultWeight()), is(greaterThan(0))); + assertThat(comparator.compare(new NoWeight(), null), is(lessThan(0))); + assertThat(comparator.compare(null, new NoWeight()), is(greaterThan(0))); + + assertThat(comparator.compare(new DefaultWeight(), new DefaultWeight()), is(0)); + assertThat(comparator.compare(new DefaultWeight(), new NoWeight()), lessThan(0)); + assertThat(comparator.compare(new NoWeight(), new NoWeight()), is(0)); + assertThat(comparator.compare(new NoWeight(), new DefaultWeight()), is(greaterThan(0))); + + assertThat(comparator.compare(new JustAClass(), new JustBClass()), is(lessThan(0))); + assertThat(comparator.compare(new JustBClass(), new JustAClass()), is(greaterThan(0))); + assertThat(comparator.compare(new JustBClass(), new DefaultWeight()), is(greaterThan(0))); + assertThat(comparator.compare(new DefaultWeight(), new JustAClass()), is(lessThan(0))); + assertThat(comparator.compare(null, new JustAClass()), is(greaterThan(0))); + assertThat(comparator.compare(new JustAClass(), null), is(lessThan(0))); + + var list = new ArrayList<>(List.of(new PicoServices1Provider(), + new PicoServices2Provider(), + new PicoServices3Provider())); + list.sort(comparator); + assertThat(list.get(0), instanceOf(PicoServices2Provider.class)); + assertThat(list.get(1), instanceOf(PicoServices3Provider.class)); + assertThat(list.get(2), instanceOf(PicoServices1Provider.class)); + } + + static class DefaultWeight implements Weighted { + } + + static class NoWeight implements Weighted { + public Integer getPriority() { + return null; + } + } + + static class LowWeight implements Weighted { + public Integer getPriority() { + return 1; + } + } + + static class HighWeight implements Weighted { + @Override + public double weight() { + return DEFAULT_WEIGHT + 10; + } + } + + static class JustAClass { + } + + static class JustBClass { + } + +} diff --git a/pico/pico/src/test/java/io/helidon/pico/test/ServiceLoaderTest.java b/pico/pico/src/test/java/io/helidon/pico/test/ServiceLoaderTest.java new file mode 100644 index 00000000000..b42270831ee --- /dev/null +++ b/pico/pico/src/test/java/io/helidon/pico/test/ServiceLoaderTest.java @@ -0,0 +1,45 @@ +/* + * 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.pico.test; + +import io.helidon.pico.PicoServices; +import io.helidon.pico.test.testsubjects.PicoServices2; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Service loader test for Pico. + */ +class ServiceLoaderTest { + + /** + * Test basic loader. + */ + @Test + void testGetPicoServices() { + PicoServices picoServices = PicoServices.picoServices().get(); + assertThat(picoServices, notNullValue()); + assertThat(picoServices, instanceOf(PicoServices2.class)); + assertThat(picoServices, sameInstance(PicoServices.picoServices().get())); + } + +} diff --git a/pico/pico/src/test/java/io/helidon/pico/test/testsubjects/PicoServices1.java b/pico/pico/src/test/java/io/helidon/pico/test/testsubjects/PicoServices1.java new file mode 100644 index 00000000000..221d86fb345 --- /dev/null +++ b/pico/pico/src/test/java/io/helidon/pico/test/testsubjects/PicoServices1.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.pico.test.testsubjects; + +import io.helidon.pico.PicoServices; +import io.helidon.pico.Services; + +import jakarta.inject.Singleton; + +@Singleton +public class PicoServices1 implements PicoServices { + public PicoServices1() { + } + + @Override + public Services services() { + return null; + } + +} diff --git a/pico/pico/src/test/java/io/helidon/pico/test/testsubjects/PicoServices1Provider.java b/pico/pico/src/test/java/io/helidon/pico/test/testsubjects/PicoServices1Provider.java new file mode 100644 index 00000000000..2c9015f8167 --- /dev/null +++ b/pico/pico/src/test/java/io/helidon/pico/test/testsubjects/PicoServices1Provider.java @@ -0,0 +1,29 @@ +/* + * 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.pico.test.testsubjects; + +import io.helidon.common.Weight; +import io.helidon.pico.PicoServices; +import io.helidon.pico.spi.PicoServicesProvider; + +@Weight(1) +public class PicoServices1Provider implements PicoServicesProvider { + @Override + public PicoServices services() { + return new PicoServices1(); + } +} diff --git a/pico/pico/src/test/java/io/helidon/pico/test/testsubjects/PicoServices2.java b/pico/pico/src/test/java/io/helidon/pico/test/testsubjects/PicoServices2.java new file mode 100644 index 00000000000..7da18697e2a --- /dev/null +++ b/pico/pico/src/test/java/io/helidon/pico/test/testsubjects/PicoServices2.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.pico.test.testsubjects; + +import io.helidon.pico.PicoServices; +import io.helidon.pico.Services; + +import jakarta.inject.Singleton; + +@Singleton +public class PicoServices2 implements PicoServices { + public PicoServices2() { + } + + @Override + public Services services() { + return null; + } + +} diff --git a/pico/pico/src/test/java/io/helidon/pico/test/testsubjects/PicoServices2Provider.java b/pico/pico/src/test/java/io/helidon/pico/test/testsubjects/PicoServices2Provider.java new file mode 100644 index 00000000000..d78ebafd2c3 --- /dev/null +++ b/pico/pico/src/test/java/io/helidon/pico/test/testsubjects/PicoServices2Provider.java @@ -0,0 +1,29 @@ +/* + * 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.pico.test.testsubjects; + +import io.helidon.common.Weight; +import io.helidon.pico.PicoServices; +import io.helidon.pico.spi.PicoServicesProvider; + +@Weight(20) +public class PicoServices2Provider implements PicoServicesProvider { + @Override + public PicoServices services() { + return new PicoServices2(); + } +} diff --git a/pico/pico/src/test/java/io/helidon/pico/test/testsubjects/PicoServices3.java b/pico/pico/src/test/java/io/helidon/pico/test/testsubjects/PicoServices3.java new file mode 100644 index 00000000000..051a1d65be4 --- /dev/null +++ b/pico/pico/src/test/java/io/helidon/pico/test/testsubjects/PicoServices3.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.pico.test.testsubjects; + +import io.helidon.pico.PicoServices; +import io.helidon.pico.Services; + +import jakarta.inject.Singleton; + +@Singleton +public class PicoServices3 implements PicoServices { + public PicoServices3() { + } + + @Override + public Services services() { + return null; + } + +} diff --git a/pico/pico/src/test/java/io/helidon/pico/test/testsubjects/PicoServices3Provider.java b/pico/pico/src/test/java/io/helidon/pico/test/testsubjects/PicoServices3Provider.java new file mode 100644 index 00000000000..c6c8bd249a8 --- /dev/null +++ b/pico/pico/src/test/java/io/helidon/pico/test/testsubjects/PicoServices3Provider.java @@ -0,0 +1,29 @@ +/* + * 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.pico.test.testsubjects; + +import io.helidon.common.Weight; +import io.helidon.pico.PicoServices; +import io.helidon.pico.spi.PicoServicesProvider; + +@Weight(3) +public class PicoServices3Provider implements PicoServicesProvider { + @Override + public PicoServices services() { + return new PicoServices3(); + } +} diff --git a/pico/pico/src/test/java/module-info.java b/pico/pico/src/test/java/module-info.java new file mode 100644 index 00000000000..c8c1f97d980 --- /dev/null +++ b/pico/pico/src/test/java/module-info.java @@ -0,0 +1,36 @@ +/* + * 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. + */ + +/** + * Pico API / SPI test module. + */ +open module io.helidon.pico.spi.test { + requires org.junit.jupiter.api; + requires hamcrest.all; + requires jakarta.inject; + requires io.helidon.common; + requires transitive io.helidon.pico; + + uses io.helidon.pico.PicoServices; + + exports io.helidon.pico.test.testsubjects; + exports io.helidon.pico.test; + + provides io.helidon.pico.spi.PicoServicesProvider with + io.helidon.pico.test.testsubjects.PicoServices1Provider, + io.helidon.pico.test.testsubjects.PicoServices2Provider, + io.helidon.pico.test.testsubjects.PicoServices3Provider; +} diff --git a/pico/pom.xml b/pico/pom.xml index d6288a5990e..681bc471128 100644 --- a/pico/pom.xml +++ b/pico/pom.xml @@ -45,10 +45,9 @@ - api - types builder + pico diff --git a/pico/types/src/main/java/io/helidon/pico/types/DefaultAnnotationAndValue.java b/pico/types/src/main/java/io/helidon/pico/types/DefaultAnnotationAndValue.java index 0ee732e9631..ce9281fe304 100644 --- a/pico/types/src/main/java/io/helidon/pico/types/DefaultAnnotationAndValue.java +++ b/pico/types/src/main/java/io/helidon/pico/types/DefaultAnnotationAndValue.java @@ -18,7 +18,6 @@ import java.lang.annotation.Annotation; import java.util.Collection; -import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; @@ -41,7 +40,7 @@ public class DefaultAnnotationAndValue implements AnnotationAndValue, Comparable protected DefaultAnnotationAndValue(Builder b) { this.typeName = b.typeName; this.value = b.value; - this.values = Objects.isNull(b.values) ? Collections.emptyMap() : Collections.unmodifiableMap(b.values); + this.values = Map.copyOf(b.values); } @Override @@ -109,7 +108,9 @@ public Map values() { * @return the new instance */ public static DefaultAnnotationAndValue create(Class annoType) { - return create(annoType, (String) null); + return builder() + .type(annoType) + .build(); } /** @@ -156,67 +157,21 @@ public static DefaultAnnotationAndValue create(TypeName annoTypeName, Map coll, - boolean mustHaveValue) { - assert (!annoTypeName.isBlank()); - Objects.requireNonNull(coll, "collection is required"); - Optional result = coll.stream() - .filter(it -> it.typeName().name().equals(annoTypeName)) - .findFirst(); - if (result.isPresent() && mustHaveValue && !result.get().hasNonBlankValue()) { - result = Optional.empty(); - } - return result.orElseThrow(() -> new AssertionError("Unable to find " + annoTypeName)); - } - /** * Attempts to find the annotation in the provided collection. * * @param annoTypeName the annotation type name * @param coll the collection to search - * @return the result of the find. + * @return the result of the find */ public static Optional findFirst(String annoTypeName, Collection coll) { assert (!annoTypeName.isBlank()); - Objects.requireNonNull(coll, "collection is required"); return coll.stream() .filter(it -> it.typeName().name().equals(annoTypeName)) .findFirst(); } - /** - * The same as calling {@link #findFirst(String, java.util.Collection)} with an added optional check for the value being - * present and non-blank. - * - * @param annoTypeName the annotation type name - * @param coll the collection to search - * @param mustHaveValue true if the result must have a non-blank value - * @return the result of the find. - */ - public static Optional findFirst(TypeName annoTypeName, - Collection coll, - boolean mustHaveValue) { - Objects.requireNonNull(coll, "collection is required"); - Optional result = coll.stream() - .filter(it -> it.typeName().equals(annoTypeName)) - .findFirst(); - if (result.isPresent() && mustHaveValue && !result.get().hasNonBlankValue()) { - result = Optional.empty(); - } - return result; - } - @Override public int compareTo(AnnotationAndValue other) { return typeName().compareTo(other.typeName()); @@ -234,12 +189,13 @@ public static Builder builder() { /** - * The fluent builder. + * Fluent API builder for {@link io.helidon.pico.types.DefaultAnnotationAndValue}. */ - public static class Builder { + public static class Builder implements io.helidon.common.Builder { + private final Map values = new LinkedHashMap<>(); + private TypeName typeName; private String value; - private Map values; /** * Default ctor. @@ -247,6 +203,11 @@ public static class Builder { protected Builder() { } + @Override + public DefaultAnnotationAndValue build() { + return new DefaultAnnotationAndValue(this); + } + /** * Set the type name. * @@ -254,6 +215,7 @@ protected Builder() { * @return this fluent builder */ public Builder typeName(TypeName val) { + Objects.requireNonNull(val); this.typeName = val; return this; } @@ -265,6 +227,7 @@ public Builder typeName(TypeName val) { * @return this fluent builder */ public Builder value(String val) { + Objects.requireNonNull(val); this.value = val; return this; } @@ -276,19 +239,21 @@ public Builder value(String val) { * @return this fluent builder */ public Builder values(Map val) { - this.values = new LinkedHashMap<>(val); + Objects.requireNonNull(val); + this.values.clear(); + this.values.putAll(val); return this; } /** - * Build the instance. + * Annotation type name from annotation type. * - * @return the built instance + * @param annoType annotation class + * @return updated builder */ - public DefaultAnnotationAndValue build() { - Objects.requireNonNull(typeName, "type name is required"); - return new DefaultAnnotationAndValue(this); + public Builder type(Class annoType) { + Objects.requireNonNull(annoType); + return typeName(DefaultTypeName.create(annoType)); } } - } diff --git a/pico/types/src/main/java/io/helidon/pico/types/DefaultTypeName.java b/pico/types/src/main/java/io/helidon/pico/types/DefaultTypeName.java index db226de5a48..8ebaa76e328 100644 --- a/pico/types/src/main/java/io/helidon/pico/types/DefaultTypeName.java +++ b/pico/types/src/main/java/io/helidon/pico/types/DefaultTypeName.java @@ -23,15 +23,10 @@ import java.util.List; import java.util.Objects; -import io.helidon.common.LazyValue; - /** * Default implementation for {@link io.helidon.pico.types.TypeName}. */ public class DefaultTypeName implements TypeName { - private static final boolean CALC_NAME = false; - private final LazyValue name = CALC_NAME ? LazyValue.create(this::calcName) : null; - private final LazyValue fqName = CALC_NAME ? LazyValue.create(this::calcFQName) : null; private final String packageName; private final String className; private final boolean primitive; @@ -53,8 +48,7 @@ protected DefaultTypeName(Builder b) { this.array = b.array; this.wildcard = b.wildcard; this.generic = b.generic; - this.typeArguments = Objects.isNull(b.typeArguments) - ? Collections.emptyList() : Collections.unmodifiableList(b.typeArguments); + this.typeArguments = Collections.unmodifiableList(b.typeArguments); } /** @@ -127,6 +121,7 @@ public static DefaultTypeName createFromGenericDeclaration(String genericAliasTy * @return the TypeName for the provided type name */ public static DefaultTypeName createFromTypeName(String typeName) { + Objects.requireNonNull(typeName); if (typeName.startsWith("? extends ")) { return createFromTypeName(typeName.substring(10).trim()) .toBuilder() @@ -154,7 +149,9 @@ public static DefaultTypeName createFromTypeName(String typeName) { } if (packageElements.isEmpty()) { - return create(null, typeName); + return DefaultTypeName.builder() + .className(typeName) + .build(); } String packageName = String.join(".", packageElements); @@ -165,10 +162,11 @@ public static DefaultTypeName createFromTypeName(String typeName) { * Throws an exception if the provided type name is not fully qualified, having a package and class name representation. * * @param name the type name to check + * @throws java.lang.IllegalStateException if the name is invalid */ public static void ensureIsFQN(TypeName name) { if (!isFQN(name)) { - throw new AssertionError("needs to be a fully qualified name: " + name); + throw new IllegalStateException("needs to be a fully qualified name: " + name); } } @@ -221,7 +219,7 @@ public List typeArguments() { @Override public String name() { - return (null == name) ? calcName() : name.get(); + return calcName(); } @Override @@ -231,7 +229,7 @@ public String declaredName() { @Override public String fqName() { - return (null == fqName) ? calcFQName() : fqName.get(); + return calcFQName(); } /** @@ -240,7 +238,7 @@ public String fqName() { * @return the name */ protected String calcName() { - return (primitive || Objects.isNull(packageName())) + return (primitive || packageName().isEmpty()) ? className() : packageName() + "." + className(); } @@ -250,26 +248,26 @@ protected String calcName() { * @return the fully qualified name */ protected String calcFQName() { - String name = wildcard() ? "? extends " + name() : name(); + StringBuilder nameBuilder = new StringBuilder(wildcard() ? "? extends " + name() : name()); if (null != typeArguments && !typeArguments.isEmpty()) { - name += "<"; + nameBuilder.append("<"); int i = 0; for (TypeName param : typeArguments) { if (i > 0) { - name += ", "; + nameBuilder.append(", "); } - name += param.fqName(); + nameBuilder.append(param.fqName()); i++; } - name += ">"; + nameBuilder.append(">"); } if (array()) { - name += "[]"; + nameBuilder.append("[]"); } - return name; + return nameBuilder.toString(); } @@ -305,14 +303,15 @@ public Builder toBuilder() { /** * The fluent builder. */ - public static class Builder { + public static class Builder implements io.helidon.common.Builder { + private final List typeArguments = new ArrayList<>(); + private String packageName; private String className; private boolean primitive; private boolean array; private boolean wildcard; private boolean generic; - private List typeArguments; /** * Default ctor. @@ -332,8 +331,19 @@ protected Builder(TypeName val) { this.array = val.array(); this.wildcard = val.wildcard(); this.generic = val.generic(); - Collection args = val.typeArguments(); - this.typeArguments = (Objects.isNull(args) || args.isEmpty()) ? Collections.emptyList() : new ArrayList<>(args); + this.typeArguments.addAll(val.typeArguments()); + } + + /** + * Builds the instance. + * + * @return the built instance + */ + public DefaultTypeName build() { + Objects.requireNonNull(className, "Class name must be specified"); + packageName = packageName == null ? "" : packageName; + + return new DefaultTypeName(this); } /** @@ -343,6 +353,7 @@ protected Builder(TypeName val) { * @return this fluent builder */ public Builder packageName(String val) { + Objects.requireNonNull(val); this.packageName = val; return this; } @@ -354,6 +365,7 @@ public Builder packageName(String val) { * @return the fluent builder */ public Builder className(String val) { + Objects.requireNonNull(val); this.className = val; return this; } @@ -365,6 +377,7 @@ public Builder className(String val) { * @return the fluent builder */ public Builder type(Class classType) { + Objects.requireNonNull(classType); Class componentType = classType.isArray() ? classType.getComponentType() : classType; packageName(componentType.getPackageName()); className(componentType.getSimpleName()); @@ -379,6 +392,7 @@ public Builder type(Class classType) { * @return the fluent builder */ public Builder array(boolean val) { + Objects.requireNonNull(val); this.array = val; return this; } @@ -390,6 +404,7 @@ public Builder array(boolean val) { * @return the fluent builder */ public Builder primitive(boolean val) { + Objects.requireNonNull(val); this.primitive = val; return this; } @@ -401,6 +416,7 @@ public Builder primitive(boolean val) { * @return the fluent builder */ public Builder generic(boolean val) { + Objects.requireNonNull(val); this.generic = val; return this; } @@ -412,6 +428,7 @@ public Builder generic(boolean val) { * @return the fluent builder */ public Builder wildcard(boolean val) { + Objects.requireNonNull(val); this.wildcard = val; if (val) { this.generic = true; @@ -426,17 +443,10 @@ public Builder wildcard(boolean val) { * @return the fluent builder */ public Builder typeArguments(Collection val) { - this.typeArguments = Objects.isNull(val) ? null : new ArrayList<>(val); - return (Objects.nonNull(val) && !val.isEmpty()) ? generic(true) : this; - } - - /** - * Builds the instance. - * - * @return the built instance - */ - public DefaultTypeName build() { - return new DefaultTypeName(this); + Objects.requireNonNull(val); + this.typeArguments.clear(); + this.typeArguments.addAll(val); + return !val.isEmpty() ? generic(true) : this; } } diff --git a/pico/types/src/main/java/io/helidon/pico/types/DefaultTypedElementName.java b/pico/types/src/main/java/io/helidon/pico/types/DefaultTypedElementName.java index 8598af1d428..28c6328489b 100644 --- a/pico/types/src/main/java/io/helidon/pico/types/DefaultTypedElementName.java +++ b/pico/types/src/main/java/io/helidon/pico/types/DefaultTypedElementName.java @@ -16,8 +16,7 @@ package io.helidon.pico.types; -import java.util.Collections; -import java.util.LinkedList; +import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -41,14 +40,11 @@ public class DefaultTypedElementName implements TypedElementName { */ protected DefaultTypedElementName(Builder b) { this.typeName = b.typeName; - this.componentTypeNames = Objects.isNull(b.componentTypeNames) - ? Collections.emptyList() : Collections.unmodifiableList(b.componentTypeNames); + this.componentTypeNames = List.copyOf(b.componentTypeNames); this.elementName = b.elementName; this.defaultValue = b.defaultValue; - this.annotations = Objects.isNull(b.annotations) - ? Collections.emptyList() : Collections.unmodifiableList(b.annotations); - this.elementTypeAnnotations = Objects.isNull(b.elementTypeAnnotations) - ? Collections.emptyList() : Collections.unmodifiableList(b.elementTypeAnnotations); + this.annotations = List.copyOf(b.annotations); + this.elementTypeAnnotations = List.copyOf(b.elementTypeAnnotations); } @Override @@ -131,12 +127,13 @@ public static Builder builder() { * The fluent builder. */ public static class Builder { + private final List componentTypeNames = new ArrayList<>(); + private final List annotations = new ArrayList<>(); + private final List elementTypeAnnotations = new ArrayList<>(); + private TypeName typeName; - private List componentTypeNames; private String elementName; private String defaultValue; - private List annotations; - private List elementTypeAnnotations; /** * Default ctor. @@ -172,7 +169,9 @@ public Builder typeName(Class type) { * @return this fluent builder */ public Builder componentTypeNames(List val) { - this.componentTypeNames = Objects.isNull(val) ? Collections.emptyList() : new LinkedList<>(val); + Objects.requireNonNull(val); + this.componentTypeNames.clear(); + this.componentTypeNames.addAll(val); return this; } @@ -205,7 +204,9 @@ public Builder defaultValue(String val) { * @return this fluent builder */ public Builder annotations(List val) { - this.annotations = new LinkedList<>(val); + Objects.requireNonNull(val); + this.annotations.clear(); + this.annotations.addAll(val); return this; } @@ -215,11 +216,9 @@ public Builder annotations(List val) { * @param annotation the annotation to add * @return the fluent builder */ - public Builder annotation(AnnotationAndValue annotation) { - if (Objects.isNull(annotations)) { - this.annotations = new LinkedList<>(); - } - this.annotations.add(Objects.requireNonNull(annotation)); + public Builder addAnnotation(AnnotationAndValue annotation) { + Objects.requireNonNull(annotation); + this.annotations.add(annotation); return this; } @@ -230,7 +229,9 @@ public Builder annotation(AnnotationAndValue annotation) { * @return this fluent builder */ public Builder elementTypeAnnotations(List val) { - this.elementTypeAnnotations = new LinkedList<>(val); + Objects.requireNonNull(val); + this.elementTypeAnnotations.clear(); + this.elementTypeAnnotations.addAll(val); return this; } diff --git a/pico/types/src/main/java/io/helidon/pico/types/TypeName.java b/pico/types/src/main/java/io/helidon/pico/types/TypeName.java index 43399f92cb3..c45f002b470 100644 --- a/pico/types/src/main/java/io/helidon/pico/types/TypeName.java +++ b/pico/types/src/main/java/io/helidon/pico/types/TypeName.java @@ -44,7 +44,7 @@ public interface TypeName extends Comparable { /** * Functions the same as {@link Class#getPackageName()}. * - * @return the package name + * @return the package name, never null */ String packageName(); @@ -83,6 +83,42 @@ public interface TypeName extends Comparable { */ boolean wildcard(); + /** + * Indicates whether this type is a {@code java.util.List}. + * + * @return if this is a list + */ + default boolean isList() { + return "java.util.List".equals(name()); + } + + /** + * Indicates whether this type is a {@code java.util.Set}. + * + * @return if this is a set + */ + default boolean isSet() { + return "java.util.Set".equals(name()); + } + + /** + * Indicates whether this type is a {@code java.util.Map}. + * + * @return if this is a map + */ + default boolean isMap() { + return "java.util.Map".equals(name()); + } + + /** + * Indicates whether this type is a {@code java.util.Optional}. + * + * @return if this is an optional + */ + default boolean isOptional() { + return "java.util.Optional".equals(name()); + } + /** * Returns the list of generic type parameters, or an empty list if no generics are in use. *